Compare commits

...
1765 Commits
Author SHA1 Message Date
Dr.Lt.Data 3f159c5f65 feat(custom-node-list): register ComfyUI-Bernini-PromptEnhancer (§4.3 manual, fix malformed JSON from #3098)
Python Linting / Run Ruff (push) Waiting to run
2026-07-22 08:48:24 +09:00
Dr.Lt.Data d0d8611866 update DB
Python Linting / Run Ruff (push) Canceled after 0s
2026-07-21 21:08:08 +09:00
Dr.Lt.Data 2112b6a569 fix(custom-node-list): MERGE_DEDUP remove pre-registered duplicates (#2152 #3090 #2977 #3043)
Python Linting / Run Ruff (push) Has been cancelled
2026-07-20 20:16:51 +09:00
Dr.Lt.DataandGitHub faa719aaeb Merge pull request #3043 from ukr8b3g-cmyk/add-krea2-bbox-prompter-suite
Add Krea2 BBOX Prompter Suite
2026-07-20 20:13:17 +09:00
Dr.Lt.DataandGitHub 32284a7100 Merge pull request #2977 from yunqiankuangyu/main
Add comfyui-anima-lora-comparison to node list
2026-07-20 20:13:13 +09:00
Dr.Lt.DataandGitHub be017c40b8 Merge pull request #3090 from designloves2/add-itda
Add ITDA to custom-node-list
2026-07-20 20:13:09 +09:00
Dr.Lt.DataandGitHub 79dd5e2358 Merge pull request #2152 from jfcantu/main
New feature: Node Usage Analyzer
2026-07-20 20:13:04 +09:00
Dr.Lt.Data c5c1e1bbc6 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-07-20 12:29:09 +09:00
Dr.Lt.Data 065070a32a update DB
Python Linting / Run Ruff (push) Waiting to run
2026-07-20 03:29:09 +09:00
Dr.Lt.Data a4a9dd54c5 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-07-19 02:29:27 +09:00
Dr.Lt.Data 9c9b882e51 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-07-18 14:22:28 +09:00
Dr.Lt.Data 11f983fa36 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-07-17 21:59:20 +09:00
Dr.Lt.Data 9e73b6b41d update DB
Python Linting / Run Ruff (push) Waiting to run
2026-07-17 16:51:27 +09:00
Dr.Lt.Data bfae406a10 register 4 default-channel nodes (#3083 #3086 #3088 #3089) 2026-07-17 16:45:29 +09:00
Designloves2 7b0ff1afb4 Add ITDA to custom-node-list 2026-07-17 15:09:06 +09:00
ukr8b3g-cmyk 1f288b2f3c Fix existing publisher placeholder 2026-07-16 23:09:01 +09:00
ukr8b3g-cmyk 93f694a9ee Merge origin/main into add-krea2-bbox-prompter 2026-07-16 16:41:22 +09:00
Dr.Lt.Data bd88cffa71 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-07-16 12:41:54 +09:00
Dr.Lt.Data e3ea3e154a update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-07-15 09:45:26 +09:00
Dr.Lt.Data 4ee1ab7991 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-07-14 18:39:35 +09:00
Dr.Lt.Data 8a55772ece fix(custom-node-list): relocate ComfyUI Model Resolver to end (new author) + node_db/new sync 2026-07-14 18:38:28 +09:00
Dr.Lt.DataandGitHub 071688ebb1 Merge pull request #3081 from Azornes/add-comfyui-model-resolver
Add ComfyUI Model Resolver
2026-07-14 18:38:07 +09:00
Dr.Lt.Data 05a75a10b5 update DB 2026-07-14 18:38:01 +09:00
Dr.Lt.Data c58bf0d49c update DB 2026-07-14 15:36:12 +09:00
Dr.Lt.Data 66a408ebb4 update DB 2026-07-14 15:33:16 +09:00
Dr.Lt.Data 997b9e45f4 fix(custom-node-list): relocate ComfyUI-LinkRouter to end (new author) + node_db/new sync 2026-07-14 15:32:01 +09:00
Dr.Lt.DataandGitHub b669845f5e Merge pull request #3054 from 90-RED/main
Add ComfyUI-LinkRouter
2026-07-14 15:30:32 +09:00
Dr.Lt.Data d192cb62c1 update DB 2026-07-14 15:28:25 +09:00
Dariusz L 54014b770d Add ComfyUI Model Resolver 2026-07-14 00:30:17 +02:00
Dr.Lt.Data 351d9c62c4 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-07-12 00:55:29 +09:00
Dr.Lt.Data 297b0b96e9 fix(custom-node-list): relocate Anima-Tools to end (new author) + node_db/new sync 2026-07-11 20:23:59 +09:00
Dr.Lt.DataandGitHub df761bcf27 Merge pull request #3049 from nregret/codex/register-anima-tools
[codex] Register Anima-Tools custom node
2026-07-11 20:23:06 +09:00
Dr.Lt.Data ba2516dc48 update DB 2026-07-11 20:09:49 +09:00
Dr.Lt.Data 377f01ee2f update DB 2026-07-11 20:09:28 +09:00
Dr.Lt.Data 347e37a376 update DB 2026-07-11 20:08:40 +09:00
Dr.Lt.Data 7c2b54ff04 update DB 2026-07-11 20:07:24 +09:00
Dr.Lt.Data 5154e3c070 fix(custom-node-list): relocate Ultimate Lora Loader to end (new author WaitWut) + node_db/new sync 2026-07-11 20:06:03 +09:00
Dr.Lt.DataandGitHub ef8fa07a7d Merge pull request #3070 from WaitWut/add-ultimate-lora-loader
Add Ultimate Lora Loader
2026-07-11 20:05:27 +09:00
Dr.Lt.Data 3c5a948239 fix(custom-node-list): register ComfyUI-ComfySidebar to root (new author) + normalize node_db/new entry 2026-07-11 20:01:33 +09:00
Dr.Lt.DataandGitHub 15204133a3 Merge pull request #3055 from m0rtus59/main
Add new custom node 'ComfyUI-ComfySidebar'
2026-07-11 20:00:27 +09:00
Dr.Lt.Data c7b880c46c fix(custom-node-list): relocate ComfyUI-Supertonic3TTS next to existing Anonymzx block + node_db/new sync 2026-07-11 19:59:12 +09:00
Dr.Lt.DataandGitHub 842bbd5104 Merge pull request #3052 from Anonymzx/main
add ComfyUI-Supertonic3TTS
2026-07-11 19:56:40 +09:00
Dr.Lt.Data ec3f3030f9 updatebD DB 2026-07-11 19:51:09 +09:00
Dr.Lt.Data 619eae0cd6 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-07-11 09:52:47 +09:00
Dr.Lt.Data 7ccc7742c7 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-07-10 12:47:51 +09:00
Eric WilsonandClaude Opus 4.8 f2af615314 Add Ultimate Lora Loader
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 13:28:09 -04:00
Dr.Lt.Data 7aef461132 update db
Python Linting / Run Ruff (push) Waiting to run
2026-07-09 21:33:04 +09:00
yunqiankuangyu 90ba854b1e Merge upstream and keep comfyui-anima-lora-comparison entry 2026-07-09 16:59:00 +08:00
Dr.Lt.Data a2a90adc72 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-07-09 02:36:49 +09:00
Dr.Lt.DataandGitHub e11e9034ce Merge pull request #3058 from kgilper/update-krea-reference-sliders
Python Linting / Run Ruff (push) Has been cancelled
Update ComfyUI Krea Reference description: Concept Slider nodes
2026-07-07 14:23:10 +09:00
Kevin Gilper fb75c048bd Update ComfyUI Krea Reference description: Concept Slider nodes added 2026-07-07 01:01:16 -04:00
Dr.Lt.Data f7fd701e19 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-07-07 04:49:31 +09:00
Dr.Lt.DataandGitHub cf883eb762 Merge pull request #3057 from Azornes/azornes-resolutionmaster-preemption
Add ResolutionMaster preemption for Comfyui-Resolution-Master
2026-07-07 03:49:16 +09:00
Dariusz L 45891097bb Add ResolutionMaster preemption for Comfyui-Resolution-Master 2026-07-06 19:14:59 +02:00
m0rtus59andGitHub 54bd1c3861 Add new custom node 'ComfyUI-ComfySidebar'
Added a new custom node 'ComfyUI-ComfySidebar'
2026-07-06 20:56:54 +05: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
90-RED 80db5bd605 Add ComfyUI-LinkRouter 2026-07-06 05:58:56 +08:00
Anonymzx 14fe358337 add ComfyUI-Supertonic3TTS 2026-07-06 00:53:37 +07: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
YunLong 977ba03dbd Register Anima-Tools custom node 2026-07-04 13:20:45 +08: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
ukr8b3g-cmyk 71fd0affad Add Krea2 BBOX Prompter Suite 2026-07-02 23:36:14 +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
yunqiankuangyu 4ab3d1631e Add comfyui-anima-lora-comparison to node list 2026-06-11 17:34:59 +08: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
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
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
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
John Cantu 1bdcd1bdbf Merge branch 'main' of https://github.com/jfcantu/ComfyUI-Manager 2025-12-14 18:11:34 -08: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
Dr.Lt.Data e4a90089ab fixed: a bug where updating ComfyUI using Update: ComfyUI Stable Version did not updating ComfyUI's dependencies 2025-11-26 21:54:28 +09:00
Dr.Lt.Data 674b9f3705 update DB 2025-11-26 21:41:55 +09:00
Dr.Lt.Data 4941fb8aa0 fixed: scanner.py
Python Linting / Run Ruff (push) Waiting to run
2025-11-26 08:58:02 +09:00
Dr.Lt.Data 183af0dfa5 update DB
Python Linting / Run Ruff (push) Waiting to run
2025-11-25 12:59:01 +09:00
Dr.Lt.Data 45ac5429f8 "update DB" 2025-11-25 12:46:44 +09:00
Dr.Lt.Data c771977a95 update DB
Python Linting / Run Ruff (push) Waiting to run
2025-11-24 23:10:06 +09:00
Dr.Lt.Data 668d7bbb2c update DB 2025-11-24 22:56:38 +09:00
926cfabb58 Add Keybinding Extra (keyboard shortcut extension) (#2306)
* Add Keybinding Extra custom node

Added a new custom node for Keybinding Extra with relevant details.

* Enhance description for Keybinding Extra

Updated the description for the Keybinding Extra to provide more detail about its functionality.

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-11-24 22:55:44 +09:00
Dr.Lt.Data a9a8d05115 update DB 2025-11-24 22:54:26 +09:00
Eric RolleiandGitHub e368f4366a Add Download Tools for ComfyUI (#2298)
Added new download tools for ComfyUI with extensive features for media downloading and web scraping.
2025-11-24 22:51:50 +09:00
Dr.Lt.Data dc5bddbc17 update DB
Python Linting / Run Ruff (push) Waiting to run
2025-11-24 02:00:50 +09:00
358a480408 IcyHider Nodes (#2304)
* Update custom-node-list.json

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-11-24 00:17:22 +09:00
Dr.Lt.Data c96fdb3c7a update DB 2025-11-22 10:36:00 +09:00
Dr.Lt.Data c090abcc02 update DB 2025-11-22 09:46:14 +09:00
kjqwerandGitHub 1ff02be35f add node (#2282)
* add node

* add node
2025-11-22 09:45:21 +09:00
Dr.Lt.Data 10fbfb88f7 update DB 2025-11-22 09:43:20 +09:00
MadiatorLabsandGitHub 9753df72ed Added ComfyUI-RunpodDirect to node list (#2291) 2025-11-22 09:41:54 +09:00
Dr.Lt.Data 095cc3f792 Merge PR #2297: Add PDF Tools and update AAA Metadata System
Resolved merge conflict with PR #2297 by integrating:
- PDF Tools - Advanced PDF Processing & OCR (new entry)
- AAA Metadata System (updated with enhanced description and metadata)
- HYPIR Image Restoration (preserved from main branch)

All entries use consistent spacing and JSON formatting.
2025-11-22 09:33:58 +09:00
Dr.Lt.DataandGitHub 656171037b Update custom-node-list.json
HYPIR-ComfyUI was a separated PR.
2025-11-22 09:28:40 +09:00
Dr.Lt.Data 7ac10f9442 update DB 2025-11-22 09:25:07 +09:00
3925ba27b4 feat: Add HunyuanVideo-1.5 nodes (#2300)
* feat: Add HunyuanVideo-1.5 nodes

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <dr.lt.data@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-11-22 09:23:18 +09:00
Dr.Lt.Data 44ba79aa31 update DB 2025-11-22 09:15:50 +09:00
Eric RolleiandGitHub 14d0e31268 Add HYPIR Image Restoration nodes to custom-node-list (#2299)
Added custom ComfyUI nodes for HYPIR image restoration, including details on author, title, reference, and description.
2025-11-22 09:12:27 +09:00
Dr.Lt.Data 033acffad1 update DB 2025-11-22 08:42:06 +09:00
d29ff808a5 I added my node to the JSON file (#2287)
* Update custom-node-list.json

Added my node to the JSON

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-11-22 08:40:35 +09:00
Dr.Lt.Data dc9b6d655b update DB 2025-11-22 08:40:02 +09:00
Casual GamerandGitHub d340c85013 feat: add ComfyUI Text Processor to node list (#2295) 2025-11-22 08:39:00 +09:00
Dr.Lt.Data e328353664 update DB 2025-11-21 00:33:43 +09:00
Eric RolleiandGitHub 02785af8fd Merge pull request #2 from EricRollei/EricRollei-patch-1
Add HYPIR Image Restoration entry to custom-node-list
2025-11-20 01:39:02 -08:00
Eric RolleiandGitHub 736ae5d63e Add HYPIR Image Restoration entry to custom-node-list
Added a new entry for HYPIR Image Restoration including author, title, reference, files, install type, description, and nodename pattern.
2025-11-20 01:38:39 -08:00
Eric RolleiandGitHub e1eeb617d2 Merge pull request #1 from EricRollei/EricRollei-patch-1
Add AAA Metadata System entry to custom-node-list
2025-11-20 01:34:27 -08:00
Eric RolleiandGitHub 23b6c7f0de Add AAA Metadata System entry to custom-node-list
Added a new entry for the AAA Metadata System with detailed features and installation instructions.
2025-11-20 01:34:04 -08:00
Eric RolleiandGitHub 997f97e1fc Add PDF Tools for advanced PDF processing and OCR
Added a new entry for advanced PDF processing tools, including OCR and image parsing capabilities.
2025-11-20 01:10:01 -08:00
Dr.Lt.Data ff335ff1a0 update DB 2025-11-19 23:12:01 +09:00
Dr.Lt.Data cb3036ef81 modified: scanner.py – updated main so it can be imported 2025-11-19 22:43:28 +09:00
Dr.Lt.Data f762906188 update DB 2025-11-19 22:42:14 +09:00
dde7920f8c Add ComfyUI-Animon node (#2293)
* Add ComfyUI-Animon node

* Update custom-node-list.json

* Remove and re-add ComfyUI-Animon entry in JSON

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-11-19 22:41:13 +09:00
Dr.Lt.Data 1a0d24110a update DB 2025-11-19 22:38:35 +09:00
e79f6c4471 Add new node for ComfyUI_Make-It-Animatable (#2292)
* Add new node for ComfyUI_Make-It-Animatable

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-11-19 22:37:20 +09:00
Dr.Lt.Data a8a7024a84 update DB 2025-11-19 18:46:14 +09:00
Dr.Lt.Data d93d002da0 improved: scanner.py - bugfix about preemption and support extraction only mode 2025-11-19 13:00:26 +09:00
Dr.Lt.Data baaa0479e8 update DB 2025-11-19 01:43:47 +09:00
Dr.Lt.Data cc3bd7a056 update DB 2025-11-18 12:59:21 +09:00
Dr.Lt.Data 4ecefb3b71 improved: scanner.py - supports scanning v3 nodes 2025-11-18 12:48:02 +09:00
Dr.Lt.Data f24b5aa251 update DB 2025-11-17 12:27:39 +09:00
Dr.Lt.Data de547da4cd update DB 2025-11-17 00:37:26 +09:00
Dr.Lt.Data 0f884166a6 update DB 2025-11-16 23:03:10 +09:00
Alan KentandGitHub 63379f759d Added 360 interactive crop tool. (#2285) 2025-11-16 23:02:22 +09:00
Dr.Lt.Data 8fdff20243 update DB 2025-11-16 23:01:34 +09:00
5dfa07ca03 Updates to the existing details (#2286)
* Update custom-node-list.json

* Update custom-node-list.json

* 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-16 23:00:03 +09:00
Dr.Lt.Data 343645be6a update DB 2025-11-16 22:58:58 +09:00
Wei DengandGitHub 91bf21d7a8 Add ComfyUI-MiVolo-V2 node details to JSON (#2283) 2025-11-16 22:57:30 +09:00
Dr.Lt.Data be6516cfd3 update DB 2025-11-15 13:32:22 +09:00
Dr.Lt.Data 61f1e516a3 update DB 2025-11-15 08:27:09 +09:00
73b2278b45 Update custom-node-list.json (#2275)
* Update custom-node-list.json

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-11-15 08:25:44 +09:00
Dr.Lt.Data aa625e30b6 update DB 2025-11-15 08:25:30 +09:00
29a46fe4ce Update custom-node-list.json (#2279)
* Update custom-node-list.json

* Update custom-node-list.json

* Update BoxBox ID in custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-11-15 08:22:12 +09:00
Dr.Lt.Data 5b3ee49530 update DB 2025-11-15 08:21:27 +09:00
Owl-VandGitHub a9158a101f feat: add ComfyUI-MultiTranslator (replaces ComfyUI-Translator) (#2278)
This PR addresses two issues with the previous ComfyUI-Translator entry:

Missing id field — caused the plugin to be invisible in ComfyUI-Manager's Install tab.
Naming conflict — the original name ComfyUI-Translator overlaps with other translation plugins, leading to user confusion.
The repository has been renamed to ComfyUI-MultiTranslator to better reflect its multi-engine capability and avoid conflicts.

This PR removes the old entry and adds a new, fully compliant one with a unique id.
2025-11-15 08:20:31 +09:00
feed8abb34 Add iamccs annotate node — ComfyUI annotation extension (#2254)
* feat: Add IAMCCS-nodes repository (WANAnimate LoRA Loader Fix)

This repo adds a node fixing LoRA loading in native WANAnimate workflows without using WanVideoWrapper. Critical for FLUX/WAN 2.1 users.

* Update custom-node-list.json

* feat: Add IAMCCS annotate node to the list

* Update custom-node-list.json

---------

Co-authored-by: IAMCCS <info@carminecristalloscalzi.com>
Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-11-15 08:16:01 +09:00
Dr.Lt.Data 70decc740f update DB 2025-11-13 00:27:19 +09:00
Dr.Lt.Data 5b5c83f8c5 update DB 2025-11-12 23:52:37 +09:00
Takahiro YanoandGitHub 773c06f40d Add ComfyUI Fast Mosaic Detector node (#2274)
Added a new custom node for ComfyUI that provides high-speed mosaic detection with multiple modes.
2025-11-12 23:51:34 +09:00
Dr.Lt.Data 737e6ad5ed update DB 2025-11-12 23:50:51 +09:00
81bca9c94e Update custom-node-list.json (#2273)
* Update custom-node-list.json

* Modify 'world weaver' node details in JSON

Updated the 'id' and 'install_type' fields for the 'world weaver' node.

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-11-12 23:50:10 +09:00
Dr.Lt.Data eef0654de2 update DB 2025-11-12 23:49:43 +09:00
Dr.Lt.Data 997a00b8a2 update DB 2025-11-12 23:48:24 +09:00
4d25232c5f This adds rafacostcomfy nodes DreamOmni2 GGUF (#2272)
* This adds rafacostcomfy nodes, which adds support to DreamOmni2 GGUF VLM models

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-11-12 23:47:44 +09:00
Dr.Lt.Data 135befa101 update DB 2025-11-12 23:45:41 +09:00
44cac3fc43 Add a set of translator nodes (#2271)
* Add a set of translator nodes

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-11-12 23:44:49 +09:00
Dr.Lt.Data 449fa3510e update DB 2025-11-12 23:42:29 +09:00
d958af8aad Add ComfyUI-Painterl2V to custom-node-list (#2267)
* Update custom-node-list.json

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-11-12 23:41:30 +09:00
Dr.Lt.Data 09f8d5cb2d update DB 2025-11-11 01:56:51 +09:00
Dr.Lt.Data aedc99cefd bump version 2025-11-11 00:42:32 +09:00
unclepomedevandGitHub b32cab6e9a Fix: Gracefully handle errors during pip package enumeration (#2266) 2025-11-11 00:41:16 +09:00
Dr.Lt.Data a95186965e update DB 2025-11-11 00:40:41 +09:00
7067de1bb2 request to add the node comfyui fmj llm in the manager. (#2265)
* Add FMJ-LLM node for Olama interaction

Added a new node for FMJ-LLM with details about its functionality and installation.

* Add FMJ-LLM node for Olama interaction

Added new node for FMJ-LLM with details for installation and usage.

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-11-11 00:39:36 +09:00
Dr.Lt.Data f45d878d21 update DB 2025-11-11 00:39:23 +09:00
a0532b938d Update custom-node-list.json to point to Usonaki fork (#2264)
Co-authored-by: Usonaki <you@example.com>
2025-11-11 00:37:32 +09:00
Dr.Lt.Data 6ad12b7652 update DB 2025-11-11 00:37:15 +09:00
02887c6c9b Add FMJ-speed-Prompt node to custom-node-list (#2263)
* Add FMJ-speed-Prompt node to custom-node-list

Added a new node for FMJ-speed-Prompt with details.

* Fix JSON structure for FMJ-speed-Prompt entry

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-11-11 00:35:29 +09:00
Dr.Lt.Data 1b645e1cc3 update DB 2025-11-11 00:31:40 +09:00
UygarandGitHub 0a4b2a0488 Update custom-node-list.json (#2268)
Added ComfyUI-Artha Nodes
2025-11-11 00:29:52 +09:00
Alex FurerandGitHub d4ce6ddc52 Add ComfyUI AF - Enhanced-HTML-Note node (#2269)
Added new ComfyUI custom node for enhanced HTML notes.
2025-11-11 00:28:44 +09:00
Dr.Lt.Data 5a5b989533 update DB 2025-11-11 00:27:17 +09:00
b57cffb0fa feat: add ComfyUI-Owlv_Nodes (#2270)
* feat: add ComfyUI-Owlv_Nodes

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-11-11 00:25:53 +09:00
Dr.Lt.Data 72aa95cacf update DB 2025-11-10 12:56:58 +09:00
Dr.Lt.Data 14ef448937 update DB 2025-11-08 13:51:59 +09:00
1c10607c06 Add My Comfyui-TextLine-counter node (#2253)
* Update custom-node-list.json

* Update custom-node-list.json

* Update custom-node-list.json

* Remove 'id' field from Comfyui-TextLine-counter

Removed 'id' field from the Comfyui-TextLine-counter entry.

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-11-08 13:40:13 +09:00
Dr.Lt.Data 41e53a1f2a update DB 2025-11-08 13:40:04 +09:00
cde83e9a38 Add ComfyUI-LinkModeToggle custom node (#2260)
* Update custom-node-list.json

* Update custom-node-list.json

Renamed LinkModeToggle to ComfyUI-LinkModeToggle.

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-11-08 13:36:58 +09:00
Dr.Lt.Data 6c206b1c72 update DB 2025-11-08 13:36:48 +09:00
Hansheng ChenandGitHub a474219e7b Add ComfyUI-piFlow node to custom-node-list.json (#2261) 2025-11-08 13:35:57 +09:00
Dr.Lt.Data 3b8d25eb31 update DB 2025-11-08 13:34:23 +09:00
0faa0aa668 Add multiple angle camera control node (#2262)
* Add multiple angle camera control node

Added a new node for multiple angle camera control with detailed description and installation type.

* Remove duplicate entry for Multiple-Angle-Camera-Control

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-11-08 13:33:08 +09:00
Dr.Lt.Data 0fcdcc93a2 update DB 2025-11-07 12:37:02 +09:00
Dr.Lt.Data 461d5e72fe update DB 2025-11-07 00:42:18 +09:00
Dr.Lt.Data 3f030a2121 update DB 2025-11-06 00:37:25 +09:00
Dr.Lt.Data 7fb8e8662f update DB 2025-11-05 07:48:33 +09:00
Dr.Lt.Data dd3ab9cff2 update DB 2025-11-05 00:33:32 +09:00
Dr.Lt.Data 97b518de12 update DB 2025-11-04 23:25:04 +09:00
Dr.Lt.Data d2a795c866 update DB 2025-11-04 07:54:26 +09:00
8a3d65be20 Add ComfyUI-GRAG-ArchAi3D custom node (#2252)
* Add ComfyUI-ArchAi3d-Qwen custom nodes

* Add new node entry for ComfyUI-GRAG-ArchAi3D

Adding GRAG (Guided Region-Adaptive Guidance) custom node with:
- Simple and Unified controllers
- Advanced sampler
- 54 presets
- Per-layer and adaptive control

* Update custom-node-list.json

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-11-04 07:32:06 +09:00
Dr.Lt.Data b2126d8ba5 update DB 2025-11-03 18:23:26 +09:00
Dr.Lt.Data 6386411d21 update DB 2025-11-03 12:33:07 +09:00
Dr.Lt.Data 4250244136 update DB 2025-11-03 07:40:01 +09:00
Dr.Lt.Data 77c4f9993d update DB 2025-11-02 12:26:36 +09:00
Dr.Lt.Data c7c8417577 update DB 2025-11-02 08:49:24 +09:00
Dr.Lt.Data 9d0985ded8 update DB 2025-11-01 13:39:08 +09:00
3663e10e33 Change Custom Node github address (#2247)
* Add MultiSaveImage custom node

* feat: change nodes github address and name

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-11-01 11:55:55 +09:00
Dr.Lt.Data 5f37a82c3c update DB 2025-11-01 11:47:07 +09:00
Dr.Lt.Data 026bf1dfd7 update DB 2025-10-31 12:58:49 +09:00
Dr.Lt.Data 643a6e5080 update DB 2025-10-31 08:01:20 +09:00
Alex FurerandGitHub 5267502896 Remove AF - Edit Generated Prompt node entry (#2245)
Removed 'AF - Edit Generated Prompt' node entry from the custom node list, as it's being migrated to a node pack and summitted via official registry
2025-10-31 07:41:26 +09:00
Dr.Lt.Data c3c152122d update DB 2025-10-30 07:53:35 +09:00
Dr.Lt.Data afeac097e5 update DB 2025-10-29 07:45:00 +09:00
Roberts SlisansandGitHub e5cea64132 Add TTS WebUI API nodes (#2243) 2025-10-29 07:44:16 +09:00
Dr.Lt.Data 26da78cf15 update DB 2025-10-27 12:57:21 +09:00
Dr.Lt.Data 179a1e1ca0 update DB 2025-10-26 14:27:17 +09:00
dehypnoticandGitHub b379d275d1 Add Dehypnotic Save nodes to custom-node-list.json (#2221)
Added new node entry for Dehypnotic Save nodes with details.
2025-10-26 14:22:22 +09:00
Dr.Lt.Data 133cdfb203 update DB 2025-10-26 13:10:07 +09:00
Dr.Lt.Data 2b79edd9be update DB 2025-10-26 13:03:24 +09:00
3862a92e04 Add ComfyUI-CCXManager node (#2222)
* Add ComfyUI-CCXManager node

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-10-26 13:02:01 +09:00
Dr.Lt.Data f4e3817fcc update DB 2025-10-26 13:01:34 +09:00
Novice_ChenandGitHub 61f0f5d67c Added ComfyUI-Simple-IndexTTs node (#2236) 2025-10-26 13:00:26 +09:00
Dr.Lt.Data 87f57551ea update DB 2025-10-26 13:00:06 +09:00
ee51efed69 Update custom-node-list.json (#2238)
* Update custom-node-list.json

* Simplify node entry by removing file links

Removed specific file links and custom installer from the node list entry.

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-10-26 12:58:30 +09:00
wzgrxandGitHub 5dab865681 Update requirements.txt (#2242) 2025-10-26 12:57:49 +09:00
Dr.Lt.Data 8c0581eebc update DB 2025-10-26 12:52:31 +09:00
Dr.Lt.Data a72f9f422c update DB 2025-10-25 11:09:04 +09:00
Dr.Lt.Data 1354a8c970 update DB 2025-10-24 12:25:01 +09:00
Dr.Lt.Data 00a5115267 update DB 2025-10-23 20:45:14 +09:00
EdgerunnerandGitHub 00282eab7b Added ComfyUI Queue Manager node (#2235) 2025-10-23 20:40:11 +09:00
Dr.Lt.Data bec128de58 update DB 2025-10-23 20:16:27 +09:00
Dr.Lt.Data 9edfa7b4fa update DB 2025-10-23 07:59:17 +09:00
Dr.Lt.Data a9af70e5f0 update DB 2025-10-23 06:59:38 +09:00
Dr.Lt.Data 910caf593f update DB 2025-10-23 06:59:22 +09:00
LeechKingandGitHub 02dc072dc7 Add ComfyUI Channel Ops custom node entry (#2239) 2025-10-23 06:58:15 +09:00
Dr.Lt.Data 78fb354452 update DB 2025-10-21 12:32:28 +09:00
Dr.Lt.Data 66f5eca7fa update DB 2025-10-21 07:53:50 +09:00
Dr.Lt.Data be95396a57 update DB 2025-10-20 18:47:13 +09:00
Dr.Lt.Data 59cbed429f update DB 2025-10-20 12:47:53 +09:00
Dr.Lt.Data d49df7aebb update DB 2025-10-20 07:43:35 +09:00
Dr.Lt.Data 0aa9faad2e update DB 2025-10-20 06:57:50 +09:00
1337def888 Add OmiXDev Custom Nodes collection for Online-Chat ( OpenAI , grok ,.. ) with API Key (#2200)
* Update custom-node-list.json

* 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-10-20 06:08:04 +09:00
Dr.Lt.Data 4b100c558b update DB 2025-10-19 12:01:07 +09:00
Dr.Lt.Data 1425a71ece update DB 2025-10-19 10:49:22 +09:00
Dr.Lt.Data a8524508fe update DB 2025-10-17 23:01:11 +09:00
Dr.Lt.Data a5ff973d53 update DB 2025-10-17 07:55:16 +09:00
Dr.Lt.Data 337c9aa2c7 update DB 2025-10-16 18:34:03 +09:00
Dr.Lt.Data f1448403ac update DB 2025-10-16 12:46:10 +09:00
Dr.Lt.Data d0b5f77ec6 update DB 2025-10-16 12:16:26 +09:00
Dr.Lt.Data 9cb22ffb60 update DB 2025-10-16 07:45:03 +09:00
KarryCharonandGitHub f556962d82 Add Blender-IO extension (#2223) 2025-10-16 06:46:16 +09:00
Dr.Lt.Data d28448d519 update DB 2025-10-15 07:47:45 +09:00
Casual GamerandGitHub c590a88ffd feat: Add ComfyUI-Danbooru-Tags-Upsampler (#2220) 2025-10-15 06:50:07 +09:00
Dr.Lt.Data a1fc6c817b update DB 2025-10-14 12:30:05 +09:00
Dr.Lt.Data 5554e52799 update DB 2025-10-14 08:39:13 +09:00
Dr.Lt.Data ca749eb4d2 update DB 2025-10-14 07:22:26 +09:00
MichaelandGitHub 41ceee3d24 Add ML_nodes to ComfyUI Manager registry (#2219)
* Update custom-node-list.json

add ML_nodes

* Update custom-node-list.json

fix imstall_type
2025-10-14 07:21:01 +09:00
Dr.Lt.Data 5acfd52986 update DB 2025-10-14 07:20:45 +09:00
ec4c7b2f6a feat: Add IAMCCS-nodes repository (WANAnimate LoRA Loader Fix) (#2218)
* feat: Add IAMCCS-nodes repository (WANAnimate LoRA Loader Fix)

This repo adds a node fixing LoRA loading in native WANAnimate workflows without using WanVideoWrapper. Critical for FLUX/WAN 2.1 users.

* Update custom-node-list.json

---------

Co-authored-by: IAMCCS <info@carminecristalloscalzi.com>
Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-10-14 07:14:33 +09:00
Dr.Lt.Data 22a3d8f95f update DB 2025-10-13 12:48:21 +09:00
Dr.Lt.Data 06b89ca277 update DB 2025-10-13 08:39:08 +09:00
Dr.Lt.Data 9e5ffbd00a update DB 2025-10-13 07:36:15 +09:00
Dr.Lt.Data 39e92ed778 update DB 2025-10-13 06:30:24 +09:00
68a3ec567a Add comfy-deploy node to the custom node list and extension note map list (#2215)
* Update custom-node-list.json

* Update extension-node-map.json

* Update extension-node-map.json

* Update extension-node-map.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-10-13 06:29:38 +09:00
Dr.Lt.Data 28231e81b3 update DB 2025-10-13 06:25:59 +09:00
ChrisandGitHub b2ee0feeaa Update custom-node-list.json (#2209)
Added comfyui-seedvr2-tilingupscaler
2025-10-13 06:25:23 +09:00
Dr.Lt.Data 5541b6b366 update DB 2025-10-13 06:25:03 +09:00
Amir FerdosandGitHub 408a5fe27e Add ComfyUI-ArchAi3d-Qwen custom nodes (#2214) 2025-10-13 06:23:41 +09:00
Dr.Lt.Data bffc73f976 update DB 2025-10-13 06:23:21 +09:00
bd6edfc9dd Add PortraitUtils to custom-node-list.json (#2216)
* Update custom-node-list.json

* 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-10-13 06:21:28 +09:00
Hardly WorkingandGitHub 2cb24e8a94 Update custom-node-list.json (#2217) 2025-10-13 06:16:52 +09:00
Dr.Lt.Data a49779c4d2 update DB 2025-10-13 06:14:21 +09:00
kj863257rcandGitHub 15a5a5f5df Update custom-node-list.json (#2213)
Add reference information
2025-10-13 06:12:43 +09:00
Sonny BoxandGitHub b5e0558d6e Update custom-node-list.json (#2212) 2025-10-13 06:12:00 +09:00
4d683b23fc Include new node (#2211)
* Include New node

* Update custom-node-list.json

* 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-10-13 06:11:13 +09:00
SBCODEandGitHub c13da606b2 Update custom-node-list.json with a Video Reverse Tool (#2210)
Add a reference to a tool for reversing the image batch tensor of video type files such as MP4, WebM, WebP, Animated Gif.

Also added tags to my other custom nodes Virtual Camera and Image Compare.
2025-10-13 06:09:43 +09:00
Dr.Lt.Data c792f9277c update DB 2025-10-10 09:03:13 +09:00
Dr.Lt.Data b430f42622 update DB 2025-10-10 08:27:54 +09:00
fee822f5ae feat: updated models-list to add support for Qwen Images models (#2204)
* feat: updated models-list to add support for Qwen Images models

* fix: give back orginal spacing

---------

Co-authored-by: remote-dev <you@example.com>
2025-10-10 08:23:26 +09:00
SBCODEandGitHub 192659ecbd Update custom-node-list.json (#2205)
Added an option for ComfyUI Image Compare.
A no frills image comparing tool. 
Compare two images using ComfyUI. 
Connect images to both image_a and image_b inputs. 
Press RUN. 
Then use the slider to compare.
2025-10-10 08:21:56 +09:00
810431b9e2 Adding PiePieTweaks custom nodes (#2206)
Co-authored-by: Imbrium201 <133581634+imbrium201@users.noreply.github.com>
2025-10-10 08:21:42 +09:00
Dr.Lt.Data 02d845adf3 update DB 2025-10-10 08:18:51 +09:00
Dr.Lt.Data 89c7b960fb update scanner.py 2025-10-10 08:03:52 +09:00
Dr.Lt.Data ed1e399a56 update DB 2025-10-10 01:56:14 +09:00
Dr.Lt.Data 8a3ce1ae57 update DB 2025-10-10 00:41:43 +09:00
sumitchatterjee13andGitHub d89ff649f8 Removed HDR Vae Decode node (#2202)
Due to my company policy, my company asked me to remove this node and asked me not to share the codes, so I removed the nodes.
2025-10-10 00:07:01 +09:00
Dr.Lt.Data 24a73b5d1c update DB 2025-10-09 11:56:44 +09:00
Dr.Lt.Data 4d0c40ff8a update DB 2025-10-09 10:57:02 +09:00
Dr.Lt.Data b5a2bed539 update DB 2025-10-08 12:16:22 +09:00
dehypnoticandGitHub 0efb79f571 Update custom-node-list.json (#2199) 2025-10-08 11:07:40 +09:00
Seungyong LeeandGitHub df944b9a0f Update custom-node-list.json (#2198) 2025-10-08 11:06:50 +09:00
2c11846430 Add new node - Character and Word counter (#2195)
* Include New node

* Update custom-node-list.json

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-10-08 11:05:24 +09:00
Dr.Lt.Data 0035c01186 update DB 2025-10-07 14:29:49 +09:00
snicolastandGitHub 34be3384fe new node: Lightweight ComfyUI wrapper for OVI (#2196) 2025-10-07 13:19:36 +09:00
Dr.Lt.Data ebbc7b3335 update DB 2025-10-07 13:18:57 +09:00
TEKUA@AITECCAFEandGitHub 4ccc8c3086 Update custom-node-list.json (#2194)
Add MyCustomNode.
ComfyUI_AITECCAFE_Toolkit
2025-10-07 13:17:35 +09:00
Dr.Lt.Data af9ebc9568 update DB 2025-10-05 09:15:49 +09:00
Dr.Lt.Data ca4b61c5f0 update DB 2025-10-04 07:26:25 +09:00
Dr.Lt.Data 393839b3ab update DB 2025-10-03 12:54:02 +09:00
Dr.Lt.Data dadfc96e00 update DB 2025-10-03 11:10:48 +09:00
u5devandGitHub a0a33aef03 Add: u5 VramFREE to custom-node list (#2191)
Register "u5 VramFREE" (repo: https://github.com/u5dev/comfyUI_u5_VramFREE).
Utility node for freeing VRAM and sequential model loading.
Tags: utility, vram, memory, optimization.
2025-10-03 10:49:12 +09:00
Dr.Lt.Data 99ed81e0f5 update DB 2025-10-02 12:58:04 +09:00
Dr.Lt.Data 5b697db219 update DB 2025-10-02 07:45:30 +09:00
rubyrubypandGitHub 8e5bf46e14 Fix spelling in font definition (#2189) 2025-10-02 07:36:57 +09:00
Dr.Lt.Data 9f649b0900 update DB 2025-10-01 12:32:02 +09:00
Dr.Lt.Data abb15e06d3 update DB 2025-10-01 07:51:07 +09:00
Dr.Lt.Data 11a317493e update DB 2025-10-01 00:35:12 +09:00
InolandandGitHub e8cece0c1b Add ComfyUI Ino nodes (#2187)
https://github.com/nobandegani/comfyui_ino_nodes
2025-10-01 00:10:39 +09:00
Dr.Lt.Data 1ab882f81d update DB 2025-09-30 12:45:40 +09:00
Dr.Lt.Data b9338186e3 update DB 2025-09-30 07:34:06 +09:00
Dr.Lt.Data 7c3cbff425 update DB 2025-09-29 12:37:31 +09:00
Dr.Lt.Data 1ff0afc633 update DB 2025-09-29 08:12:12 +09:00
Dr.Lt.Data bfe7ee8fba update DB 2025-09-29 07:48:55 +09:00
Dr.Lt.Data 49c73ed10e update DB 2025-09-29 07:18:46 +09:00
Light-x02andGitHub f571baacf9 Add ComfyUI-Lightx02-Nodes and remove duplicates (#2185)
* Update custom-node-list.json

* Update custom-node-list.json
2025-09-29 07:18:20 +09:00
6f02e1114c add VNCCS node to node list (#2184)
* add VNCCS node to node list

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-09-29 07:16:56 +09:00
Dr.Lt.Data e230f43565 update DB 2025-09-27 08:11:37 +09:00
Dr.Lt.Data 0d9593e71b update DB 2025-09-27 06:26:28 +09:00
VéroleandGitHub 20778ecfb0 Add custom node: ComfyUI-VideoCompressor (#2182)
Adds a new unified video compression node. It handles image/video inputs and features 2-pass, CRF, and GPU encoding modes
2025-09-27 05:37:58 +09:00
kj863257rcandGitHub 2ea991d960 Update custom-node-list.json : add ComfyUI_RC_Image_Compositor to custom-node-list.json (#2180) 2025-09-27 05:37:05 +09:00
Dr.Lt.Data 119c107834 update DB 2025-09-26 12:41:49 +09:00
Dr.Lt.Data 800a0d0449 update DB 2025-09-26 07:20:06 +09:00
Dr.Lt.Data 95c43f0189 update DB 2025-09-26 07:00:07 +09:00
9c77176c7f updated nodes (#2179)
* 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-09-26 06:59:16 +09:00
Dr.Lt.Data ddb6a55cd6 update DB 2025-09-25 18:12:48 +09:00
Dr.Lt.Data 56a4f6fdd7 update DB 2025-09-25 07:46:13 +09:00
Dr.Lt.Data 8a30f788b5 update DB 2025-09-25 07:21:37 +09:00
ae(seth)ticsandGitHub 380a1c2c8c Update custom-node-list.json (#2177)
Add ComfyUI 3D Model Viewer custom node
2025-09-25 07:18:50 +09:00
Dr.Lt.Data cd8e8335cf update DB 2025-09-25 07:18:33 +09:00
sumitchatterjee13andGitHub 6e1beb54a4 Add vae-decode-hdr node with HDR support (#2174)
Added a new custom The vae-decode-hdr repository is for a custom ComfyUI node designed to preserve High Dynamic Range (HDR) data during VAE decoding. The developer, Sumit Chatterjee, created this node to address the limitation of ComfyUI's default VAE Decode node, which compresses outputs to a 0-1 pixel range, resulting in a loss of dynamic range.

The node uses a "scientific approach" to analyze the VAE's conv_out layer and intelligently expand highlight regions, bypassing the clipping that typically occurs. The repository also includes a companion tool called the "Linear EXR Export" node, which is essential for creating professional HDR output files that are ready for use in compositing software. The project is licensed under the MIT License and is open to contributions.ComfyUI node for HDR VAE decoding.
2025-09-25 07:17:28 +09:00
Dr.Lt.Data 9217c965dd update DB 2025-09-25 07:17:12 +09:00
DaLongZZiandGitHub a4d71ef487 Add Gemini Prompt Studio node (#2175) 2025-09-25 07:16:18 +09:00
Dr.Lt.Data 518f332047 update DB 2025-09-25 07:15:19 +09:00
PeterandGitHub 9257d497b8 Add pg nodesAdd "PG Nodes" to custom-node-list.json (#2173)
* Update custom-node-list.json for PG Nodes

* Update custom-node-list.json for PG Nodes (fix)
2025-09-25 07:13:59 +09:00
Dr.Lt.Data 07cf5de4f7 update DB 2025-09-24 07:47:34 +09:00
43ad69e48d Update custom-node-list.json-Add ComfyUI-QI-QwenEditSafe (#2169)
* Update custom-node-list.json-Add ComfyUI-QI-QwenEditSafe

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-09-24 07:17:34 +09:00
Dr.Lt.Data c62e236cc6 update DB 2025-09-24 07:17:07 +09:00
fkxianzhouandGitHub 15a2fbb293 Update custom-node-list.json (#2168) 2025-09-24 07:15:32 +09:00
Dr.Lt.Data 16800c3fa0 update DB 2025-09-23 12:57:23 +09:00
Dr.Lt.Data ce09f41aa3 update DB 2025-09-23 07:54:51 +09:00
Dr.Lt.Data 47dc2f036a update DB 2025-09-23 07:21:26 +09:00
f27a154bfd Add ComfyUI-PromptsO to Custom node list (#2167)
* Add ComfyUI-S4Tool-Image to custom nodes list

Add ComfyUI-S4Tool-Image to custom nodes list

* Update custom-node-list.json

Add custom-node : ComfyUI-S4Motion

* Add ComfyUI-S4Tool-Text to custom node list

Text rendering and styling nodes for ComfyUI. This extension provides a basic text renderer, multiple font loaders, and a style node that adds stroke, shadow, gradient fill, and opacity control.

* Add ComfyUI-Prepack to custom node list

A small, practical bundle of ComfyUI nodes that streamlines common workflows.

* Update custom-node-list.json

* Add ComfyUI-PromptsO to Custom node list

A comprehensive AI API integration and prompt processing toolkit for ComfyUI, providing powerful text and image generation capabilities with advanced prompt manipulation tools.

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-09-23 07:20:33 +09:00
umyunsangandGitHub 79757366e8 docs: fix typos and phrasing in README and docs (en/ko)\n\n- README: grammar, capitalization, option name (--skip-stat-update), double-click, macOS\n- js/README: Copus platform name\n- docs/en: Colab capitalization\n- docs/ko: spacing, wording, typos (예를, 명령, show를, etc.) (#2166) 2025-09-23 07:17:41 +09:00
Dr.Lt.Data 2cd9a417d6 update DB 2025-09-22 12:32:22 +09:00
Dr.Lt.Data deb05c6cc3 update DB 2025-09-22 07:30:28 +09:00
sumitchatterjee13andGitHub b6f171de51 Add Luminance Stack Processor node (#2165)
A set of nodes to stack multi exposure images to produce high dynamic range image, dedicated exr output node.
2025-09-22 07:14:53 +09:00
Dr.Lt.Data a58d5f6999 update DB 2025-09-21 12:03:39 +09:00
Dr.Lt.Data e0b3f3eb45 update DB 2025-09-20 07:50:52 +09:00
Dr.Lt.Data 4bbc8594a7 update DB 2025-09-19 18:05:57 +09:00
Dr.Lt.Data 3a377300e1 update DB 2025-09-19 07:57:39 +09:00
33a07e3a86 add ComfyUI Virtual Webcam to custom-node-list.json (#2161)
* Added ComfyUI Virtual Webcam plugin to custom-node-list.json

The ComfyUI Virtual Webcam allows you to stream your ksampler output images to a webcam driver

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-09-19 07:27:21 +09:00
Dr.Lt.Data 212cafc1d7 update DB 2025-09-18 12:45:47 +09:00
LeylahandGitHub 2643b3cbcc Update author and reference for ComfyUI-Violet-Tools (#2159) 2025-09-18 07:38:32 +09:00
Dr.Lt.Data d445229b6d update DB 2025-09-17 12:54:06 +09:00
Dr.Lt.Data dab5c451b0 update DB 2025-09-17 07:37:39 +09:00
Dr.Lt.Data 7bdf06131a update DB 2025-09-17 06:44:45 +09:00
LeechKingandGitHub 854648d5af Add Danbooru FAISS Search Node to custom-node-list (#2157) 2025-09-17 06:43:55 +09:00
Dr.Lt.Data c5f7b97359 update DB 2025-09-17 06:43:39 +09:00
AaaliceandGitHub dd8a727ad6 Update custom-node-list.json (#2154) 2025-09-17 06:42:10 +09:00
Dr.Lt.Data 6c627fe422 update DB 2025-09-17 06:41:49 +09:00
dehypnoticandGitHub ee980e1caf Update custom-node-list.json (#2153) 2025-09-17 06:39:29 +09:00
SemonxueandGitHub 22bfaf6527 Add ComfyUI FlexAI Nodes to custom-node-list (#2149)
Added a new node for ComfyUI FlexAI with detailed description.
2025-09-17 06:35:58 +09:00
Dr.Lt.Data 48ab48cc30 fixed: more complete uv support
* Previously, only `uv` installed inside a venv was properly handled. Now `uv` installed outside the venv is also supported.
* Even if `use_uv=False`, `uv` is used as a fallback when `pip` is unavailable.
* Even if `use_uv=True`, `pip` is used as a fallback when `uv` is unavailable.

https://github.com/Comfy-Org/ComfyUI-Manager/issues/2125
2025-09-17 06:28:06 +09:00
Dr.Lt.Data a0b14d4127 update DB 2025-09-16 12:39:14 +09:00
Dr.Lt.Data 03f9fe1a70 update DB 2025-09-16 07:44:02 +09:00
Jonnathan NakagawaandGitHub 8915b8d796 Add custom node: comfyui_nakagawa for websocket video data handling (#2151) 2025-09-16 06:36:09 +09:00
John Cantu e04ed0eda7 update README.md for node usage analyzeer 2025-09-14 23:10:10 -07:00
John Cantu 02aa67b541 Merge branch 'main' of https://github.com/jfcantu/ComfyUI-Manager 2025-09-14 22:52:41 -07:00
John Cantu 67d03530a3 Changes and new code for Node Usage Analyzer 2025-09-14 22:49:54 -07:00
Dr.Lt.Data c77ffeeec0 update DB 2025-09-15 12:52:43 +09:00
Dr.Lt.Data 4acf5660b2 fixed: broken db 2025-09-15 08:11:56 +09:00
Dr.Lt.Data 2d9f0a668c update DB 2025-09-15 07:41:30 +09:00
9e6cb246cc Update ComfyUI-S4Tool-Image to custom_nodes list (#2150)
* Add ComfyUI-S4Tool-Image to custom nodes list

Add ComfyUI-S4Tool-Image to custom nodes list

* Update custom-node-list.json

Add custom-node : ComfyUI-S4Motion

* Add ComfyUI-S4Tool-Text to custom node list

Text rendering and styling nodes for ComfyUI. This extension provides a basic text renderer, multiple font loaders, and a style node that adds stroke, shadow, gradient fill, and opacity control.

* Add ComfyUI-Prepack to custom node list

A small, practical bundle of ComfyUI nodes that streamlines common workflows.

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-09-15 07:01:27 +09:00
Dr.Lt.Data 14544ca63d update DB 2025-09-14 08:21:21 +09:00
fr0nky0ngandGitHub 26b347c04c Add custom node: ComfyUI-Face-Comparator (#2147) 2025-09-14 08:20:23 +09:00
Dr.Lt.Data 36f75d1811 update DB 2025-09-13 15:55:25 +09:00
Dr.Lt.Data 27fc787294 update DB 2025-09-13 08:06:27 +09:00
snicolastandGitHub d23286d390 IndexTTS2 custom node (custom-node-list.json) (#2146) 2025-09-13 07:36:18 +09:00
Dr.Lt.Data 7c3ccc76c3 update DB 2025-09-12 12:48:20 +09:00
Dr.Lt.Data 892dc5d4f3 update DB 2025-09-12 07:53:17 +09:00
Dr.Lt.Data e278692749 update DB 2025-09-11 12:36:38 +09:00
Dr.Lt.Data 8d77dd2246 update DB 2025-09-11 07:23:42 +09:00
Dr.Lt.Data 14ede2a585 update DB 2025-09-10 11:58:27 +09:00
Dr.Lt.Data 5b525622f1 update DB 2025-09-10 07:52:05 +09:00
Dr.Lt.Data a24b11905c update DB 2025-09-09 12:19:49 +09:00
darkamenosaandGitHub 5d70858341 Add Comfy Nano Banana - Interact directly with Gemini API using your own API key, also add custom batch images node to avoid chaining a lot of nodes (#2141) 2025-09-09 07:39:31 +09:00
dehypnoticandGitHub 3daa006741 Update custom-node-list.json (#2140) 2025-09-09 07:39:18 +09:00
Dr.Lt.Data 0bcc0c2101 update DB 2025-09-08 12:31:06 +09:00
Dr.Lt.Data b8850c808c update DB 2025-09-08 07:47:24 +09:00
Dr.Lt.Data f4f2c01ac1 update DB 2025-09-08 06:40:55 +09:00
Dr.Lt.Data 7072e82dff update DB 2025-09-08 06:38:52 +09:00
Leylah KrellandGitHub 53dc36c4cf Add ComfyUI Violet Tools to custom node list (#2136)
Added aesthetic-focused custom nodes package with 7 specialized nodes:
- Aesthetic Alchemist (style blending with 20+ curated aesthetics)
- Quality Queen (quality prompts)
- Glamour Goddess (hair/makeup)
- Body Bard (body features)
- Pose Priestess (positioning)
- Encoding Enchantress (text processing)
- Negativity Nullifier (negative prompts)

Features weighted blending, randomization, and modular YAML-based configuration.
2025-09-08 06:37:00 +09:00
Satadal DharaandGitHub 5aadc3af00 Updated Node List with My node (#2134) 2025-09-06 03:55:06 +09:00
Dr.Lt.Data 8c28a698ed update DB 2025-09-06 03:54:56 +09:00
Dr.Lt.Data 5ed6d8b202 update DB 2025-09-06 03:53:56 +09:00
Vantage with AIandGitHub b73dc7bf5e Changed name of node from ComfyUI-HunyuanFoley to Vantage-HunyuanFoley because of conflict. (#2130)
* Update custom-node-list.json

* Update custom-node-list.json
2025-09-06 03:51:08 +09:00
Dr.Lt.Data 71d0f4ab63 update DB 2025-09-05 12:56:40 +09:00
Dr.Lt.Data d479dcde81 update DB 2025-09-05 07:53:04 +09:00
Dr.Lt.Data ae536017d5 update DB 2025-09-05 07:49:12 +09:00
matthewfriedrichsandGitHub 67ddfce279 adding thought bubble custom node (#2129) 2025-09-05 07:48:06 +09:00
Vantage with AIandGitHub b1f39b34d7 Update custom-node-list.json (#2128) 2025-09-05 07:47:26 +09:00
Dr.Lt.Data 6cf958ccce udpate DB 2025-09-04 12:22:45 +09:00
Dr.Lt.Data eaed3677d3 update DB 2025-09-04 07:27:31 +09:00
sumitchatterjee13andGitHub b9c88da54d Add Nuke Nodes for ComfyUI to registry (#2123)
This PR adds nuke-nodes-comfyui to the ComfyUI Manager registry.

Features:
- Professional compositing nodes replicating Nuke functionality
- 15+ nodes including merge, grade, transform, and blur operations
- Designed for professional compositing workflows in ComfyUI
- Well-documented with installation instructions

Repository: https://github.com/sumitchatterjee13/nuke-nodes-comfyui
2025-09-04 07:23:48 +09:00
Dr.Lt.Data 104ae77f7a update DB 2025-09-03 12:12:40 +09:00
Dr.Lt.Data bfcb2ce61b update DB 2025-09-03 07:40:58 +09:00
Dr.Lt.Data 63ba5fed09 update DB 2025-09-03 01:07:30 +09:00
Dr.Lt.Data 98a8464933 update DB 2025-09-03 00:16:55 +09:00
7e3e6726e0 Add ComfyUI-Prepack to custom nodes list (#2121)
* Add ComfyUI-S4Tool-Image to custom nodes list

Add ComfyUI-S4Tool-Image to custom nodes list

* Update custom-node-list.json

Add custom-node : ComfyUI-S4Motion

* Add ComfyUI-S4Tool-Text to custom node list

Text rendering and styling nodes for ComfyUI. This extension provides a basic text renderer, multiple font loaders, and a style node that adds stroke, shadow, gradient fill, and opacity control.

* Add ComfyUI-Prepack to custom node list

A small, practical bundle of ComfyUI nodes that streamlines common workflows.

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-09-03 00:15:49 +09:00
Dr.Lt.Data 09567b2bb2 update DB 2025-09-03 00:15:34 +09:00
f3bd116184 Add ComfyUI-LoRAWeightAxisXY (#2120)
* Add ComfyUI-LoRAWeightAxisXY

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-09-03 00:12:50 +09:00
Dr.Lt.Data 7509737563 update DB 2025-09-02 12:59:44 +09:00
Dr.Lt.Data cfb815d879 update DB 2025-09-01 12:05:21 +09:00
Dr.Lt.Data 44241fb967 update DB 2025-09-01 07:31:34 +09:00
mengqinandGitHub c4b45129bd Update DB. (#2118) 2025-09-01 06:53:35 +09:00
Dr.Lt.Data 70741008ca Update DB 2025-08-31 18:11:54 +09:00
daehwaandGitHub 6c2d2cae2a Add ComfyUI-NanoBananaAPI node entry (#2115) 2025-08-31 17:22:18 +09:00
gsusggandGitHub 28f13d3311 Add ComfyUI-CozyGen custom node entry (#2113)
Added a new custom node entry for ComfyUI-CozyGen with details.
2025-08-31 17:20:43 +09:00
Dr.Lt.Data 4e31aaa8fb update DB 2025-08-30 10:47:43 +09:00
dehypnoticandGitHub ba99f0c2cc Update custom-node-list.json (#2112) 2025-08-30 10:41:28 +09:00
Dr.Lt.Data e0a96b4937 update DB 2025-08-29 13:00:32 +09:00
Dr.Lt.Data 82c055f527 update DB 2025-08-29 07:59:21 +09:00
Makki ShizuandGitHub f94008192c Update custom-node-list.json (#2110) 2025-08-29 07:47:26 +09:00
Fabio SarracinoandGitHub 3895d5279e Add VibeVoice ComfyUI node (#2109) 2025-08-29 07:45:41 +09:00
Dr.Lt.Data 3d85ecc525 update DB 2025-08-28 00:25:45 +09:00
Dr.Lt.Data 7da00796e5 update DB 2025-08-27 12:21:31 +09:00
Dr.Lt.Data 6086419cb6 update DB 2025-08-27 07:51:36 +09:00
Dr.Lt.Data 5bc1f2f2c0 update DB 2025-08-26 19:39:38 +09:00
32a83b211e Update Rodin Plugin url (#2102)
Co-authored-by: WhiteGiven <c15838568211@163.com>
2025-08-26 19:03:05 +09:00
AlexandGitHub bead7b3a7f Add Custom Node - Save Checkpoint with Metadata (#2105)
* Added entry for ComfyUI-SaveCheckpointWithMetadata

* Added entry for ComfyUI-SaveCheckpointWithMetadata in git-clone section
2025-08-26 19:01:52 +09:00
jialuw0830andGitHub 815d6d6572 Add Eigen AI FLUX API Plugin to custom node list (#2104) 2025-08-26 18:59:51 +09:00
Dr.Lt.Data 95ce812992 update DB 2025-08-25 12:59:46 +09:00
Dr.Lt.Data 9a36f4748c update DB 2025-08-25 08:06:43 +09:00
Dr.Lt.Data 50b7849a35 update DB 2025-08-25 07:27:39 +09:00
Dr.Lt.Data 6f1245b27c update DB 2025-08-25 06:30:51 +09:00
cc87ed3899 Update custom-node-list.json (#2097)
* Update custom-node-list.json

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-08-25 06:28:06 +09:00
Dr.Lt.Data 1d9037fefe update DB 2025-08-25 06:27:46 +09:00
DaxamurandGitHub 03016e2d16 Add DaxNodes to custom node list (#2100) 2025-08-25 06:26:28 +09:00
Dr.Lt.Data 3d41617f4e update DB 2025-08-23 17:54:00 +09:00
Dr.Lt.Data 35151ffdd1 update DB 2025-08-23 09:20:01 +09:00
Dr.Lt.Data 4527d41a7a update DB 2025-08-22 21:13:29 +09:00
553cba12f3 Update custom-node-list.json (#2096)
* Update custom-node-list.json

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-08-22 20:54:35 +09:00
Dr.Lt.Data 116e068ac3 update DB 2025-08-22 12:41:08 +09:00
Dr.Lt.Data 1010dd2d28 update DB 2025-08-22 07:35:26 +09:00
Dr.Lt.Data 7354242906 update workflow 2025-08-22 06:05:27 +09:00
Dr.Lt.Data e7d0b158e9 update DB 2025-08-22 05:41:35 +09:00
Dr.Lt.Data 330c4657b1 update DB 2025-08-21 12:25:20 +09:00
Dr.Lt.Data 72a109f109 update DB 2025-08-21 07:29:53 +09:00
licykandGitHub cf45c51dfb Add HDM-ext to custom-node-list (#2094) 2025-08-21 06:52:09 +09:00
Dr.Lt.Data 0b013adb34 update DB 2025-08-20 12:24:39 +09:00
Dr.Lt.Data 7457d91f64 update DB 2025-08-20 07:44:09 +09:00
Dr.Lt.Data 7fe1159426 update DB 2025-08-20 05:23:08 +09:00
c2665e3677 Update custom-node-list.json (#2091)
* Update custom-node-list.json

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-08-20 05:10:13 +09:00
Dr.Lt.Data d63de803a4 update DB 2025-08-20 04:02:02 +09:00
Dr.Lt.Data 11aca3513c update DB 2025-08-20 03:53:51 +09:00
Joel Andrés Navarro NavarroandGitHub 561c9f40e5 Update custom-node-list.json (#2089) 2025-08-20 03:49:46 +09:00
Saquib AlamandGitHub 54ed13aadf add nodes for omini-kontext framework (#2087) 2025-08-20 03:47:56 +09:00
Dr.Lt.Data 109cc21337 update DB 2025-08-19 07:48:17 +09:00
Dr.Lt.Data 7e46b30fa5 update DB 2025-08-18 12:33:30 +09:00
Dr.Lt.Data 0ba112c2c7 update DB 2025-08-18 07:47:41 +09:00
davidandGitHub fc15d94170 Update custom-node-list.json (#2086) 2025-08-18 07:38:28 +09:00
Dr.Lt.Data dcb37d9c55 update DB 2025-08-17 18:23:05 +09:00
Marco ZanellaandGitHub 755b9d6342 Add ComfyUI-BooleanExpression to custom-node-list (#2084) 2025-08-17 17:53:24 +09:00
Joel Andrés Navarro NavarroandGitHub 3d6151c94f Update custom-node-list.json (#2085) 2025-08-17 17:51:20 +09:00
jupo-aiandGitHub 590bd8c4b9 Update custom-node-list.json (#2083) 2025-08-17 07:05:03 +09:00
Dr.Lt.Data e99aafd876 update DB 2025-08-16 10:26:33 +09:00
Dr.Lt.Data 1f0adf8bcf update DB 2025-08-16 09:53:13 +09:00
jupo-aiandGitHub dbd5d5fb43 Update custom-node-list.json (#2082)
* Update custom-node-list.json

* Update custom-node-list.json
2025-08-16 09:36:35 +09:00
Dr.Lt.Data a8b0e3641b update DB 2025-08-15 10:13:33 +09:00
AfterGlow.SYXandGitHub 9efb350be9 Update custom-node-list.json (#2081) 2025-08-15 10:08:10 +09:00
Dr.Lt.Data 8d9820b3fb update DB 2025-08-14 23:24:08 +09:00
Dr.Lt.Data 103f89551a update DB 2025-08-14 22:00:23 +09:00
Dr.Lt.Data 6030d961ad update DB 2025-08-14 12:01:24 +09:00
Dr.Lt.Data ee08c9e17f update DB 2025-08-14 07:42:41 +09:00
Dr.Lt.Data 48dd9a3240 update DB 2025-08-14 02:35:34 +09:00
BaverneandGitHub e122e206a6 Add TiledWan (#2078)
* Add TiledWan

* Add TiledWan

* Add TiledWan
2025-08-14 02:21:37 +09:00
Dr.Lt.Data 398b905758 update DB 2025-08-13 12:12:36 +09:00
Dr.Lt.Data dc2ec08fe3 update DB 2025-08-13 07:44:54 +09:00
Dr.Lt.Data 3bf5edf5c9 update DB 2025-08-12 10:34:55 +09:00
Dr.Lt.Data 134bca526c update DB 2025-08-12 09:52:15 +09:00
millerlightandGitHub a471263602 Merge branch 'Comfy-Org:main' into millerlight-patch-1 2025-08-11 16:24:36 +02:00
Dr.Lt.Data 3393e58b06 update DB 2025-08-11 22:52:13 +09:00
Dr.Lt.Data eab6cdeee4 bump version 2025-08-11 12:48:38 +09:00
Christian ByrneandGitHub e8ec1ce8e3 recurse when finding nodes in workflow (#2070) 2025-08-11 12:47:20 +09:00
Dr.Lt.Data b3581564ed update DB 2025-08-11 12:28:12 +09:00
S4MUELandGitHub 29e1bd95fd Add ComfyUI S4Motion to custom-node-list.json (#2072)
* Add ComfyUI-S4Tool-Image to custom nodes list

Add ComfyUI-S4Tool-Image to custom nodes list

* Update custom-node-list.json

Add custom-node : ComfyUI-S4Motion
2025-08-11 12:23:16 +09: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
Dr.Lt.Data 8bff401c14 update DB 2025-08-11 08:47:56 +09:00
Dr.Lt.Data 41798e9255 update DB 2025-08-11 07:44:25 +09:00
Dr.Lt.Data 9e4f0228d1 update DB 2025-08-10 20:54:49 +09:00
Dr.Lt.Data 76ee93c98c update DB 2025-08-10 11:25:27 +09:00
ericKuangandGitHub fb1a89efb7 Update custom-node-list.json (#2068)
Add ComfyUI-Only node:
Pain Point Solved: Eliminates the need to manually move .latent files into the ComfyUI input directory.
2025-08-10 11:16:32 +09:00
Dr.Lt.Data aface43554 update DB 2025-08-10 11:02:38 +09:00
Dr.Lt.Data a35f0157b2 update DB 2025-08-10 10:20:57 +09:00
Dr.Lt.Data 9b32162906 update DB 2025-08-09 15:13:30 +09:00
Dr.Lt.Data 21bba62572 update DB 2025-08-09 12:35:05 +09:00
Dr.Lt.Data 302327d6b3 update DB 2025-08-09 07:54:04 +09:00
Dr.Lt.Data 5667e8bcbb update DB 2025-08-08 23:13:50 +09:00
Dr.Lt.Data ae66bd0e31 update DB 2025-08-08 12:15:46 +09:00
Dr.Lt.Data 48dfadc02d update DB 2025-08-08 07:54:54 +09:00
Dr.Lt.Data 3df6272bb6 update DB 2025-08-08 07:37:49 +09:00
CY-CHENYUEandGitHub e7f9bcda01 Update custom-node-list.json (#2064) 2025-08-08 07:35:24 +09:00
Dr.Lt.Data 205044ca66 update DB 2025-08-07 12:19:21 +09:00
Dr.Lt.Data d497eb1f00 update DB 2025-08-07 08:42:22 +09:00
Dr.Lt.Data 4e6f970ee9 update DB 2025-08-06 12:14:25 +09:00
Dr.Lt.Data 0b6cdda6f5 update DB 2025-08-06 08:59:45 +09:00
Dr.Lt.Data a896ded763 update DB 2025-08-06 07:26:55 +09:00
Dr.Lt.Data fb5dd9ebc2 update DB 2025-08-05 12:24:03 +09:00
Dr.Lt.Data c8b7db6c38 update DB 2025-08-05 08:57:36 +09:00
Dr.Lt.Data 44a3191be3 update DB 2025-08-05 07:16:04 +09:00
Dr.Lt.Data b4f7cdc9e7 update DB 2025-08-05 06:20:52 +09:00
8da07018d5 Update custom-node-list.json (#2058)
* Update custom-node-list.json

Added my custom node "AF-EditGeneratedPrompt", which let's one pipe a generated prompt, edit it, or use the node as a regular prompting node. Thank you for your efforts!

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-08-05 06:19:40 +09:00
Dr.Lt.Data 0c19a27065 update DB 2025-08-04 20:13:27 +09:00
3296b0ecdf Add ComfyUI Gemini Nodes by jqy-yo (#2057)
Add entry for comfyui-gemini-nodes - a collection of custom nodes for integrating Google Gemini API with ComfyUI, providing AI capabilities for text generation, image generation, and video analysis.

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

Co-authored-by: jqy-yo <jqy-yo@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-08-04 19:58:59 +09:00
UygarandGitHub 0a07261124 Update custom-node-list.json (#2055) 2025-08-04 12:12:13 +09:00
Dr.Lt.Data 33106d0ecf update DB 2025-08-04 12:10:52 +09:00
Novice_ChenandGitHub 5bb887206a add new node:ComfyUI-XingLiu (#2040) 2025-08-04 12:09:22 +09:00
Dr.Lt.Data b30b0e27cb update DB 2025-08-04 08:59:56 +09:00
Dr.Lt.Data 363736489c update DB 2025-08-04 08:59:40 +09:00
Dr.Lt.Data 8dbf5e87a0 update DB 2025-08-04 07:39:25 +09:00
Dr.Lt.Data 0b30f2cb50 update DB 2025-08-04 07:02:06 +09:00
BrekelandGitHub ba5265dac4 Update custom-node-list.json (#2054)
Add ComfyUI-Brekel
2025-08-04 06:16:37 +09:00
Dr.Lt.Data ecb9c65917 update DB 2025-08-04 06:16:24 +09:00
jupo-aiandGitHub 8a98474600 Update custom-node-list.json (#2051) 2025-08-04 06:09:28 +09:00
Radiating ReverberationsandGitHub b072216e67 Add Wan2.2 models from Comfy-Org (#2050) 2025-08-04 06:08:44 +09:00
Dr.Lt.Data cfb3181716 update DB 2025-08-02 08:03:23 +09:00
Dr.Lt.Data ab684cdc99 update DB 2025-08-01 12:22:27 +09:00
Dr.Lt.Data facadc3a44 update DB 2025-08-01 07:29:09 +09:00
Dr.Lt.Data 281319d2da update DB 2025-08-01 00:08:52 +09:00
SimlymandGitHub 5cb203685c Update custom-node-list.json (#2045) 2025-07-31 23:44:48 +09:00
Dr.Lt.Data 01fa37900b update DB 2025-07-31 12:32:47 +09:00
Dr.Lt.Data edbe744e17 update DB 2025-07-31 07:57:27 +09:00
Jin YiandGitHub 2a32a1a4a8 Add bulk API endpoint for import fail info (#2039)
* feat(api): Implement endpoint for bulk import failure info

Adds the `/v2/customnode/import_fail_info_bulk` endpoint to allow
fetching multiple import error statuses in a single request.

* chore(api): Update OpenAPI spec for new bulk endpoint

Adds the `import_fail_info_bulk` route and its corresponding
request/response schemas to `openapi.yaml`.
2025-07-31 07:43:49 +09:00
Dr.Lt.Data 404bdb21e6 update DB 2025-07-30 18:39:08 +09:00
PD19 AnimeandGitHub b260c9a512 Update custom-node-list.json (#2044) 2025-07-30 18:33:29 +09:00
Yuan-ManandGitHub 4b941adb6a Add ComfyUI-SkyworkUniPic (#2043) 2025-07-30 18:32:15 +09:00
bd752550a8 feat: change web icon (#2042)
Co-authored-by: john <john@server31.io>
2025-07-30 18:31:56 +09:00
Dr.Lt.Data b8b71bb961 update DB 2025-07-30 12:16:25 +09:00
Kevin LinandGitHub 5aaf7a4092 Update custom node listing (#2041) 2025-07-30 12:03:28 +09:00
Dr.Lt.Data 030e02ffb8 update DB 2025-07-30 08:57:38 +09:00
Dr.Lt.Data d962aa03f4 update DB 2025-07-30 07:37:26 +09:00
Dr.Lt.Data 9e4a2aae43 update DB 2025-07-30 00:02:30 +09:00
ee6eb685e7 Add Whirlpool Upscaler (#2037)
* Added Whirlpool Upscaler

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-07-29 23:52:57 +09:00
Dr.Lt.Data 09a38a32ce update DB 2025-07-29 21:30:45 +09:00
Android zhangandGitHub d13b19d43d Update custom-node-list.json (#2036)
Add ComfyUI-MoGe2
2025-07-29 21:02:18 +09:00
Dr.Lt.Data e730dca1ad update DB 2025-07-29 12:13:35 +09:00
Dr.Lt.Data 8da30640bb update DB
fixed: scanner.py
2025-07-29 07:45:05 +09:00
Dr.Lt.Data 6f4eb88e07 update DB 2025-07-28 12:15:58 +09:00
Dr.Lt.Data d9592b9dab update DB 2025-07-28 08:57:58 +09:00
Dr.Lt.Data b87ada72aa update DB 2025-07-28 07:04:57 +09:00
Dr.Lt.Data 83363ba1f0 update DB 2025-07-27 21:36:48 +09:00
Dr.Lt.Data 23ebe7f718 update DB 2025-07-27 15:04:41 +09:00
Dr.Lt.Data e04264cfa3 update DB 2025-07-27 10:45:00 +09:00
Shmuel RonenandGitHub 8d29e5037f Add ComfyUI-HiggsAudio_Wrapper to custom node list (#2034) 2025-07-27 10:28:27 +09:00
Dr.Lt.Data 6926ed45b0 update DB 2025-07-26 21:05:02 +09:00
Dr.Lt.Data 736b85b8bb update DB 2025-07-26 20:51:43 +09:00
NanthakumarandGitHub 9e3361bc31 Update custom-node-list.json (#2031) 2025-07-26 20:37:40 +09:00
Dr.Lt.Data 6e10381020 update DB 2025-07-26 11:13:08 +09:00
Dr.Lt.Data a1d37d379c update DB 2025-07-26 09:34:57 +09:00
07d87db7a2 Update custom-node-listAdd ComfyUI-Studio-nodes to custom_nodes registry.json (#2029)
* 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-07-26 09:29:31 +09:00
Dr.Lt.Data 4e556673d2 update DB 2025-07-26 09:27:00 +09:00
AIWarperandGitHub f421304fc1 Update custom-node-list.json (#2028) 2025-07-26 09:25:34 +09:00
Dr.Lt.Data c9271b1686 update DB 2025-07-25 12:19:45 +09:00
Dr.Lt.Data 12eb6863da update DB 2025-07-25 08:58:56 +09:00
Dr.Lt.Data 4834874091 fixed: ruff check 2025-07-25 07:26:48 +09:00
Dr.Lt.Data 8759ebf200 bump version 2025-07-25 07:03:14 +09:00
YAN WenkunandGitHub d4715aebef Migrate matrix-client to matrix-nio (#2025) 2025-07-25 06:59:46 +09:00
Dr.Lt.Data 0fe2ade7bb update DB 2025-07-25 06:59:32 +09:00
Dr.Lt.Data 0c71565535 update DB 2025-07-24 21:28:41 +09:00
Dr.Lt.Data 6a637091a2 update DB 2025-07-24 12:10:49 +09:00
Dr.Lt.Data 31eba60012 update DB 2025-07-24 09:00:09 +09:00
Dr.Lt.Data 51e58e9078 update DB 2025-07-24 07:07:58 +09:00
Dr.Lt.Data 4a1e76730a fixed: security_check - robust checking
https://github.com/Comfy-Org/ComfyUI-Manager/issues/2002
2025-07-24 02:44:43 +09:00
Dr.Lt.Data 5599bb028b fixed: security_check - robust checking
https://github.com/Comfy-Org/ComfyUI-Manager/issues/2002
2025-07-24 02:38:53 +09:00
Dr.Lt.Data 552c6da0cc modified: download_url - provide more informative error messages
https://github.com/Comfy-Org/ComfyUI-Manager/issues/2016
2025-07-24 02:30:07 +09:00
Dr.Lt.Data cc6817a891 fixed: cnr_utils – fixed improper behavior of bypass_ssl
https://github.com/Comfy-Org/ComfyUI-Manager/issues/2017
2025-07-24 02:15:31 +09:00
Dr.Lt.Data fb48d1b485 update DB 2025-07-24 02:06:14 +09:00
UygarandGitHub 1c336dad6b ComfyUI-Artha-Gemini custom node (#2024)
* Add files via upload

* Update custom-node-list.json
2025-07-24 02:01:31 +09:00
Dr.Lt.Data a4940d46cd update DB 2025-07-24 02:01:16 +09:00
499b2f44c1 Add builmenlabo custom node entry (#2020)
* Add files via upload

* Add files via upload

* Delete manager_registration.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-07-24 01:59:13 +09:00
Yuan-ManandGitHub 2b200c9281 Add ComfyUI-HiggsAudio (#2023) 2025-07-24 01:58:09 +09:00
Dr.Lt.Data 36a900c98f update DB 2025-07-23 12:50:44 +09:00
Dr.Lt.Data 5236b03f66 update DB 2025-07-23 07:32:34 +09:00
kpsss34andGitHub 8be35e3621 Update custom-node-list.json (#2021)
Rename: ComfyUI-kpsss34-Sana to ComfyUI-kpsss34
2025-07-23 07:31:26 +09:00
Dariusz LandGitHub 509f00fe89 Add Comfyui-LayerForge (#2022)
Add the "Comfyui-LayerForge" node to the community list.
2025-07-23 07:30:43 +09:00
Dr.Lt.Data a98b87f148 update DB 2025-07-22 12:17:42 +09:00
Dr.Lt.Data ae9b2b3b72 update DB 2025-07-22 08:59:51 +09:00
Dr.Lt.Data 02e1ec0ae3 update DB 2025-07-22 07:32:38 +09:00
Vaishnav V NairandGitHub daefb0f120 Update custom-node-list.json (#2018)
first custom node
2025-07-22 07:22:18 +09:00
Dr.Lt.Data ff0604e3b6 update DB 2025-07-21 12:14:49 +09:00
Dr.Lt.Data 20e41e22fa update DB 2025-07-21 08:59:07 +09:00
Dr.Lt.Data a0e3bdd594 update DB 2025-07-20 19:15:45 +09:00
brucew4yn3rpandGitHub 6580aaf3ad Added Save Image (Selective Metadata) node (#2012) 2025-07-20 18:57:27 +09:00
Dr.Lt.Data 0b46701b60 update DB 2025-07-20 18:57:10 +09:00
Edoardo CarmignaniandGitHub 0bb4effede Add ComfyUI-ExtraLinks (#2009)
A one-click collection of alternate connection styles for ComfyUI.
2025-07-20 18:21:25 +09:00
Dr.Lt.Data b07082a52d update DB 2025-07-19 18:16:26 +09:00
04f267f5a7 Add StrawberryFist VRAM Optimizer node to custom-node-list.json (#2007)
* Add StrawberryFist VRAM Optimizer node to custom-node-list.json

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-07-19 18:15:22 +09:00
Dr.Lt.Data 03ccce2804 fixed: cm-cli - provides pip dependency restoration using the options --pip-non-url, --pip-non-local-url, and --pip-local-url.
https://github.com/Comfy-Org/ComfyUI-Manager/issues/2008
2025-07-19 06:51:07 +09:00
Dr.Lt.Data e894bd9f24 update DB 2025-07-18 07:50:14 +09:00
Dr.Lt.Data 10e6988273 update DB 2025-07-18 07:26:51 +09:00
ErehrandGitHub 905b61e5d8 Publish ComfyUI-Eagle-Autosend (#2006) 2025-07-18 07:25:55 +09:00
Dr.Lt.Data ee69d393ae update DB
update scanner script
2025-07-17 12:22:13 +09:00
Dr.Lt.Data cab39973ae update DB 2025-07-17 12:10:40 +09:00
Dr.Lt.Data d93f5d07bb update DB 2025-07-17 08:57:16 +09:00
Dr.Lt.Data ba00ffe1ae update DB 2025-07-17 07:39:11 +09:00
6afaf5eaf5 Add LTX-Video 0.9.8 distilled models (#2005)
- Add LTX-Video 2B Distilled v0.9.8 (6.34GB)
- Add LTX-Video 2B Distilled FP8 v0.9.8 (4.46GB)
- Add LTX-Video 13B Distilled v0.9.8 (28.6GB)
- Add LTX-Video 13B Distilled FP8 v0.9.8 (15.7GB)

These v0.9.8 models feature improved prompt understanding and detail generation.
Both 2B and 13B variants available in standard and FP8 quantized versions.

Co-authored-by: gschreiber <gschreiber@infra-image-generator.c.ltx-research-vms.internal>
2025-07-17 07:38:53 +09:00
Dr.Lt.Data d30459cc34 update DB 2025-07-16 12:31:58 +09:00
Dr.Lt.Data e92fbb7b1b update DB 2025-07-16 12:24:26 +09:00
42d464b532 Update custom-node-list.json (#2004)
* Update custom-node-list.json

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-07-16 12:22:34 +09:00
Dr.Lt.Data c2e9e5c63a update DB 2025-07-16 07:28:23 +09:00
CreepybitsandGitHub bc36726925 Update custom-node-list.json (#2001)
Add Save To OneDrive node for ComfyUI
2025-07-16 07:08:59 +09:00
Dr.Lt.Data 7abbff8c31 update DB 2025-07-15 12:14:23 +09:00
Android zhangandGitHub 6236f4bcf4 Add ComfyUI nodes to use Distill-Any-Depth prediction (#1999) 2025-07-15 06:27:32 +09:00
Jukka SeppänenandGitHub 3c3e80f77f Add WanVideoWrapper (#1998)
* Add IC-Light nodes and models

* Add Florence2 and LuminaWrapper -nodes

https://github.com/kijai/ComfyUI-Florence2
https://github.com/kijai/ComfyUI-LuminaWrapper

* Update custom-node-list.json

* Update custom-node-list.json

* Update custom-node-list.json

* Add segment-anything-2

* Update custom-node-list.json

* Add T5 encoder models

* Update custom-node-list.json

* Add PyramidFlowWrapper

* Add HunyuanVideoWrapper

* Add ComfyUI-WanVideoWrapper
2025-07-15 06:25:56 +09:00
Dr.Lt.Data 4aae2fb289 update DB 2025-07-14 20:29:22 +09:00
Dr.Lt.Data 66ff07752f update DB 2025-07-14 19:04:10 +09:00
5cf92f2742 Add ComfyUI-WBLESS (#1990)
* Update custom-node-list.json

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-07-14 19:03:33 +09:00
Dr.Lt.Data 6d3fddc474 update DB 2025-07-14 19:02:41 +09:00
Dr.Lt.Data 66d4ad6174 update DB 2025-07-14 18:58:05 +09:00
ChenNingandGitHub 2a366a1607 Add ComfyUI_Image_Pin (#1992) 2025-07-14 18:56:22 +09:00
Dr.Lt.Data d87a0995b4 update DB 2025-07-14 18:55:31 +09:00
Dr.Lt.Data 9a73a41e04 update DB 2025-07-14 18:55:11 +09:00
company8andGitHub ba041b36bc Update custom-node-list.json (#1993) 2025-07-14 18:54:18 +09:00
f5f9de69b4 Add EsesImageCompare node to node list (#1994)
Co-authored-by: eses <13034046+quasiblob@users.noreply.github.com>
2025-07-14 18:53:29 +09:00
Yuan-ManandGitHub 71e56c62e8 Add ComfyUI-ThinkSound (#1989) 2025-07-14 18:52:27 +09:00
Dr.Lt.Data 0f496619fd update DB 2025-07-12 11:07:46 +09:00
Dr.Lt.Data 5fdd6a441a update DB 2025-07-12 09:07:33 +09:00
Dr.Lt.Data 00f287bb63 fixed: ruff check 2025-07-12 06:15:09 +09:00
Dr.Lt.Data 785268efa6 modified: By default, do not forcefully downgrade numpy to below version 2. I believe enough of a grace period has now been given.
https://github.com/Comfy-Org/ComfyUI-Manager/issues/1981#issuecomment-3058772842
2025-07-12 06:07:10 +09:00
Dr.Lt.Data 2c976d9394 update DB 2025-07-12 05:54:51 +09:00
Dr.Lt.Data 1e32582642 fixed: broken db 2025-07-12 05:29:32 +09:00
IsItDanOrAiandGitHub 6f8f6d07f5 Update custom-node-list.json (#1980) 2025-07-12 05:28:36 +09:00
3958111e76 Add LTX-Video ICLoRA models for depth, pose, and canny control (#1988)
- Add LTX-Video ICLoRA Depth 13B v0.9.7 (81.9MB)
- Add LTX-Video ICLoRA Pose 13B v0.9.7 (151MB)
- Add LTX-Video ICLoRA Canny 13B v0.9.7 (81.9MB)

These In-Context LoRA models enable precise control for video-to-video generation
with depth, pose, and canny edge conditioning respectively.

Co-authored-by: gschreiber <gschreiber@infra-image-generator.c.ltx-research-vms.internal>
2025-07-12 05:20:21 +09:00
Dr.Lt.Data 86fcc4af74 update DB 2025-07-10 12:33:19 +09:00
Dr.Lt.Data 2fd26756df update DB 2025-07-10 07:41:25 +09:00
478f4b74d8 add ComfyUI-EsesImageTransform node (#1987)
Co-authored-by: eses <13034046+quasiblob@users.noreply.github.com>
2025-07-10 07:36:40 +09:00
Dr.Lt.Data 73d0d2a1bb update DB 2025-07-09 22:59:44 +09:00
Dr.Lt.Data 546db08ec4 update DB 2025-07-09 08:56:44 +09:00
Dr.Lt.Data 0dd41a8670 update DB 2025-07-09 07:19:11 +09:00
82c0c89f46 Add ComfyUI-PD19Anime-Nodes to custom node list (#1975)
* Update custom-node-list.json

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-07-09 06:38:43 +09:00
Dr.Lt.Data c3798bf4c2 update DB 2025-07-08 12:12:31 +09:00
Dr.Lt.Data ff80b6ccb0 update DB 2025-07-08 08:58:03 +09:00
e729217116 add ComfyUI-EsesImageEffectCurves node (#1976)
Co-authored-by: eses <13034046+quasiblob@users.noreply.github.com>
2025-07-08 08:56:58 +09:00
Dr.Lt.Data 94c695daca update DB 2025-07-08 08:56:11 +09:00
FortunaCournotandGitHub 9f189f0420 Stereoscopic Nodes added (#1978) 2025-07-08 08:55:24 +09:00
Bas NijholtandGitHub ad09e53f60 Remove file argument from logging.error in manager_server.py (#1977)
Otherwise this results in:
```python
TypeError: Logger._log() got an unexpected keyword argument 'file' 
```
2025-07-08 08:48:16 +09:00
Dr.Lt.Data 092a7a5f3f update DB 2025-07-07 23:38:10 +09:00
Dr.Lt.Data f45649bd25 update DB 2025-07-07 12:59:28 +09:00
Dr.Lt.Data 2595cc5ed7 bump version 2025-07-07 01:05:25 +09:00
Dr.Lt.Data 2f62190c6f update DB 2025-07-07 01:00:58 +09:00
Alexander PiskunandGitHub 577314984c fix(Windows, numpy): fix for cm-cli usage (#1972) 2025-07-06 22:36:49 +09:00
Dr.Lt.Data f0346b955b update DB 2025-07-06 16:57:36 +09:00
Dr.Lt.Data 70139ded4a bump version 2025-07-06 13:40:50 +09:00
Dr.Lt.Data bf379900e1 update DB 2025-07-06 13:40:17 +09:00
Dr.Lt.Data 9bafc90f5e update DB 2025-07-06 08:31:22 +09:00
Alexander PiskunandGitHub fce0d9e88e fix(Windows, numpy): do not use 'uv' by default (#1971) 2025-07-06 08:23:31 +09:00
namtb96andGitHub 2b3b154989 Add OmniGen2 Simple Node (#1970)
* add OmniGen2 custom node

* Change extenions name
2025-07-06 08:22:02 +09:00
Dr.Lt.Data 948d2440a1 update DB 2025-07-05 09:40:28 +09:00
Dr.Lt.Data 5adbe1ce7a update DB 2025-07-05 06:42:32 +09:00
8157d34ffa Add VRGameDevGirl’s Video Enhancement Nodes (#1966)
* 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-07-05 06:26:15 +09:00
Dr.Lt.Data 3ec8cb2204 update DB 2025-07-05 06:06:16 +09:00
Dr.Lt.Data 0daa826543 fixed: invalid default config.ini
https://github.com/Comfy-Org/ComfyUI-Manager/issues/1967
2025-07-04 17:54:26 +09:00
Dr.Lt.Data a66028da58 update DB 2025-07-04 08:53:35 +09:00
Dr.Lt.Data 807c9e6872 update DB 2025-07-04 07:02:41 +09:00
Dr.Lt.Data e71f3774ba modified: If uv is available, set use_uv to True by default. 2025-07-03 12:32:50 +09:00
Dr.Lt.Data dd7314bf10 update DB 2025-07-03 12:22:59 +09:00
Dr.Lt.Data f33bc127dc update DB 2025-07-03 07:31:25 +09:00
db92b87782 Update custom-node-list.json (#1965)
* Update custom-node-list.json

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-07-03 07:08:40 +09:00
Dr.Lt.Data eba41c8693 update DB 2025-07-02 21:38:06 +09:00
c855308162 Update DB (#1963)
* Update custom-node-list.json

update

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-07-02 21:32:13 +09:00
Dr.Lt.Data 73d971bed8 bump version 2025-07-02 12:33:16 +09:00
bcfe0c2874 feat: copus content add rating (#1962)
Co-authored-by: john <john@server31.io>
2025-07-02 12:32:17 +09:00
Dr.Lt.Data 931ff666ae update DB 2025-07-02 12:02:20 +09:00
Dr.Lt.Data 18b6d86cc4 update DB 2025-07-02 08:57:41 +09:00
Dr.Lt.Data 3c5efa0662 update DB 2025-07-01 12:18:14 +09:00
Dr.Lt.Data 9b739bcbbf update DB 2025-07-01 08:57:40 +09:00
Dr.Lt.Data db89076e48 update DB 2025-07-01 07:30:59 +09:00
Dr.Lt.Data 19b341ef18 update DB 2025-07-01 01:04:40 +09:00
Dr.Lt.Data be3713b1a3 update DB 2025-07-01 00:21:53 +09:00
Dr.Lt.Data 99c4415cfb update DB 2025-06-30 21:29:41 +09:00
方长君andGitHub 7b311f2ccf Add MultiSaveImage custom node (#1956) 2025-06-30 21:13:20 +09:00
Dr.Lt.Data 4aeabfe0a7 update DB 2025-06-30 07:34:20 +09:00
Dr.Lt.Data 431ed02194 update DB 2025-06-30 07:25:27 +09:00
07f587ed83 Add KarmaNodes to Comfy Registry (#1958)
Co-authored-by: Karma Swint <karmaaswint@gmail.com>
2025-06-30 07:16:43 +09:00
S4MUELandGitHub 0408341d82 Add ComfyUI-S4Tool-Image to custom nodes list (#1957)
Add ComfyUI-S4Tool-Image to custom nodes list
2025-06-30 07:16:33 +09:00
Dr.Lt.Data 5b3c9432f3 update DB 2025-06-29 15:48:08 +09:00
Dr.Lt.Data 4a197e63f9 update DB 2025-06-28 23:31:08 +09:00
Dr.Lt.Data 0876a12fe9 update DB 2025-06-28 19:33:20 +09:00
Dr.Lt.Data c43c7ecc03 update DB 2025-06-28 18:15:49 +09:00
Dr.Lt.Data 4a6dee3044 update DB 2025-06-28 08:45:28 +09:00
Dr.Lt.Data 019acdd840 update DB 2025-06-28 08:22:30 +09:00
1c98512720 Update custom-node-list.json (#1955)
* 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-06-28 08:21:18 +09:00
Dr.Lt.Data 23a09ad546 update DB 2025-06-27 12:23:33 +09:00
Dr.Lt.Data 0836e8fe7c update DB 2025-06-27 07:23:09 +09:00
Dr.Lt.Data 90196af8f8 update DB 2025-06-27 01:48:34 +09:00
Dr.Lt.Data 566fe05772 update DB 2025-06-26 22:56:48 +09:00
uinodesandGitHub 18772c6292 Update custom-node-list.json (#1953) 2025-06-26 22:34:15 +09:00
Yuan-ManandGitHub 6278bddc9b Add ComfyUI-PosterCraft (#1952) 2025-06-26 22:33:02 +09:00
Dr.Lt.Data f74bf71735 update DB 2025-06-26 08:58:08 +09:00
Dr.Lt.Data efe9ed68b2 update DB 2025-06-26 06:56:56 +09:00
AmbrosinusandGitHub 7c1e75865d Add ComfyUI-ATk-Nodes plugin (#1949)
* Update custom-node-list.json

* Update custom-node-list.json

fixing the correct insertion of new entries in alphabetical order.
2025-06-26 06:37:29 +09:00
Dr.Lt.Data a0aee41f1a fixed: Support configuration with use_uv enabled in environments where only uv exists without pip.
https://github.com/Comfy-Org/ComfyUI-Manager/issues/1828
2025-06-25 12:44:26 +09:00
Dr.Lt.Data 2049dd75f4 update DB 2025-06-25 12:17:07 +09:00
Dr.Lt.Data 0864c35ba9 update DB 2025-06-25 07:27:45 +09:00
Dr.Lt.Data 92c9f66671 update DB 2025-06-25 00:52:31 +09:00
Dr.Lt.Data 815784e809 fixed: Fix issue where some nodepacks were displayed redundantly in custom nodes manager. 2025-06-25 00:18:18 +09:00
Dr.Lt.Data 2795d00d1e update DB 2025-06-24 23:39:31 +09:00
Dr.Lt.Data 86dd0b4963 update DB 2025-06-24 07:17:40 +09:00
Dr.Lt.Data 77a4f4819f update DB 2025-06-24 00:18:16 +09:00
Dr.Lt.Data b63d603482 update DB 2025-06-23 23:40:54 +09:00
Dr.Lt.Data e569b4e613 update DB 2025-06-23 12:35:42 +09:00
Gero DollandGitHub 8a70997546 Add ComfyUI Face Detection Node (#1947) 2025-06-23 12:29:58 +09:00
Dr.Lt.Data 80d0a0f882 update DB 2025-06-23 08:47:23 +09:00
Dr.Lt.Data 70b3997874 update DB 2025-06-23 06:53:30 +09:00
Dr.Lt.Data e8e4311068 update DB 2025-06-22 18:43:10 +09:00
Dr.Lt.Data c58b93ff51 update DB 2025-06-22 00:31:46 +09:00
Dr.Lt.Data 7d8ebfe91b update DB 2025-06-22 00:08:43 +09:00
Dr.Lt.Data 810381eab2 update DB 2025-06-22 00:03:44 +09:00
Dr.Lt.Data 61dc6cf2de update DB 2025-06-21 23:35:58 +09:00
NumZandGitHub 0205ebad2a Add ComfyUI-SeedVR2_VideoUpscaler Nodes (#1945)
* Update custom-node-list.json for Comfyui-Orpheus

add custom nodes from https://github.com/numz/Comfyui-Orpheus

* Update custom-node-list.json

add ComfyUI-SeedVR2_VideoUpscaler Node
2025-06-21 23:34:47 +09:00
Dr.Lt.Data 09a94133ac update DB 2025-06-21 23:34:05 +09:00
Dr.Lt.Data 1eb3c3b219 update DB 2025-06-21 23:25:10 +09:00
Alejandro Olivares MompóandGitHub 457845bb51 Add Kaizen Package by aleolidev (#1946) 2025-06-21 23:18:54 +09:00
Yuan-ManandGitHub 0c11b46585 Add ComfyUI-OmniGen2 (#1944) 2025-06-21 23:17:36 +09:00
Dr.Lt.Data c35100d9e9 update DB 2025-06-21 00:51:05 +09:00
Dr.Lt.Data 847031cb04 update DB 2025-06-20 12:33:28 +09:00
Dr.Lt.Data f8d87bb452 update DB 2025-06-20 07:38:39 +09:00
Dr.Lt.Data f60b3505e0 update DB 2025-06-19 20:44:57 +09:00
Dr.Lt.Data addefbc511 update DB 2025-06-19 12:55:15 +09:00
Dr.Lt.Data c4314b25a3 update DB 2025-06-19 07:34:54 +09:00
Dr.Lt.Data 921bb86127 update DB 2025-06-18 12:37:38 +09:00
Dr.Lt.Data b3a7fb9c3e update DB 2025-06-17 23:53:40 +09:00
Lord LethrisandGitHub c143c81a7e Update custom-node-list.json (#1941) 2025-06-17 23:46:54 +09:00
Dr.Lt.Data dd389ba0f8 update DB 2025-06-17 22:34:28 +09:00
seeoandGitHub 46b1649ab8 Update custom-node-list.json (#1940) 2025-06-17 22:24:24 +09:00
Dr.Lt.Data 89710412e4 fixed: indentation error 2025-06-17 07:27:46 +09:00
Dr.Lt.Data 931973b632 update DB 2025-06-17 07:22:13 +09:00
Dr.Lt.Data 60aaa838e3 update DB 2025-06-17 00:52:22 +09:00
Dr.Lt.Data 1246538bbb fixed: Issue where installation status was not properly recognized when the nodepack ID registered in the registry was not normalized.
- ex) `ComfyUI-Crystools`

https://github.com/Comfy-Org/ComfyUI-Manager/issues/1834#issuecomment-2937370214
2025-06-17 00:31:51 +09:00
Dr.Lt.Data 80518abf9d update DB 2025-06-16 22:42:41 +09:00
Leon WongandGitHub fc1ae2a18e added comfyui-leon-nodes to ustom-node-list.json (#1937) 2025-06-16 22:17:45 +09:00
Yuan-ManandGitHub 3fd8d2049c Add ComfyUI-Hunyuan3D-2.1 (#1936) 2025-06-16 22:16:50 +09:00
Dr.Lt.Data 35a6bcf20c update DB 2025-06-16 12:52:05 +09:00
Dr.Lt.Data 0d75fc331e update DB 2025-06-16 07:28:55 +09:00
Paolo Calvi 6d87c3e981 Add ComfyUI-PuLID-Flux-Chroma - First PuLID implementation for Chroma models. 2025-06-16 00:20:47 +02:00
Dr.Lt.Data 0a23e793e3 update DB 2025-06-15 15:43:09 +09:00
Dr.Lt.Data 2c1c03e063 update DB 2025-06-15 14:27:27 +09:00
64059d2949 Added ComfyUI-YouTubeUploader to custom nodes json (#1933)
* Update custom-node-list.json

Added ComfyUI-YouTubeUploader

* Update custom-node-list.json

* Update custom-node-list.json

Added proper link

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-06-15 14:13:03 +09:00
Dr.Lt.Data 648aa7c4d3 update DB 2025-06-14 18:56:19 +09:00
Dr.Lt.Data 274bb81a08 update DB 2025-06-14 10:06:34 +09:00
Dr.Lt.Data e2c90b4681 update DB 2025-06-13 22:41:52 +09:00
Dr.Lt.Data fa0a98ac6e update DB 2025-06-13 12:53:51 +09:00
Dr.Lt.Data e6e7b42415 update DB 2025-06-13 03:01:18 +09:00
Dr.Lt.Data 0b7ef2e1d4 update DB 2025-06-12 18:21:40 +09:00
Yuan-ManandGitHub 2fac67a9f9 Add ComfyUI-Vui (#1930) 2025-06-12 18:15:32 +09:00
Dr.Lt.Data 8b9892de2e update DB 2025-06-12 12:31:04 +09:00
Dr.Lt.Data b3290dc909 update DB 2025-06-12 12:24:22 +09:00
3e3176eddb Update custom-node-list.json for new node: Add ComfyUI LoRA Auto Downloader (#1929)
* Add ComfyUI LoRA Auto Downloader extension

Adding ComfyUI LoRA Auto Downloader extension to the registry.
- Automatically downloads missing LoRAs from CivitAI
- Detects missing LoRAs in workflows
- Smart directory detection

* Update custom-node-list.json

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-06-12 12:22:50 +09:00
Dr.Lt.Data b1ef84894a update DB 2025-06-12 12:22:02 +09:00
hassan-sdandGitHub c6cffc92c4 Update custom-node-list.json for new node: comfyui-image-prompt-loader (#1928)
https://github.com/hassan-sd/comfyui-image-prompt-loader

Load images with automatic prompt extraction from Civitai URLs, caption files, or EXIF metadata. Features smart dataset detection and dynamic preview updates.
2025-06-12 12:16:27 +09:00
Dr.Lt.Data efb9fd2712 update DB 2025-06-12 07:21:17 +09:00
Dr.Lt.Data 94b294ff93 update DB 2025-06-12 07:17:09 +09:00
Dr.Lt.Data 99a9e33648 update DB 2025-06-11 22:11:42 +09:00
055d94a919 add node extractstoryboards (#1927)
* add node extractstoryboards

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-06-11 22:00:32 +09:00
Dr.Lt.Data 0978005240 update DB 2025-06-11 12:31:34 +09:00
Yuan-ManandGitHub 1f796581ec Add ComfyUI-Direct3D-S2 node (#1925) 2025-06-11 07:31:56 +09:00
Dr.Lt.Data f3a1716dad update DB 2025-06-11 07:23:14 +09:00
ZacharyandGitHub a1c3a0db1f add my custom node for read metadata from filepath. (#1926) 2025-06-11 06:59:54 +09:00
Dr.Lt.Data 9f80cc8a6b update DB 2025-06-10 12:27:20 +09:00
Dr.Lt.Data 133786846e update DB 2025-06-10 07:28:53 +09:00
keitandGitHub bdf297a5c6 Add ComfyUI-keitNodes (#1924) 2025-06-10 07:28:02 +09:00
Dr.Lt.Data 6767254eb0 update DB 2025-06-10 07:27:48 +09:00
11dogziandGitHub 691cebd479 CYBERPUNK-STYLE-DIY (#1923) 2025-06-10 07:26:14 +09:00
f3932cbf29 Add Comfyui-Dynamic-Params Node Plugin (#1922)
* Update custom-node-list.json to add Comfyui-Dynamic-Params Node

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-06-10 07:25:52 +09:00
Dr.Lt.Data 3f73a97037 update DB 2025-06-10 07:25:40 +09:00
ErehrandGitHub 226f1f5be4 Add ComfyUI-EreNodes (#1921)
* Add ComfyUI-EreNodes

* Update custom-node-list.json
2025-06-10 07:23:53 +09:00
Dr.Lt.Data 7e45c07660 update DB 2025-06-10 07:23:40 +09:00
INuBq8andGitHub 0c815036b9 Update custom-node-list.json (#1920) 2025-06-10 07:22:31 +09:00
Dr.Lt.Data ae9fdd0255 update DB 2025-06-09 07:19:09 +09:00
Vlad BondarovichandGitHub b3874ee6fd Update custom-node-list.json (#1917) 2025-06-09 06:06:15 +09:00
Eric W. BurnsandGitHub 62af4891f3 Update custom-node-list.json (#1912)
Submitting my new custom nodes at https://github.com/burnsbert/ComfyUI-EBU-Workflow for inclusion, thanks!
2025-06-09 06:02:16 +09:00
Budi HartonoandGitHub 2176e0c0ad Add CAS Aspect Ratio Presets Node for ComfyUI to custom-node-list.json (#1910)
Add a custom node to quickly create empty latents in common resolutions and aspect ratios for SD 1.5, SDXL, Flux, Chroma, and HiDream. Choose from curated presets or generate by axis and aspect ratio. Appears in the 'latent' node group.
2025-06-09 06:01:18 +09:00
DerrickandGitHub acc11e1bda Add ComfyUI_rgbx_xrgb_Wrapper 2025-06-07 20:55:40 -07:00
Dr.Lt.Data cac105b0d5 fixed: prevent halting when log flushing fails.
https://github.com/Comfy-Org/ComfyUI-Manager/issues/1794
2025-06-08 06:54:39 +09:00
Dr.Lt.Data cd7c42cc23 update DB 2025-06-08 06:39:30 +09:00
Dr.Lt.Data a3fb847773 fixed: Don't override preview method if --preview-method is given
https://github.com/Comfy-Org/ComfyUI-Manager/issues/1887
2025-06-08 06:33:42 +09:00
Dr.Lt.Data 5c2f4f9e4b fixed: Issue where cloning Comfy-Org/ComfyUI-Manager would cause mismatches with ltdrdata/ComfyUI-Manager, resulting in it not being recognized properly.
https://github.com/Comfy-Org/ComfyUI-Manager/issues/1900
2025-06-08 06:24:19 +09:00
Dr.Lt.Data 0a511d5b87 update DB 2025-06-08 05:00:25 +09:00
Dr.Lt.Data efe1aad5db update DB 2025-06-07 16:20:15 +09:00
Dr.Lt.Data eed4c53df0 update DB 2025-06-07 12:55:45 +09:00
Dr.Lt.Data 9c08a6314b update DB 2025-06-07 12:32:42 +09:00
PigidiyandGitHub a6b2d2c722 Add ComfyUI-LikeSpiderAI-UI (UI Framework for Node Creators) (#1907)
This PR adds a declarative UI framework for ComfyUI nodes: ComfyUI-LikeSpiderAI-UI.

Highlights:
- Minimalistic base class: LikeSpiderUINode
- Built-in input schema with auto-generated UI
- Example node: AudioExport (supports mp3/wav/flac + bitrate/filename)
- Designed for extensibility and clean UX

Author: Pigidiy
2025-06-07 12:31:47 +09:00
Dr.Lt.Data 3c6b5300e5 update DB 2025-06-06 14:37:15 +09:00
f084c30b20 Add LoRA-Safe TorchCompile node (#1905)
* Add LoRA-Safe TorchCompile node

* Update custom-node-list.json

---------

Co-authored-by: xmarre <mmquant1@gmail.com>
Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-06-06 14:17:19 +09:00
Dr.Lt.Data 206004fc1f update DB 2025-06-06 07:13:30 +09:00
Dr.Lt.Data d9641cbff8 update DB 2025-06-06 06:14:09 +09:00
Dr.Lt.Data 13b272052a update DB 2025-06-06 05:56:26 +09:00
MDMAchineandGitHub c79e0d26d8 Update custom-node-list.json (#1904)
Added:
https://github.com/MDMAchine/ComfyUI_MD_Nodes
2025-06-06 05:55:26 +09:00
Dr.Lt.Data ec4a4c2cfc update DB 2025-06-06 05:53:38 +09:00
9a9491bff9 Add Comfy-Topaz-Photo (#1901)
* Update custom-node-list.json

Add Comfy-Topaz-Photo

* Update custom-node-list.json

* Update custom-node-list.json

* Update custom-node-list.json

Add Comfy-Topaz-Photo

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-06-06 05:53:13 +09:00
Dr.Lt.Data 5b5155819f update DB 2025-06-06 05:52:34 +09:00
1b941c6b29 Fix: correct author & ID for ComfyUI-LikeSpiderAI-SaveMP3 (#1899)
* Fix: correct author & ID for ComfyUI-LikeSpiderAI-SaveMP3

This PR corrects the metadata for the ComfyUI-LikeSpiderAI-SaveMP3 node:

Changes author from aimingfail → Pigidiy

Adds missing version field: v1.0.0

Updates id from img2halftone → likeSpiderMP3

The previous metadata was mistakenly duplicated from another node.

Project repo: https://github.com/Pigidiy/ComfyUI-LikeSpiderAI-SaveMP3

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-06-06 05:51:14 +09:00
9b9665d2e9 Update custom-node-list.json (Add ComfyUI-E-Tier-TextSaver to node list) (#1879)
* Update custom-node-list.json

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-06-06 05:49:02 +09:00
Dr.Lt.Data 4cceb46641 update DB 2025-06-03 18:50:49 +09:00
Dr.Lt.Data 19cf83cce6 update DB 2025-06-03 18:47:13 +09:00
Dr.Lt.Data bb60d399fc update DB 2025-06-03 13:57:28 +09:00
Dr.Lt.Data 1a9f1dd0ae update DB 2025-06-03 10:47:49 +09:00
violetzandGitHub 586c465aaa Add custom node: Hugging Face LoRA Uploader (#1897) 2025-06-03 10:42:15 +09:00
Dr.Lt.Data 50ceb974d9 update DB 2025-06-03 10:42:03 +09:00
27cf40d392 Add: ComfyUI-LikeSpiderAI-SaveMP3 (save AUDIO to .mp3) (#1894)
* Add: ComfyUI-LikeSpiderAI-SaveMP3 (save AUDIO to .mp3)

Adds a node that saves AUDIO output to .mp3 format via ffmpeg.
Repo: https://github.com/Pigidiy/ComfyUI-LikeSpiderAI-SaveMP3

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-06-03 10:39:15 +09:00
Dr.Lt.Data bbb6005634 fixed: scanner
update DB
2025-06-03 10:36:48 +09:00
8dbd996558 Add ComfyUI Fix Node Translate custom node (#1892)
* 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-06-03 10:35:41 +09:00
Dr.Lt.Data 8605345499 update DB 2025-06-01 06:55:16 +09:00
Dr.Lt.Data 3671ddbd4b update DB 2025-06-01 04:30:56 +09:00
Dr.Lt.Data 5bc1ceacb2 update DB 2025-06-01 04:11:34 +09:00
YuSuuandGitHub 47b9fa3651 Add comfyui-merge plugin info (#1866) 2025-06-01 04:10:42 +09:00
Dr.Lt.Data 6062b87771 update DB 2025-06-01 04:09:55 +09:00
Yuan-ManandGitHub 213152aa43 Add ComfyUI-ChatterboxTTS node (#1888) 2025-06-01 04:03:24 +09:00
Hiroaki OgasawaraandGitHub ea8047344f feat: ComfyUl-FramePackWrapper_PlusOne (#1891) 2025-06-01 04:01:57 +09:00
Dr.Lt.Data a7bc167d53 update DB 2025-05-30 12:42:14 +09:00
Yuan-ManandGitHub 18e78ee2c2 Add ComfyUI-HunyuanVideo-Avatar node (#1886) 2025-05-30 12:35:47 +09:00
Dr.Lt.Data 754236e35b update DB 2025-05-30 12:30:21 +09:00
Dr.Lt.Data 2645d62991 fixed: scanner.py - better limitation check 2025-05-30 07:26:03 +09:00
Dr.Lt.Data e55d9416dc update DB 2025-05-29 07:49:40 +09:00
Yuan-ManandGitHub 24d35eec54 Add ComfyUI-HunyuanPortrait node (#1882) 2025-05-29 05:29:50 +09:00
seungwoo-jiandGitHub ee053f50b4 fix: replace link to registry (#1883) 2025-05-29 05:27:13 +09:00
Dr.Lt.Data 3593c9ed3e update DB 2025-05-28 08:58:19 +09:00
Dr.Lt.Data 93f548696d update DB 2025-05-28 07:15:18 +09:00
Dr.Lt.Data cecb952add update DB 2025-05-27 07:01:44 +09:00
Ethan YangandGitHub 596571bb38 add openvino custom node (#1864) 2025-05-27 06:28:23 +09:00
filteredandGitHub 85a6fb75b8 Add workaround for delay in link connection (#1873)
New input sockets have no pos, and require a render frame to occur before links can be set to the correct location.
2025-05-27 06:27:45 +09:00
Dominik BargielandGitHub 7dea42433b Update custom-node-list.json with Deadline Rneder manager plugin (#1874) 2025-05-27 06:27:06 +09:00
Faych ChenandGitHub ec5e4af6b7 feat: Add ComfyUI-BAGEL custom node (#1875) 2025-05-27 06:26:24 +09:00
Dr.Lt.Data 0048754fe8 fixed: An issue occurs when attempting to update a node pack installed via git clone if its URL has changed or if the node is not registered in custom-nodes-list.json.
https://github.com/Comfy-Org/ComfyUI-Manager/issues/1834#issuecomment-2907690538
2025-05-26 02:21:25 +09:00
Dr.Lt.Data 5c0bd0f79c bump version 2025-05-26 01:41:49 +09:00
Alexander PiskunandGitHub 669cdffe08 fix(manager_util): used non normalized package name (#1867)
* set channel=default, mode=cache for git clone

* fix(manager_util): use normalized_name of package in fix_broken

Signed-off-by: bigcat88 <bigcat88@icloud.com>

---------

Signed-off-by: bigcat88 <bigcat88@icloud.com>
2025-05-26 01:41:07 +09:00
Dr.Lt.Data 3cd553301b update DB 2025-05-26 01:27:39 +09:00
hmwlandGitHub db7ef4f253 Add ComfyUI-TaskMonitor node (#1871) 2025-05-26 01:14:00 +09:00
Level PixelandGitHub a09704567c Update custom-node-list.json for Level Pixel Advanced nodes (#1870)
Splitting the Level Pixel node package into two separate packages:
https://github.com/LevelPixel/ComfyUI-LevelPixel
https://github.com/LevelPixel/ComfyUI-LevelPixel-Advanced

Adding information about the new ComfyUI-LevelPixel-Advanced node package to custom-node-list.json.

The new ComfyUI-LevelPixel-Advanced node package is needed to separate the complex to install and use LLM and VLM node package from the rest of the main nodes of Level Pixel.

Conflicting nodes will be removed from ComfyUI-LevelPixel later.
2025-05-26 01:12:16 +09:00
Dr.Lt.Data 21fe577a2e update DB 2025-05-25 23:51:21 +09:00
Yuan-ManandGitHub 9f258f5c9c Add ComfyUI-Bagel node (#1863) 2025-05-25 23:44:55 +09:00
Dr.Lt.Data 9cd088feb0 update DB 2025-05-23 15:10:47 +09:00
Dr.Lt.Data 89e3828138 update DB 2025-05-21 22:23:08 +09:00
Christian ByrneandGitHub 731c89dc27 [api] Add OpenAPI specification file (#1856) 2025-05-21 21:48:50 +09:00
Yuan-ManandGitHub 3d920cab4d Add ComfyUI-AniSora node (#1860) 2025-05-21 21:47:04 +09:00
TrophiHunterandGitHub 470b8c1fb8 Update custom-node-list.json (#1858)
Fixed node references to github
2025-05-21 21:46:34 +09:00
Christian ByrneandGitHub dbf988fd5a [docs] Add README for docs directory (#1855)
* [docs] Add README for docs directory

* [docs] Remove redundant sections from docs README
2025-05-21 21:45:17 +09:00
Christian ByrneandGitHub 0031743ad4 [docs] Add README for node_db directory (#1854) 2025-05-21 21:45:05 +09:00
Christian ByrneandGitHub 0f2c0ab65d [docs] Add README for js directory (#1853)
* [docs] Add README for js directory

* [docs] Update js/README.md based on PR review feedback

* [docs] Update js/README.md with corrected descriptions
2025-05-21 21:44:48 +09:00
Christian ByrneandGitHub 53244b794f [docs] Add README for glob directory (#1852) 2025-05-21 21:44:24 +09:00
Dr.Lt.Data 416122d61d update DB 2025-05-21 00:03:10 +09:00
Dr.Lt.Data d3c625e791 update DB 2025-05-20 23:43:34 +09:00
ca2c41783c Add AQnodes (#1849)
* add AQnodes

* add AQnodes - fix repo url

---------

Co-authored-by: pk <poczta@aquasite.pl>
2025-05-20 23:42:57 +09:00
Dr.Lt.Data e2a6446585 update DB 2025-05-20 23:42:44 +09:00
ICAI Icelandic Center for Artificial IntelligenceandGitHub 839790b5ab Update custom-node-list.json (#1848)
added entry for Sample Scheduler Metrics Tester custom node
2025-05-20 23:41:32 +09:00
jqy-yoandGitHub 58b9946936 Add Comfyui-BBoxLowerMask2 to custom-node-list (#1842) 2025-05-20 23:41:00 +09:00
Dr.Lt.Data a19ba22eaf update DB 2025-05-20 23:40:40 +09:00
Yuan-ManandGitHub 117715aa22 Add ComfyUI-MoviiGen node (#1846) 2025-05-20 23:35:37 +09:00
891a5a85ee add ModelQuantizer node to custom node list (#1806)
* add-ModelQuantizer to custom node list

* Update custom-node-list.json

---------

Co-authored-by: yogotatara3 <milan.kastenmueller@thjnk.de>
Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-05-20 23:32:43 +09:00
Dr.Lt.Data 166debfabb modified: In Python 3.13, the functionality to forcibly downgrade the numpy version below 3.13 is disabled.
- Starting from Python 3.13, prebuilt wheels for `numpy` 1.26.4 are no longer provided.

https://github.com/comfyanonymous/ComfyUI/discussions/8187
2025-05-19 05:13:40 +09:00
Dr.Lt.Data 7258a09fe5 update DB 2025-05-19 05:03:54 +09:00
Dr.Lt.Data 058a436187 update DB 2025-05-17 17:39:31 +09:00
Yuan-ManandGitHub 1950802c55 Update ComfyUI-Step1X-3D node (#1840) 2025-05-17 17:11:51 +09:00
Dr.Lt.Data eb52a03372 update DB 2025-05-16 03:52:03 +09:00
Dr.Lt.Data f8aa428be3 update DB 2025-05-15 22:09:48 +09:00
Dr.Lt.Data ec0893f136 update DB 2025-05-15 21:48:56 +09:00
TrophiHunterandGitHub 92b99ea963 Update custom-node-list.json (#1832)
add my nodes to manager
2025-05-15 21:47:37 +09:00
Dr.Lt.Data 02cd52bb65 update DB 2025-05-15 21:45:19 +09:00
DontdrunkandGitHub af1ec2c87b Update custom-node-list.json (#1818)
* Submit Registration

* Update custom-node-list.json

* Update custom-node-list.json
2025-05-15 21:43:29 +09:00
Dr.Lt.Data 41006c3a33 update DB 2025-05-15 08:09:03 +09:00
116a6d500d model-list: add new ltxv 13b distilled models. (#1835)
Co-authored-by: gschreiber <gschreiber@infra-image-generator.c.ltx-research-vms.internal>
2025-05-15 08:03:12 +09:00
Dr.Lt.Data 87d0ac807f update DB 2025-05-15 07:24:34 +09:00
Dr.Lt.Data fc943172eb update DB 2025-05-14 06:07:35 +09:00
9daa5a2fbd fix: update ltxv upscale models metadata. (#1830)
Co-authored-by: gschreiber <gschreiber@infra-image-generator.c.ltx-research-vms.internal>
2025-05-14 06:07:22 +09:00
Dr.Lt.Data b7b2746a61 update DB 2025-05-13 03:36:18 +09:00
Dr.Lt.Data d66a4fbfc8 update DB 2025-05-13 03:23:47 +09:00
Dr.Lt.Data 683a172ad8 modified: Added a feature to prevent numpy from being forcibly downgraded to below 2 via pip_overrides.json.
https://github.com/Comfy-Org/ComfyUI-Manager/issues/1665#issuecomment-2862099191
2025-05-13 03:04:27 +09:00
Dr.Lt.Data 6e12358f5a update DB 2025-05-13 02:56:36 +09:00
Dr.Lt.Data 8bcf16dc90 fixed: A type error occurred during the creation of the pip fixer object when an error occurred while retrieving the list of installed packages.
https://github.com/Comfy-Org/ComfyUI-Manager/issues/1804
2025-05-13 02:46:34 +09:00
Dr.Lt.Data 65c0a2a1f5 update DB 2025-05-13 02:10:21 +09:00
Alastor 666 1933andGitHub 115236eb9c adding caching_to_not_waste custom node (#1786) 2025-05-13 02:06:23 +09:00
Dr.Lt.Data 08de942abe update DB 2025-05-13 02:05:51 +09:00
Seb HirschandGitHub e9dff83290 Update custom-node-list.json (#1802)
added seb nodes
2025-05-13 02:02:55 +09:00
Yuan-ManandGitHub 3bc6c7584d Add ComfyUI-Muyan-TTS node (#1805) 2025-05-13 02:00:54 +09:00
Dr.Lt.Data 22a2bf1584 Apply https://github.com/Comfy-Org/ComfyUI-Manager/pull/1811 to prestartup_script as well. 2025-05-13 01:59:42 +09:00
79ece5f72c fix: handle pip package names with inline comments during installation (#1811)
Co-authored-by: Tomasz Dowgielewicz <todowgielewicz@artflow.me>
2025-05-13 01:53:44 +09:00
5da6fe1373 extract_url_and_commit_id (#1813)
Co-authored-by: chenyijian <chenyijian@infini-ai.com>
2025-05-13 01:52:02 +09:00
moldwebsandGitHub 48c10d0b95 Show models used in current workflow (#1819)
Simple javascript modify that filter models used in current workflow
2025-05-13 01:48:29 +09:00
Dr.Lt.Data 9bb56b1457 update DB 2025-05-13 01:46:26 +09:00
83420fd828 Add ComfyUI-1hewNodes to custom node list (#1826)
Co-authored-by: yige1127 <wangyihe370875982@gmail.com>
2025-05-13 01:45:34 +09:00
Dr.Lt.Data 52f4b9506f update DB 2025-05-13 01:44:07 +09:00
fpgaminerandGitHub b501e9b20b Add fpgaminer/joycaption_comfyui to custom-node-list.json (#1827) 2025-05-13 01:43:28 +09:00
Dr.Lt.Data 1f7ae5319a update DB 2025-05-13 01:42:35 +09:00
Goshe-niteandGitHub 68c201239d Update custom-node-list.json (#1825) 2025-05-13 01:42:13 +09:00
Dr.Lt.Data 6e4e43f612 update DB 2025-05-13 01:41:12 +09:00
AIWarperandGitHub 81c3708f39 Add NormalCrafterWrapper custom node by AIWarper (#1816) 2025-05-13 01:40:43 +09:00
Dr.Lt.Data f4d2bbde34 update DB 2025-05-13 01:40:25 +09:00
gasparuffandGitHub d14b42a42c Update custom-node-list.json (#1810)
added customselector node to custom-node-list.json
2025-05-13 01:34:46 +09:00
Dr.Lt.Data 0e9c32344c fix: syntax error 2025-05-12 18:33:24 +09:00
Liangbin LianandGitHub 30c4ea06af fix model DB for Hyper-SD LoRA (4steps) - SDXL (#1815) 2025-05-12 18:20:42 +09:00
Fadel MochammadandGitHub 8211264993 Add inline comment to __init__.py (#1823) 2025-05-12 18:15:27 +09:00
ClownsharkBatwingandGitHub 67cf5b49e1 Update custom-node-list.json (#1821) 2025-05-12 18:15:12 +09:00
Dr.Lt.Data 8e7ba18e05 update DB 2025-05-09 08:04:39 +09:00
Dr.Lt.Data 8359e1063e update DB 2025-05-09 07:23:33 +09:00
ca078e54b9 Add 'exit-on-fail' parameter to control failure behavior (#1807)
Co-authored-by: chenyijian <chenyijian@infini-ai.com>
2025-05-09 07:08:41 +09:00
Dr.Lt.Data f7e930c5a2 update DB 2025-05-08 02:03:46 +09:00
Dr.Lt.Data 479d95e1c8 update DB 2025-05-08 01:43:01 +09:00
Demis BellotandGitHub 2b0ff08eef Add ComfyUI Asset Downloader (#1799) 2025-05-08 01:34:02 +09:00
Dr.Lt.Data 67a487db15 update DB 2025-05-08 01:30:54 +09:00
Dr.Lt.Data 2488cb3458 update DB 2025-05-08 00:11:28 +09:00
Dr.Lt.Data 157e6336fa update DB 2025-05-08 00:09:38 +09:00
d808a1f406 Add ComfyUI DAM Object Extractor node (#1796)
* 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-05-08 00:08:58 +09:00
Dr.Lt.Data 2bb4d8cd63 update DB 2025-05-08 00:08:42 +09:00
a8164e1631 Update custom-node-list.json (#1791)
* Update custom-node-list.json

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-05-08 00:07:50 +09:00
Dr.Lt.Data a31d286945 update DB 2025-05-08 00:05:49 +09:00
wakattacandGitHub 12eeef4cf0 Update custom-node-list.json (#1793) 2025-05-08 00:04:36 +09:00
Yuan-ManandGitHub ce8e6dc36e Add ComfyUI-AudioX node (#1798) 2025-05-08 00:03:58 +09:00
SssnapandGitHub 7a32e544a7 Update custom-node-list.json (#1792) 2025-05-07 23:54:45 +09:00
Dr.Lt.Data e16e9d7a0e update DB 2025-05-03 23:40:58 +09:00
unicoughandGitHub 821f908dbc Update custom-node-list.json (#1784) 2025-05-03 23:12:05 +09:00
Dr.Lt.Data e007e6f897 update DB 2025-05-01 02:08:58 +09:00
Yuan-ManandGitHub 94f496fd65 Add ComfyUI-Step1X-Edit node (#1780) 2025-05-01 01:15:03 +09:00
Dr.Lt.Data d2ce35d2e6 update DB 2025-05-01 01:13:31 +09:00
somesomebodyandGitHub 2eeebb32dc Add comfyui-lorainfo-sidebar to custom node list (#1778) 2025-05-01 01:12:44 +09:00
SanderandGitHub f6d636d82f Add fixed MagicQuill node (#1768) 2025-05-01 01:08:11 +09:00
Dr.Lt.Data 0cd397623e update DB 2025-04-29 00:21:59 +09:00
Dr.Lt.Data 5978b6c9ee updated: PIPFixer - support for pytorch 2.7.0 2025-04-28 23:49:42 +09:00
Dr.Lt.Data 9e132811bc update DB 2025-04-28 00:43:52 +09:00
Dr.Lt.Data 3a3b5c1f92 update DB 2025-04-27 23:16:48 +09:00
hua(Kungfu)andGitHub 26be01ff82 Update custom-node-list.json (#1774) 2025-04-27 22:52:56 +09:00
Dr.Lt.Data 8f6dd92374 update DB 2025-04-26 18:24:56 +09:00
Dr.Lt.Data d50b71a887 update DB 2025-04-26 14:51:07 +09:00
Dr.Lt.Data 3bc9cbc767 update DB 2025-04-26 13:16:26 +09:00
Yuan-ManandGitHub b6f6b4fd8a Add ComfyUI-LiveCC node (#1770) 2025-04-26 13:12:14 +09:00
Christian ByrneandGitHub a66bada8a3 Update workflow-metadata.js 2025-04-23 17:24:07 -07:00
Dr.Lt.Data a804f7de19 update DB 2025-04-22 02:14:50 +09:00
Dr.Lt.Data 72a61a9966 modified: pipfixer/blacklisting - add torchaudio 2025-04-22 01:17:22 +09:00
Dr.Lt.Data b08bb658ea update DB 2025-04-22 01:13:57 +09:00
Dr.Lt.Data 7b28bf608b modified: release pinning ultralytics version 2025-04-22 00:43:20 +09:00
Dr.Lt.Data b57747fdf1 update DB 2025-04-20 18:49:43 +09:00
Dr.Lt.Data 0735271b10 update DB 2025-04-20 17:13:47 +09:00
Dr.Lt.Data 770cd0f9f5 update DB 2025-04-19 10:31:07 +09:00
Dr.Lt.Data 32b6266dd9 update DB 2025-04-19 09:39:43 +09:00
NumZandGitHub 2a8412a2bf Update custom-node-list.json for Comfyui-Orpheus (#1754)
add custom nodes from https://github.com/numz/Comfyui-Orpheus
2025-04-19 09:35:28 +09:00
Dr.Lt.Data 0c4d289002 update DB 2025-04-19 09:34:52 +09:00
Nisaruj RattanaaramandGitHub cee01fec25 Add comfyui-daam to custom node list (#1753)
* Update custom-node-list.json

* Update description
2025-04-19 09:34:17 +09:00
Dr.Lt.Data f00686f3f2 update DB 2025-04-19 09:34:07 +09:00
FunnyFingerandGitHub bd33f7726e Add Dynamic Sliders Stack to custom node list (#1750)
* Update custom-node-list.json

* Update custom-node-list.json

Added my custom node to the list
2025-04-19 09:33:08 +09:00
Dr.Lt.Data 22ab526b0c update DB 2025-04-19 09:32:30 +09:00
Christian ByrneandGitHub af269d198d trim version string embedded in workflow (#1758) 2025-04-19 09:30:41 +09:00
Yuan-ManandGitHub 995ef6356e Add ComfyUI-Kimi-VL node (#1756) 2025-04-19 09:30:02 +09:00
杨必赞andGitHub aa3bf77c28 Update custom-node-list.json (#1752) 2025-04-19 09:29:15 +09:00
DantedayandGitHub 15667c1259 Update custom-node-list.json (#1751) 2025-04-19 09:28:53 +09:00
zzw5516andGitHub c7b6b565da feat: Add ComfyUI-zw-tools custom node to list (#1749) 2025-04-19 09:27:55 +09:00
Dr.Lt.Data 3214ab52c6 update DB 2025-04-15 23:40:14 +09:00
LegendeandGitHub e3062ff613 Add custom node xLegende/ComfyUI-Prompt-Formatter (#1741)
Custom node for formating prompts
2025-04-15 23:18:13 +09:00
Yoland YanandGitHub 036b63efe7 Change order of manager to be default install lateste (#1747) 2025-04-15 18:46:24 +09:00
Dr.Lt.Data 8d3e1d60d0 update DB 2025-04-15 01:24:42 +09:00
Dr.Lt.Data 59876452f4 update DB 2025-04-15 00:37:10 +09:00
04972ad87f feat: Register ComfyUI-ResolutionPresets to custom nodes list (#1738)
* Add: register ComfyUI-ResolutionPresets

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-04-15 00:29:27 +09:00
Dr.Lt.Data c7e69f4e26 update DB 2025-04-15 00:28:53 +09:00
7a59b6d0d9 Update custom-node-list.json (#1745)
* Update custom-node-list.json

Add Comfy-Topaz-Photo

* Update custom-node-list.json

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-04-15 00:28:03 +09:00
Yuan-ManandGitHub d227ad97a4 Add ComfyUI-HiDream-I1 node (#1744) 2025-04-15 00:25:45 +09:00
Dr.Lt.Data b93a474dae update DB 2025-04-15 00:23:42 +09:00
SilverandGitHub a5fe075bf3 Add custom node silveroxides/ComfyUI-ModelUtils (#1652)
Custom nodes project for model management.
2025-04-15 00:22:38 +09:00
Dr.Lt.Data 17e5c3d2f5 update DB 2025-04-12 21:20:45 +09:00
Dr.Lt.Data 27bfc539f7 fixed: Removed the possibility of locking by opening the git repo.
https://github.com/Comfy-Org/ComfyUI-Manager/issues/1717
2025-04-12 21:10:14 +09:00
Dr.Lt.Data 821fded09d update DB 2025-04-12 17:26:41 +09:00
Dr.Lt.Data ec4a2aa873 update DB 2025-04-12 15:22:09 +09:00
Dr.Lt.Data d6b2d54f3f update DB 2025-04-12 15:20:29 +09:00
Jerry ChukwudiandGitHub 97ae67bb9a Add LoadImageFromHttpURL node by jerrywap (#1732)
Add LoadImageFromHttpURL node by jerrywap
2025-04-12 15:18:35 +09:00
SanderandGitHub 765514a33f Added ComfyUI-api-tools (#1733)
Custom node for to add some extra api endpoints, including prometheus monitoring
2025-04-12 15:17:50 +09:00
Yuan-ManandGitHub e2cdcc96c4 Add ComfyUI-UNO node (#1735) 2025-04-12 15:16:57 +09:00
Dr.Lt.Data 0738b2a73f update DB 2025-04-09 00:28:04 +09:00
Dr.Lt.Data 98db79910e update DB 2025-04-09 00:16:11 +09:00
cganimittaandGitHub 0b21a05aac Update custom-node-list.json (#1724) 2025-04-09 00:14:50 +09:00
Dr.Lt.DataandGitHub 4b71db54aa Revert "Add KERRY-YUAN/ComfyUI_Simple_Executor nodes (#1721)" (#1725)
This reverts commit a6bc890f36.
2025-04-09 00:13:37 +09:00
KerryandGitHub a6bc890f36 Add KERRY-YUAN/ComfyUI_Simple_Executor nodes (#1721)
{
            "author": "KERRY-YUAN",
            "title": "NodeSimpleExecutor",
            "id": "NodeSimpleExecutor",
            "reference": "https://github.com/KERRY-YUAN/ComfyUI_Simple_Executor",
            "files": [
                "https://github.com/KERRY-YUAN/ComfyUI_Simple_Executor"
            ],
            "install_type": "git-clone",
            "description": "This node package contains automatic sampler setting according to model name in ComfyUI, adjusting image size according to specific constraints and some other nodes."
        },
2025-04-09 00:12:08 +09:00
jerrywapandGitHub 76903c39e1 Update custom-node-list.json (#1723)
This extension sends generated images or videos to any HTTP webhook with optional parameters such as prompt-id and meta-data.
This is useful for those rendering COMFYUI as an api service and need to send a webhook to requested api when task is successful.
2025-04-07 22:45:51 +09:00
Yuan-ManandGitHub cf9ed1c631 Add ComfyUI-SkyReels-A2 node (#1719) 2025-04-07 22:42:44 +09:00
Dr.Lt.Data 50fc1389b0 update DB 2025-04-05 17:50:01 +09:00
Dr.Lt.Data c70cb2868b update DB 2025-04-05 16:39:26 +09:00
Dr.Lt.Data 12fa571aa2 update DB 2025-04-05 00:13:02 +09:00
Dr.Lt.Data 4a3018760f update DB 2025-04-04 23:54:37 +09:00
VertexAnomalyandGitHub d005d06cf8 Update custom-node-list.json (#1713) 2025-04-04 23:16:57 +09:00
Dr.Lt.Data a87e3f9ee9 update DB 2025-04-04 08:21:38 +09:00
Dr.Lt.Data 52b9a3f3a0 update DB 2025-04-04 07:09:13 +09:00
Yuan-ManandGitHub c01a7e41d0 Add ComfyUI-LayerAnimate node (#1705) 2025-04-04 07:05:28 +09:00
Dr.Lt.Data fe301bb91a update DB 2025-04-04 06:56:55 +09:00
CY-CHENYUEandGitHub a42953e3be Update custom-node-list.json (#1699) 2025-04-04 06:56:03 +09:00
Dr.Lt.Data 1899255a69 update DB 2025-04-04 06:54:56 +09:00
Dr.Lt.Data 908a1009d2 fixed: cm-cli.py - save-snapshot: use default path if --output is not given
https://github.com/Comfy-Org/comfy-cli/issues/254#issuecomment-2758584763
2025-03-28 12:49:09 +09:00
Dr.Lt.Data fb9c68fc32 update DB 2025-03-28 12:47:39 +09:00
yushan777andGitHub d54ec0eb05 Update custom-node-list.json (#1696) 2025-03-28 12:46:44 +09:00
Dr.Lt.Data a386948fd1 update DB 2025-03-28 01:57:13 +09:00
CreepybitsandGitHub 007b812ede Update custom-node-list.json (#1685)
Added Creepy_nodes
2025-03-28 01:47:06 +09:00
Dr.Lt.Data 0ddb0cec03 update DB 2025-03-28 01:06:21 +09:00
chrisgoringeandGitHub e687f83fbf Update custom-node-list.json (#1692)
Removed the deprecated image-picker and replaced with the new image-filter
2025-03-28 01:03:05 +09:00
rainlizardandGitHub 458c9de70f Update custom-node-list.json - Add "Raffle" (#1686) 2025-03-28 01:00:17 +09:00
Dr.Lt.Data 87a652d038 fixed: channel error when DB: local is selected 2025-03-24 01:17:07 +09:00
Dr.Lt.Data d889df4c89 update DB 2025-03-24 01:16:09 +09:00
Dr.Lt.Data a2e72d26aa update DB 2025-03-22 12:29:34 +09:00
ImpactFramesandGitHub a4fdc874e7 add IF_Gemini node (#1678) 2025-03-22 11:56:07 +09:00
Yuan-ManandGitHub dfbe382d60 Add ComfyUI-OrpheusTTS node (#1679) 2025-03-22 11:55:36 +09:00
0d56ebb1bf Update custom-node-list.json (#1641)
* Update custom-node-list.json

im doing improve some nodes for my friends and this nodepack is growing by the time

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-03-22 11:54:55 +09:00
Dr.Lt.Data 9e66da174e hotfix: make_pip_cmd - don't apply -s always
https://github.com/ltdrdata/ComfyUI-Manager/issues/1667
2025-03-19 02:01:17 +09:00
Dr.Lt.Data 55fcb00168 update DB 2025-03-19 01:58:18 +09:00
Dr.Lt.Data 68aa534e1d fixed: An issue where restore malfunctioned since channel validation patch.
https://github.com/Comfy-Org/comfy-cli/issues/253
2025-03-19 01:24:20 +09:00
Dr.Lt.Data 7fd94a401b update DB 2025-03-19 00:23:24 +09:00
kukuo6666andGitHub 2b9cec50ce Update custom-node-list.json (#1674)
This ComfyUI custom node provides tools for processing equirectangular images. Currently supports converting equirectangular images to cubemap format.
2025-03-18 23:48:09 +09:00
Dr.Lt.Data d1a80cf082 update DB 2025-03-18 02:44:18 +09:00
Dr.Lt.Data fb445aa510 update DB 2025-03-18 00:59:25 +09:00
JuggernautandGitHub 4b904934ef AttributeError fix (#1672) 2025-03-18 00:49:37 +09:00
Jinwoo Park (Curt)andGitHub d6295a00e6 Update custom-node-list.json (#1671)
It works the same as [human-parser-comfyui-node](https://github.com/cozymantis/human-parser-comfyui-node), but I re-implemented InPlaceABNSync in pure Python so that it doesn't need a runtime build.

The pros and cons of this change:

pros
- The initial runtime is faster because it doesn't require a runtime build.
- No runtime error and complex setups for building the C++ code.

cons:
- After the initial execution, it could be slower than the original InPlaceABNSync.

related issue: https://github.com/cozymantis/human-parser-comfyui-node/issues/22
2025-03-18 00:35:14 +09:00
3b01673829 add comfyui-piq to custom-node-list.json (#1668)
* add comfyui-piq to 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-03-17 00:39:37 +09:00
Dr.Lt.Data a5e83a807f update DB 2025-03-17 00:37:22 +09:00
ImpactFramesandGitHub ddd766ce58 added nodes corrected id to match author name (#1669) 2025-03-17 00:35:39 +09:00
Dr.Lt.Data a6d2fd36fb update DB 2025-03-16 04:26:11 +09:00
9156d6bdba Update custom-node-list.json (#1660)
* Update custom-node-list.json

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-03-15 21:01:36 +09:00
d18a3ffeff chore(publish): update GitHub Actions workflow for node publishing (#1661)
- Add permissions for issue writing
- Update action version to v1 for publish-node-action
- Add condition to run job only for 'ltdrdata' repository owner

Co-authored-by: snomiao <snomiao+comfy-pr@gmail.com>
2025-03-15 20:56:06 +09:00
Dr.Lt.Data e933eaa2b0 fixed: Skip comfyui-frontend-package fixing when using an older version of ComfyUI.
https://github.com/ltdrdata/ComfyUI-Manager/issues/1662
2025-03-15 20:50:30 +09:00
Dr.Lt.Data 5393653ddc improved: restore_snapshot - better log message
fixed: restore_snapshot - failing channel validation

https://github.com/ltdrdata/ComfyUI-Manager/issues/1659

https://github.com/ltdrdata/ComfyUI-Manager/issues/1664
2025-03-15 20:36:50 +09:00
Dr.Lt.Data 1f3274d3f5 fixed: Removed -> str | None typing.
- Python versions below 3.10 do not support it.

https://github.com/ltdrdata/ComfyUI-Manager/issues/1663
2025-03-15 20:14:08 +09:00
Dr.Lt.Data 39eaa76b8a fixed: remove migration code completely
https://github.com/ltdrdata/ComfyUI-Manager/issues/1659
2025-03-14 18:24:30 +09:00
Dr.Lt.Data e5396713ce fixed: gitclone_install - add mode 2025-03-14 12:59:06 +09:00
Dr.Lt.Data 79943de808 fixed: install via git url - failed to install if the git url is exists in the default channel
https://github.com/ltdrdata/ComfyUI-Manager/issues/1651#issuecomment-2720408569
2025-03-14 12:53:59 +09:00
Dr.Lt.Data e05f329602 bump version to 3.31 2025-03-14 00:59:11 +09:00
Dr.Lt.Data eed0e8ebea update DB 2025-03-14 00:58:55 +09:00
731eb4fcbe Please verify my changes (#1643)
* Update custom-node-list.json

* Update custom-node-list.json

* Update custom-node-list.json

* Update custom-node-list.json

I felt the need to change the Title and the Description

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-03-14 00:53:36 +09:00
Dr.Lt.Data 44a63e4b6d update DB 2025-03-14 00:52:06 +09:00
CenFunandGitHub 7651e5e48b UI improvement (#1625) 2025-03-14 00:51:37 +09:00
Dr.Lt.Data 2449636d32 update DB 2025-03-14 00:45:47 +09:00
Dr.Lt.Data f9990ca8eb fixed: make_pip_cmd - add '-s' option 2025-03-13 22:48:13 +09:00
Dr.Lt.Data c3eed981c0 fixed: robust validation when model downloading #2 2025-03-12 21:24:31 +09:00
Dr.Lt.Data bbb54d4a08 fixed: robust validation when model downloading 2025-03-12 21:10:02 +09:00
Dr.Lt.Data 4566c585db fixed: a condition code wasn’t saved after editing... lol 2025-03-12 21:00:05 +09:00
Dr.Lt.Data a946338a18 fixed: invalid channel exception when startup 2025-03-12 17:28:17 +09:00
Dr.Lt.Data 0a60a44478 fixed: several security bugs
refactor: remove serveal unused code
2025-03-12 11:32:16 +09:00
Dr.Lt.Data cef0ad6707 update DB 2025-03-12 07:21:38 +09:00
Robin HuangandGitHub 7176f0837a Add linux form factor. (#1648) 2025-03-12 07:20:06 +09:00
Yuan-ManandGitHub 6b1f2b2d9d Add ComfyUI-StyleStudio node (#1639) 2025-03-12 07:15:58 +09:00
LaureηtandGitHub 38a1a9b320 add comfyui-finegrain to custom-node-list.json (#1587) 2025-03-12 07:04:18 +09:00
Dr.Lt.Data 402e2c384f fixed: Issue where install.py would not run when installed in cnr. 2025-03-11 12:34:07 +09:00
75 changed files with 221753 additions and 17194 deletions
View File
+58
View File
@@ -0,0 +1,58 @@
name: Publish to PyPI
on:
workflow_dispatch:
push:
branches:
- draft-v4
paths:
- "pyproject.toml"
jobs:
build-and-publish:
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'ltdrdata' || github.repository_owner == 'Comfy-Org' }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.9'
- name: Install build dependencies
run: |
python -m pip install --upgrade pip
python -m pip install build twine
- name: Get current version
id: current_version
run: |
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: ${{ 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@release/v1
with:
password: ${{ secrets.PYPI_TOKEN }}
skip-existing: true
verbose: true
+6 -2
View File
@@ -7,15 +7,19 @@ on:
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@main
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 }}
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
+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.
+98 -33
View File
@@ -5,10 +5,11 @@
![menu](https://raw.githubusercontent.com/ltdrdata/ComfyUI-extension-tutorials/refs/heads/Main/ComfyUI-Manager/images/dialog.jpg)
## NOTICE
* 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
* V3.3.2: Overhauled. Officially supports [https://comfyregistry.org/](https://comfyregistry.org/).
* V3.3.2: Overhauled. Officially supports [https://registry.comfy.org/](https://registry.comfy.org/).
* You can see whole nodes info on [ComfyUI Nodes Info](https://ltdrdata.github.io/) page.
## Installation
@@ -17,7 +18,7 @@
To install ComfyUI-Manager in addition to an existing installation of ComfyUI, you can follow the following steps:
1. goto `ComfyUI/custom_nodes` dir in terminal(cmd)
1. Go to `ComfyUI/custom_nodes` dir in terminal (cmd)
2. `git clone https://github.com/ltdrdata/ComfyUI-Manager comfyui-manager`
3. Restart ComfyUI
@@ -28,8 +29,8 @@ To install ComfyUI-Manager in addition to an existing installation of ComfyUI, y
- 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 use save as...
3. double click `install-manager-for-portable-version.bat` batch file
- 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)
@@ -47,7 +48,7 @@ pip install comfy-cli
comfy install
```
Linux/OSX:
Linux/macOS:
```commandline
python -m venv venv
. venv/bin/activate
@@ -57,13 +58,13 @@ comfy install
* See also: https://github.com/Comfy-Org/comfy-cli
### Installation[method4] (Installation for linux+venv: ComfyUI + ComfyUI-Manager)
### Installation[method4] (Installation for Linux+venv: ComfyUI + ComfyUI-Manager)
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 use save as...
- 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`
@@ -140,20 +141,27 @@ This repository provides Colab notebooks that allow you to install and use Comfy
## Paths
In `ComfyUI-Manager` V3.0 and later, configuration files and dynamically generated files are located under `<USER_DIRECTORY>/default/ComfyUI-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>.
* <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>/default/ComfyUI-Manager/config.ini`
* Configurable channel lists: `<USER_DIRECTORY>/default/ComfyUI-Manager/channels.ini`
* Configurable pip overrides: `<USER_DIRECTORY>/default/ComfyUI-Manager/pip_overrides.json`
* Configurable pip blacklist: `<USER_DIRECTORY>/default/ComfyUI-Manager/pip_blacklist.list`
* Configurable pip auto fix: `<USER_DIRECTORY>/default/ComfyUI-Manager/pip_auto_fix.list`
* Saved snapshot files: `<USER_DIRECTORY>/default/ComfyUI-Manager/snapshots`
* Startup script files: `<USER_DIRECTORY>/default/ComfyUI-Manager/startup-scripts`
* Component files: `<USER_DIRECTORY>/default/ComfyUI-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
@@ -176,7 +184,7 @@ The following settings are applied based on the section marked as `is_default`.
![model-install-dialog](https://raw.githubusercontent.com/ltdrdata/ComfyUI-extension-tutorials/Main/ComfyUI-Manager/images/snapshot.jpg)
## cm-cli: command line tools for power user
## cm-cli: command line tools for power users
* A tool is provided that allows you to use the features of ComfyUI-Manager without running ComfyUI.
* For more details, please refer to the [cm-cli documentation](docs/en/cm-cli.md).
@@ -222,7 +230,7 @@ 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>/default/ComfyUI-Manager/components`.
* `<component name>`: The name should be in the format `<prefix>::<node name>`.
* `<compnent nodeata>`: In the nodedata of the group node.
* `<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>/default/ComfyUI-Manager/components`.
@@ -240,7 +248,7 @@ The following settings are applied based on the section marked as `is_default`.
* Dragging and dropping or pasting a single component will add a node. However, when adding multiple components, nodes will not be added.
## Support of missing nodes installation
## Support for installing missing nodes
![missing-menu](https://raw.githubusercontent.com/ltdrdata/ComfyUI-extension-tutorials/Main/ComfyUI-Manager/images/missing-menu.jpg)
@@ -279,10 +287,10 @@ The following settings are applied based on the section marked as `is_default`.
* Logging to file feature
* This feature is enabled by default and can be disabled by setting `file_logging = False` in the `config.ini`.
* Fix node(recreate): When right-clicking on a node and selecting `Fix node (recreate)`, you can recreate the node. The widget's values are reset, while the connections maintain those with the same names.
* Fix node (recreate): When right-clicking on a node and selecting `Fix node (recreate)`, you can recreate the node. The widget's values are reset, while the connections maintain those with the same names.
* It is used to correct errors in nodes of old workflows created before, which are incompatible with the version changes of custom nodes.
* Double-Click Node Title: You can set the double click behavior of nodes in the ComfyUI-Manager menu.
* Double-Click Node Title: You can set the double-click behavior of nodes in the ComfyUI-Manager menu.
* `Copy All Connections`, `Copy Input Connections`: Double-clicking a node copies the connections of the nearest node.
* This action targets the nearest node within a straight-line distance of 1000 pixels from the center of the node.
* In the case of `Copy All Connections`, it duplicates existing outputs, but since it does not allow duplicate connections, the existing output connections of the original node are disconnected.
@@ -314,9 +322,6 @@ The following settings are applied based on the section marked as `is_default`.
* Use `aria2` as downloader
* [howto](docs/en/use_aria2.md)
* If you add the item `skip_migration_check = True` to `config.ini`, it will not check whether there are nodes that can be migrated at startup.
* This option can be used if performance issues occur in a Colab+GDrive environment.
## Environment Variables
@@ -351,7 +356,7 @@ When you run the `scan.sh` script:
* It updates the `github-stats.json`.
* This uses the GitHub API, so set your token with `export GITHUB_TOKEN=your_token_here` to avoid quickly reaching the rate limit and malfunctioning.
* To skip this step, add the `--skip-update-stat` option.
* To skip this step, add the `--skip-stat-update` option.
* The `--skip-all` option applies both `--skip-update` and `--skip-stat-update`.
@@ -359,9 +364,9 @@ 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>/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
* 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`
* if `SSL: CERTIFICATE_VERIFY_FAILED` error is occured.
* If the `SSL: CERTIFICATE_VERIFY_FAILED` error occurs.
* Edit `config.ini` file: add `bypass_ssl = True`
@@ -379,19 +384,79 @@ When you run the `scan.sh` script:
* all feature is available
* `high` level risky features
* `Install via git url`, `pip install`
* Installation of custom nodes registered not in the `default channel`.
* Fix custom nodes
* 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
### Dedicated install flags: `allow_git_url_install` / `allow_pip_install`
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`)
Key properties:
* **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`:
```ini
[default]
allow_git_url_install = true
allow_pip_install = true
```
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
+4
View File
@@ -1,3 +1,7 @@
"""
This file is the entry point for the ComfyUI-Manager package, handling CLI-only mode and initial setup.
"""
import os
import sys
+2 -2
View File
@@ -37,7 +37,7 @@ find ~/.tmp/default -name "*.py" -print0 | xargs -0 grep -E "crypto|^_A="
echo
echo CHECK3
find ~/.tmp/default -name "requirements.txt" | xargs grep "^\s*https\\?:"
find ~/.tmp/default -name "requirements.txt" | xargs grep "\.whl"
find ~/.tmp/default -name "requirements.txt" | xargs grep "^\s*[^#]*https\?:"
find ~/.tmp/default -name "requirements.txt" | xargs grep "^\s*[^#].*\.whl"
echo
+28 -33
View File
@@ -43,9 +43,10 @@ import cnr_utils
comfyui_manager_path = os.path.abspath(os.path.dirname(__file__))
cm_global.pip_blacklist = {'torch', 'torchsde', 'torchvision'}
cm_global.pip_downgrade_blacklist = ['torch', 'torchsde', 'torchvision', 'transformers', 'safetensors', 'kornia']
cm_global.pip_overrides = {'numpy': 'numpy<2'}
cm_global.pip_blacklist = {'torch', 'torchaudio', 'torchsde', 'torchvision'}
cm_global.pip_downgrade_blacklist = ['torch', 'torchaudio', 'torchsde', 'torchvision', 'transformers', 'safetensors', 'kornia']
cm_global.pip_overrides = {}
if os.path.exists(os.path.join(manager_util.comfyui_manager_path, "pip_overrides.json")):
with open(os.path.join(manager_util.comfyui_manager_path, "pip_overrides.json"), 'r', encoding="UTF-8", errors="ignore") as json_file:
@@ -147,7 +148,6 @@ class Ctx:
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)
cm_global.pip_overrides = {'numpy': 'numpy<2'}
if os.path.exists(core.manager_pip_blacklist_path):
with open(core.manager_pip_blacklist_path, 'r', encoding="UTF-8", errors="ignore") as f:
@@ -184,13 +184,18 @@ class Ctx:
cmd_ctx = Ctx()
def install_node(node_spec_str, is_all=False, cnt_msg=''):
def install_node(node_spec_str, is_all=False, cnt_msg='', **kwargs):
exit_on_fail = kwargs.get('exit_on_fail', False)
print(f"install_node exit on fail:{exit_on_fail}...")
if core.is_valid_url(node_spec_str):
# install via urls
res = asyncio.run(core.gitclone_install(node_spec_str, no_deps=cmd_ctx.no_deps))
if not res.result:
print(res.msg)
print(f"[bold red]ERROR: An error occurred while installing '{node_spec_str}'.[/bold red]")
if exit_on_fail:
sys.exit(1)
else:
print(f"{cnt_msg} [INSTALLED] {node_spec_str:50}")
else:
@@ -225,6 +230,8 @@ def install_node(node_spec_str, is_all=False, cnt_msg=''):
print("")
else:
print(f"[bold red]ERROR: An error occurred while installing '{node_name}'.\n{res.msg}[/bold red]")
if exit_on_fail:
sys.exit(1)
def reinstall_node(node_spec_str, is_all=False, cnt_msg=''):
@@ -586,7 +593,7 @@ def get_all_installed_node_specs():
return res
def for_each_nodes(nodes, act, allow_all=True):
def for_each_nodes(nodes, act, allow_all=True, **kwargs):
is_all = False
if allow_all and 'all' in nodes:
is_all = True
@@ -598,7 +605,7 @@ def for_each_nodes(nodes, act, allow_all=True):
i = 1
for x in nodes:
try:
act(x, is_all=is_all, cnt_msg=f'{i}/{total}')
act(x, is_all=is_all, cnt_msg=f'{i}/{total}', **kwargs)
except Exception as e:
print(f"ERROR: {e}")
traceback.print_exc()
@@ -642,13 +649,17 @@ def install(
None,
help="user directory"
),
exit_on_fail: bool = typer.Option(
False,
help="Exit on failure"
)
):
cmd_ctx.set_user_directory(user_directory)
cmd_ctx.set_channel_mode(channel, mode)
cmd_ctx.set_no_deps(no_deps)
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, core.manager_files_path)
for_each_nodes(nodes, act=install_node)
for_each_nodes(nodes, act=install_node, exit_on_fail=exit_on_fail)
pip_fixer.fix_broken()
@@ -1047,18 +1058,16 @@ def save_snapshot(
):
cmd_ctx.set_user_directory(user_directory)
if output is None:
print("[bold red]ERROR: missing output path[/bold red]")
raise typer.Exit(code=1)
if(not output.endswith('.json') and not output.endswith('.yaml')):
print("[bold red]ERROR: output path should be either '.json' or '.yaml' file.[/bold red]")
raise typer.Exit(code=1)
if output is not None:
if(not output.endswith('.json') and not output.endswith('.yaml')):
print("[bold red]ERROR: output path should be either '.json' or '.yaml' file.[/bold red]")
raise typer.Exit(code=1)
dir_path = os.path.dirname(output)
if(dir_path != '' and not os.path.exists(dir_path)):
print(f"[bold red]ERROR: {output} path not exists.[/bold red]")
raise typer.Exit(code=1)
dir_path = os.path.dirname(output)
if(dir_path != '' and not os.path.exists(dir_path)):
print(f"[bold red]ERROR: {output} path not exists.[/bold red]")
raise typer.Exit(code=1)
path = asyncio.run(core.save_snapshot_with_postfix('snapshot', output, not full_snapshot))
print(f"Current snapshot is saved as `{path}`")
@@ -1271,20 +1280,6 @@ def export_custom_node_ids(
print(f"{x['id']}@unknown", file=output_file)
@app.command(
"migrate",
help="Migrate legacy node system to new node system",
)
def migrate(
user_directory: str = typer.Option(
None,
help="user directory"
)
):
cmd_ctx.set_user_directory(user_directory)
asyncio.run(unified_manager.migrate_unmanaged_nodes())
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(app())
Executable → Regular
+40026 -1642
View File
File diff suppressed because it is too large Load Diff
+41
View File
@@ -0,0 +1,41 @@
# ComfyUI-Manager: Documentation
This directory contains documentation for the ComfyUI-Manager, providing guides and tutorials for users in multiple languages.
## Directory Structure
The documentation is organized into language-specific directories:
- **en/**: English documentation
- **ko/**: Korean documentation
## Core Documentation Files
### Command-Line Interface
- **cm-cli.md**: Documentation for the ComfyUI-Manager Command Line Interface (CLI), which allows using manager functionality without the UI.
### Advanced Features
- **use_aria2.md**: Guide for using the aria2 download accelerator with ComfyUI-Manager for faster model downloads.
## Documentation Standards
The documentation follows these standards:
1. **Markdown Format**: All documentation is written in Markdown for easy rendering on GitHub and other platforms
2. **Language-specific Directories**: Content is separated by language to facilitate localization
3. **Feature-focused Documentation**: Each major feature has its own documentation file
4. **Updated with Releases**: Documentation is kept in sync with software releases
## Contributing to Documentation
When contributing new documentation:
1. Place files in the appropriate language directory
2. Use clear, concise language appropriate for the target audience
3. Include examples where helpful
4. Consider adding screenshots or diagrams for complex features
5. Maintain consistent formatting with existing documentation
This documentation directory will continue to grow to support the expanding feature set of ComfyUI-Manager.
+2 -2
View File
@@ -139,9 +139,9 @@ You can set whether to use ComfyUI-Manager solely via CLI.
`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 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.
### 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.
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
+13 -13
View File
@@ -23,13 +23,13 @@ OPTIONS:
## How To Use?
* `python cm-cli.py` 를 통해서 실행 시킬 수 있습니다.
* 예를 들어 custom node를 모두 업데이트 하고 싶다면
* ComfyUI-Manager경로 에서 `python cm-cli.py update all` 를 command를 실행할 수 있습니다.
* 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 파일이 있는 경로인 경우, 다음과 같은 방식으로 코맨드를 실행해야 합니다.
* portable 버전을 사용할 경우 run_nvidia_gpu.bat 파일이 있는 경로인 경우, 다음과 같은 방식으로 명령을 실행해야 합니다.
`.\python_embeded\python.exe ComfyUI\custom_nodes\ComfyUI-Manager\cm-cli.py update all`
* ComfyUI 의 경로는 COMFYUI_PATH 환경 변수로 설정할 수 있습니다. 만약 생략할 경우 다음과 같은 경고 메시지가 나타나며, ComfyUI-Manager가 설치된 경로를 기준으로 상대 경로로 설정됩니다.
```
@@ -40,8 +40,8 @@ OPTIONS:
### 1. --channel, --mode
* 정보 보기 기능과 커스텀 노드 관리 기능의 경우는 --channel과 --mode를 통해 정보 DB를 설정할 수 있습니다.
* 예 들어 `python cm-cli.py update all --channel recent --mode remote`와 같은 command를 실행할 경우, 현재 ComfyUI-Manager repo에 내장된 로컬의 정보가 아닌 remote의 최신 정보를 기준으로 동작하며, recent channel에 있는 목록을 대상으로만 동작합니다.
* --channel, --mode 는 `simple-show, show, install, uninstall, update, disable, enable, fix` command에서만 사용 가능합니다.
* 예 들어 `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. 관리 정보 보기
@@ -51,7 +51,7 @@ OPTIONS:
* `[show|simple-show]` - `show`는 상세하게 정보를 보여주며, `simple-show`는 간단하게 정보를 보여줍니다.
`python cm-cli.py show installed` 와 같은 코맨드를 실행하면 설치된 커스텀 노드의 정보를 상세하게 보여줍니다.
`python cm-cli.py show installed` 와 같은 명령을 실행하면 설치된 커스텀 노드의 정보를 상세하게 보여줍니다.
```
-= ComfyUI-Manager CLI (V2.24) =-
@@ -67,7 +67,7 @@ FETCH DATA from: https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main
[ DISABLED ] ComfyUI-Loopchain (author: Fannovel16)
```
`python cm-cli.py simple-show installed` 와 같은 코맨드를 이용해서 설치된 커스텀 노드의 정보를 간단하게 보여줍니다.
`python cm-cli.py simple-show installed` 와 같은 명령을 이용해서 설치된 커스텀 노드의 정보를 간단하게 보여줍니다.
```
-= ComfyUI-Manager CLI (V2.24) =-
@@ -89,7 +89,7 @@ ComfyUI-Loopchain
* `installed`: enable, disable 여부와 상관없이 설치된 모든 노드를 보여줍니다
* `not-installed`: 설치되지 않은 커스텀 노드의 목록을 보여줍니다.
* `all`: 모든 커스텀 노드의 목록을 보여줍니다.
* `snapshot`: 현재 설치된 커스텀 노드의 snapshot 정보를 보여줍니다. `show` 통해서 볼 경우는 json 출력 형태로 보여주며, `simple-show`를 통해서 볼 경우는 간단하게, 커밋 해시와 함께 보여줍니다.
* `snapshot`: 현재 설치된 커스텀 노드의 snapshot 정보를 보여줍니다. `show` 통해서 볼 경우는 json 출력 형태로 보여주며, `simple-show`를 통해서 볼 경우는 간단하게, 커밋 해시와 함께 보여줍니다.
* `snapshot-list`: ComfyUI-Manager/snapshots 에 저장된 snapshot 파일의 목록을 보여줍니다.
### 3. 커스텀 노드 관리 하기
@@ -98,7 +98,7 @@ ComfyUI-Loopchain
* `python cm-cli.py install ComfyUI-Impact-Pack ComfyUI-Inspire-Pack ComfyUI_experiments` 와 같이 커스텀 노드의 이름을 나열해서 관리 기능을 적용할 수 있습니다.
* 커스텀 노드의 이름은 `show`를 했을 때 보여주는 이름이며, git repository의 이름입니다.
(추후 nickname 을 사용가능하돌고 업데이트 할 예정입니다.)
(추후 nickname을 사용 가능하도록 업데이트할 예정입니다.)
`[update|disable|enable|fix] all ?[--channel <channel name>] ?[--mode [remote|local|cache]]`
@@ -124,7 +124,7 @@ ComfyUI-Loopchain
* `--pip-non-local-url`: web URL에 등록된 pip 패키지들에 대해서 복구를 수행
* `--pip-local-url`: local 경로를 지정하고 있는 pip 패키지들에 대해서 복구를 수행
* `--user-directory`: 사용자 디렉토리 설정
* `--restore-to`: 복구될 커스텀 노드가 설치될 경로. (이 옵션을 적용할 경우 오직 대상 경로에 설치된 custom nodes 만 설치된 것으로 인식함.)
* `--restore-to`: 복구될 커스텀 노드가 설치될 경로. (이 옵션을 적용할 경우 오직 대상 경로에 설치된 custom nodes만 설치된 것으로 인식함.)
### 5. CLI only mode
@@ -133,7 +133,7 @@ ComfyUI-Manager를 CLI로만 사용할 것인지를 설정할 수 있습니다.
`cli-only-mode [enable|disable]`
* security 혹은 policy 의 이유로 GUI 를 통한 ComfyUI-Manager 사용을 제한하고 싶은 경우 이 모드를 사용할 수 있습니다.
* CLI only mode를 적용할 경우 ComfyUI-Manager 가 매우 제한된 상태로 로드되어, 내부적으로 제공하는 web API가 비활성화 되며, 메인 메뉴에서도 Manager 버튼이 표시되지 않습니다.
* CLI only mode를 적용할 경우 ComfyUI-Manager 가 매우 제한된 상태로 로드되어, 내부적으로 제공하는 web API가 비활성화되며, 메인 메뉴에서도 Manager 버튼이 표시되지 않습니다.
### 6. 의존성 설치
@@ -141,10 +141,10 @@ ComfyUI-Manager를 CLI로만 사용할 것인지를 설정할 수 있습니다.
`restore-dependencies`
* `ComfyUI/custom_nodes` 하위 경로에 커스텀 노드들이 설치되어 있긴 하지만, 의존성이 설치되지 않은 경우 사용할 수 있습니다.
* colab 과 같이 cloud instance를 새로 시작하는 경우 의존성 재설치 및 설치 스크립트가 재실행 되어야 하는 경우 사용합니다.
* ComfyUI 재설치할 경우, custom_nodes 경로만 백업했다가 재설치 할 경우 활용 가능합니다.
* Colab과 같이 cloud instance를 새로 시작하는 경우 의존성 재설치 및 설치 스크립트가 재실행되어야 하는 경우 사용합니다.
* ComfyUI 재설치할 경우, custom_nodes 경로만 백업했다가 재설치할 경우 활용 가능합니다.
### 7. clear
GUI에서 install, update를 하거나 snapshot 을 restore하는 경우 예약을 통해서 다음번 ComfyUI를 실행할 경우 실행되는 구조입니다. `clear` 는 이런 예약 상태를 clear해서, 아무런 사전 실행이 적용되지 않도록 합니다.
GUI에서 install, update를 하거나 snapshot을 restore하는 경우 예약을 통해서 다음번 ComfyUI를 실행할 경우 실행되는 구조입니다. `clear` 는 이런 예약 상태를 clear해서, 아무런 사전 실행이 적용되지 않도록 합니다.
+53207 -2660
View File
File diff suppressed because it is too large Load Diff
+9 -1
View File
@@ -2,6 +2,7 @@ import subprocess
import sys
import os
import traceback
import time
import git
import json
@@ -219,7 +220,14 @@ def gitpull(path):
repo.close()
return
remote.pull()
try:
repo.git.pull('--ff-only')
except git.GitCommandError:
backup_name = f'backup_{time.strftime("%Y%m%d_%H%M%S")}'
repo.create_head(backup_name)
print(f"[ComfyUI-Manager] Cannot fast-forward. Backup created: {backup_name}")
repo.git.reset('--hard', f'{remote_name}/{branch_name}')
print(f"[ComfyUI-Manager] Reset to {remote_name}/{branch_name}")
repo.git.submodule('update', '--init', '--recursive')
new_commit_hash = repo.head.commit.hexsha
+23694 -4779
View File
File diff suppressed because it is too large Load Diff
+53
View File
@@ -0,0 +1,53 @@
# 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_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
The backend follows a modular design pattern with clear separation of concerns:
1. **Core Layer**: Manager modules provide the primary API and business logic
2. **Utility Layer**: Helper modules provide specialized functionality
3. **Integration Layer**: Modules that connect to external systems
## Security Model
The system implements a comprehensive security framework with multiple levels:
- **Block**: Highest security - blocks most remote operations
- **High**: Allows only specific trusted operations
- **Middle**: Standard security for most users
- **Normal-**: More permissive for advanced users
- **Weak**: Lowest security for development environments
## Implementation Details
- The backend is designed to work seamlessly with ComfyUI
- Asynchronous task queuing is implemented for background operations
- The system supports multiple installation modes
- Error handling and risk assessment are integrated throughout the codebase
## API Integration
The backend exposes a REST API via `manager_server.py` that enables:
- Custom node management (install, update, disable, remove)
- Model downloading and organization
- System configuration
- Snapshot management
- Workflow component handling
+5 -2
View File
@@ -42,6 +42,7 @@ async def _get_cnr_data(cache_mode=True, dont_wait=True):
system = platform.system().lower()
is_windows = system == 'windows'
is_mac = system == 'darwin'
is_linux = system == 'linux'
# Get ComfyUI version tag
if is_desktop:
@@ -62,6 +63,8 @@ async def _get_cnr_data(cache_mode=True, dont_wait=True):
form_factor = 'git-windows'
elif is_mac:
form_factor = 'git-mac'
elif is_linux:
form_factor = 'git-linux'
else:
form_factor = 'other'
@@ -176,7 +179,7 @@ def install_node(node_id, version=None):
else:
url = f"{base_url}/nodes/{node_id}/install?version={version}"
response = requests.get(url)
response = requests.get(url, verify=not manager_util.bypass_ssl)
if response.status_code == 200:
# Convert the API response to a NodeVersion object
return map_node_version(response.json())
@@ -187,7 +190,7 @@ def install_node(node_id, version=None):
def all_versions_of_node(node_id):
url = f"{base_url}/nodes/{node_id}/versions?statuses=NodeVersionStatusActive&statuses=NodeVersionStatusPending"
response = requests.get(url)
response = requests.get(url, verify=not manager_util.bypass_ssl)
if response.status_code == 200:
return response.json()
else:
+2
View File
@@ -46,6 +46,8 @@ def git_url(fullpath):
for k, v in config.items():
if k.startswith('remote ') and 'url' in v:
if 'Comfy-Org/ComfyUI-Manager' in v['url']:
return "https://github.com/ltdrdata/ComfyUI-Manager"
return v['url']
return None
+347 -214
View File
@@ -40,10 +40,11 @@ import cnr_utils
import manager_util
import git_utils
import manager_downloader
import manager_migration
from node_package import InstalledNodePackage
version_code = [3, 30, 3]
version_code = [3, 41]
version_str = f"V{version_code[0]}.{version_code[1]}" + (f'.{version_code[2]}' if len(version_code) > 2 else '')
@@ -53,6 +54,11 @@ DEFAULT_CHANNEL = "https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/ma
default_custom_nodes_path = None
class InvalidChannel(Exception):
def __init__(self, channel):
self.channel = channel
super().__init__(channel)
def get_default_custom_nodes_path():
global default_custom_nodes_path
if default_custom_nodes_path is None:
@@ -209,9 +215,10 @@ def update_user_directory(user_dir):
global manager_pip_blacklist_path
global manager_components_path
manager_files_path = os.path.abspath(os.path.join(user_dir, 'default', 'ComfyUI-Manager'))
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):
@@ -251,6 +258,7 @@ comfy_ui_revision = "Unknown"
comfy_ui_commit_datetime = datetime(1900, 1, 1, 0, 0, 0)
channel_dict = None
valid_channels = {'default', 'local'}
channel_list = None
@@ -355,7 +363,7 @@ def normalize_channel(channel):
if channel_url:
return channel_url
raise Exception(f"Invalid channel name '{channel}'")
raise InvalidChannel(channel)
class ManagedResult:
@@ -394,18 +402,86 @@ class ManagedResult:
return self
class NormalizedKeyDict:
def __init__(self):
self._store = {}
self._key_map = {}
def _normalize_key(self, key):
if isinstance(key, str):
return key.strip().lower()
return key
def __setitem__(self, key, value):
norm_key = self._normalize_key(key)
self._key_map[norm_key] = key
self._store[key] = value
def __getitem__(self, key):
norm_key = self._normalize_key(key)
original_key = self._key_map[norm_key]
return self._store[original_key]
def __delitem__(self, key):
norm_key = self._normalize_key(key)
original_key = self._key_map.pop(norm_key)
del self._store[original_key]
def __contains__(self, key):
return self._normalize_key(key) in self._key_map
def get(self, key, default=None):
return self[key] if key in self else default
def setdefault(self, key, default=None):
if key in self:
return self[key]
self[key] = default
return default
def pop(self, key, default=None):
if key in self:
val = self[key]
del self[key]
return val
if default is not None:
return default
raise KeyError(key)
def keys(self):
return self._store.keys()
def values(self):
return self._store.values()
def items(self):
return self._store.items()
def __iter__(self):
return iter(self._store)
def __len__(self):
return len(self._store)
def __repr__(self):
return repr(self._store)
def to_dict(self):
return dict(self._store)
class UnifiedManager:
def __init__(self):
self.installed_node_packages: dict[str, InstalledNodePackage] = {}
self.cnr_inactive_nodes = {} # node_id -> node_version -> fullpath
self.nightly_inactive_nodes = {} # node_id -> fullpath
self.unknown_inactive_nodes = {} # node_id -> repo url * fullpath
self.active_nodes = {} # node_id -> node_version * fullpath
self.unknown_active_nodes = {} # node_id -> repo url * fullpath
self.cnr_map = {} # node_id -> cnr info
self.repo_cnr_map = {} # repo_url -> cnr info
self.custom_node_map_cache = {} # (channel, mode) -> augmented custom node list json
self.cnr_inactive_nodes = NormalizedKeyDict() # node_id -> node_version -> fullpath
self.nightly_inactive_nodes = NormalizedKeyDict() # node_id -> fullpath
self.unknown_inactive_nodes = {} # node_id -> repo url * fullpath
self.active_nodes = NormalizedKeyDict() # node_id -> node_version * fullpath
self.unknown_active_nodes = {} # node_id -> repo url * fullpath
self.cnr_map = NormalizedKeyDict() # node_id -> cnr info
self.repo_cnr_map = {} # repo_url -> cnr info
self.custom_node_map_cache = {} # (channel, mode) -> augmented custom node list json
self.processed_install = set()
def get_module_name(self, x):
@@ -762,6 +838,9 @@ class UnifiedManager:
@staticmethod
async def load_nightly(channel, mode):
if channel is None:
return {}
res = {}
channel_url = normalize_channel(channel)
@@ -770,6 +849,11 @@ class UnifiedManager:
print(f"[bold red]ERROR: Invalid mode is specified `--mode {mode}`[/bold red]", file=sys.stderr)
return {}
# validate channel - only the channel set by the user is allowed.
if channel_url not in valid_channels:
logging.error(f'[ComfyUI-Manager] An invalid channel was used: {channel_url}')
raise InvalidChannel(channel_url)
json_obj = await get_data_by_mode(mode, 'custom-node-list.json', channel_url=channel_url)
for x in json_obj['custom_nodes']:
try:
@@ -787,8 +871,9 @@ class UnifiedManager:
return res
async def get_custom_nodes(self, channel, mode):
# default_channel = normalize_channel('default')
# cache = self.custom_node_map_cache.get((default_channel, mode)) # CNR/nightly should always be based on the default channel.
if channel is None and mode is None:
channel = 'default'
mode = 'cache'
channel = normalize_channel(channel)
cache = self.custom_node_map_cache.get((channel, mode)) # CNR/nightly should always be based on the default channel.
@@ -797,10 +882,9 @@ class UnifiedManager:
return cache
channel = normalize_channel(channel)
print(f"nightly_channel: {channel}/{mode}")
nodes = await self.load_nightly(channel, mode)
res = {}
res = NormalizedKeyDict()
added_cnr = set()
for v in nodes.values():
v = v[0]
@@ -841,6 +925,7 @@ class UnifiedManager:
install_script_path = os.path.join(repo_path, "install.py")
requirements_path = os.path.join(repo_path, "requirements.txt")
res = True
if lazy_mode:
install_cmd = ["#LAZY-INSTALL-SCRIPT", sys.executable]
return try_install_script(url, repo_path, install_cmd)
@@ -848,26 +933,25 @@ class UnifiedManager:
if os.path.exists(requirements_path) and not no_deps:
print("Install: pip packages")
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, manager_files_path)
res = True
lines = manager_util.robust_readlines(requirements_path)
for line in lines:
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()
return res
if os.path.exists(install_script_path) and install_script_path not in self.processed_install:
self.processed_install.add(install_script_path)
print("Install: install script")
install_cmd = [sys.executable, "install.py"]
return try_install_script(url, repo_path, install_cmd, instant_execution=instant_execution)
return res and try_install_script(url, repo_path, install_cmd, instant_execution=instant_execution)
return True
return res
def reserve_cnr_switch(self, target, zip_url, from_path, to_path, no_deps):
script_path = os.path.join(manager_startup_script_path, "install-scripts.txt")
@@ -879,14 +963,6 @@ class UnifiedManager:
return True
def reserve_migration(self, moves):
script_path = os.path.join(manager_startup_script_path, "install-scripts.txt")
with open(script_path, "a") as file:
obj = ["", "#LAZY-MIGRATION", moves]
file.write(f"{obj}\n")
return True
def unified_fix(self, node_id, version_spec, instant_execution=False, no_deps=False):
"""
fix dependencies
@@ -1315,67 +1391,66 @@ class UnifiedManager:
return result.fail(f'Path not found: {repo_path}')
# version check
repo = git.Repo(repo_path)
with git.Repo(repo_path) as repo:
if repo.head.is_detached:
if not switch_to_default_branch(repo):
return result.fail(f"Failed to switch to default branch: {repo_path}")
if repo.head.is_detached:
if not switch_to_default_branch(repo):
return result.fail(f"Failed to switch to default branch: {repo_path}")
current_branch = repo.active_branch
branch_name = current_branch.name
current_branch = repo.active_branch
branch_name = current_branch.name
if current_branch.tracking_branch() is None:
print(f"[ComfyUI-Manager] There is no tracking branch ({current_branch})")
remote_name = get_remote_name(repo)
else:
remote_name = current_branch.tracking_branch().remote_name
if remote_name is None:
return result.fail(f"Failed to get remote when installing: {repo_path}")
remote = repo.remote(name=remote_name)
try:
remote.fetch()
except Exception as e:
if 'detected dubious' in str(e):
print(f"[ComfyUI-Manager] Try fixing 'dubious repository' error on '{repo_path}' repository")
safedir_path = repo_path.replace('\\', '/')
subprocess.run(['git', 'config', '--global', '--add', 'safe.directory', safedir_path])
try:
remote.fetch()
except Exception:
print("\n[ComfyUI-Manager] Failed to fixing repository setup. Please execute this command on cmd: \n"
"-----------------------------------------------------------------------------------------\n"
f'git config --global --add safe.directory "{safedir_path}"\n'
"-----------------------------------------------------------------------------------------\n")
commit_hash = repo.head.commit.hexsha
if f'{remote_name}/{branch_name}' in repo.refs:
remote_commit_hash = repo.refs[f'{remote_name}/{branch_name}'].object.hexsha
else:
return result.fail(f"Not updatable branch: {branch_name}")
if commit_hash != remote_commit_hash:
git_pull(repo_path)
if len(repo.remotes) > 0:
url = repo.remotes[0].url
if current_branch.tracking_branch() is None:
print(f"[ComfyUI-Manager] There is no tracking branch ({current_branch})")
remote_name = get_remote_name(repo)
else:
url = "unknown repo"
remote_name = current_branch.tracking_branch().remote_name
def postinstall():
return self.execute_install_script(url, repo_path, instant_execution=instant_execution, no_deps=no_deps)
if remote_name is None:
return result.fail(f"Failed to get remote when installing: {repo_path}")
if return_postinstall:
return result.with_postinstall(postinstall)
remote = repo.remote(name=remote_name)
try:
remote.fetch()
except Exception as e:
if 'detected dubious' in str(e):
print(f"[ComfyUI-Manager] Try fixing 'dubious repository' error on '{repo_path}' repository")
safedir_path = repo_path.replace('\\', '/')
subprocess.run(['git', 'config', '--global', '--add', 'safe.directory', safedir_path])
try:
remote.fetch()
except Exception:
print("\n[ComfyUI-Manager] Failed to fixing repository setup. Please execute this command on cmd: \n"
"-----------------------------------------------------------------------------------------\n"
f'git config --global --add safe.directory "{safedir_path}"\n'
"-----------------------------------------------------------------------------------------\n")
commit_hash = repo.head.commit.hexsha
if f'{remote_name}/{branch_name}' in repo.refs:
remote_commit_hash = repo.refs[f'{remote_name}/{branch_name}'].object.hexsha
else:
if not postinstall():
return result.fail(f"Failed to execute install script: {url}")
return result.fail(f"Not updatable branch: {branch_name}")
return result
else:
return ManagedResult('skip').with_msg('Up to date')
if commit_hash != remote_commit_hash:
git_pull(repo_path)
if len(repo.remotes) > 0:
url = repo.remotes[0].url
else:
url = "unknown repo"
def postinstall():
return self.execute_install_script(url, repo_path, instant_execution=instant_execution, no_deps=no_deps)
if return_postinstall:
return result.with_postinstall(postinstall)
else:
if not postinstall():
return result.fail(f"Failed to execute install script: {url}")
return result
else:
return ManagedResult('skip').with_msg('Up to date')
def unified_update(self, node_id, version_spec=None, instant_execution=False, no_deps=False, return_postinstall=False):
orig_print(f"\x1b[2K\rUpdating: {node_id}", end='')
@@ -1411,11 +1486,16 @@ class UnifiedManager:
return ManagedResult('skip')
elif self.is_disabled(node_id):
return self.unified_enable(node_id)
else:
version_spec = self.resolve_unspecified_version(node_id)
if version_spec == 'unknown' or version_spec == 'nightly':
custom_nodes = await self.get_custom_nodes(channel, mode)
try:
custom_nodes = await self.get_custom_nodes(channel, mode)
except InvalidChannel as e:
return ManagedResult('fail').fail(f'Invalid channel is used: {e.channel}')
the_node = custom_nodes.get(node_id)
if the_node is not None:
if version_spec == 'unknown':
@@ -1473,28 +1553,6 @@ class UnifiedManager:
return res
async def migrate_unmanaged_nodes(self):
"""
fix path for nightly and unknown nodes of unmanaged nodes
"""
await self.reload('cache')
await self.get_custom_nodes('default', 'cache')
print("Migration: STAGE 1")
moves = []
# migrate nightly inactive
for x, v in self.nightly_inactive_nodes.items():
if v.endswith('@nightly'):
continue
new_path = os.path.join(get_default_custom_nodes_path(), '.disabled', f"{x}@nightly")
moves.append((v, new_path))
self.reserve_migration(moves)
print("DONE (Migration reserved)")
unified_manager = UnifiedManager()
@@ -1564,8 +1622,14 @@ def get_installed_node_packs():
return res
def refresh_channel_dict():
if channel_dict is None:
get_channel_dict()
def get_channel_dict():
global channel_dict
global valid_channels
if channel_dict is None:
channel_dict = {}
@@ -1579,6 +1643,7 @@ def get_channel_dict():
channel_info = x.split("::")
if len(channel_info) == 2:
channel_dict[channel_info[0]] = channel_info[1]
valid_channels.add(channel_info[1])
return channel_dict
@@ -1631,12 +1696,18 @@ def write_config():
'model_download_by_agent': get_config()['model_download_by_agent'],
'downgrade_blacklist': get_config()['downgrade_blacklist'],
'security_level': get_config()['security_level'],
'skip_migration_check': get_config()['skip_migration_check'],
'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
for key, value in config['default'].items():
if isinstance(value, str):
config['default'][key] = value.replace('\r', '').replace('\n', '').replace('\x00', '')
directory = os.path.dirname(manager_config_path)
if not os.path.exists(directory):
os.makedirs(directory)
@@ -1650,12 +1721,14 @@ def read_config():
config = configparser.ConfigParser(strict=False)
config.read(manager_config_path)
default_conf = config['default']
manager_util.use_uv = default_conf['use_uv'].lower() == 'true' if 'use_uv' in default_conf else False
def get_bool(key, default_value):
return default_conf[key].lower() == 'true' if key in default_conf else False
return {
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)
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', ''),
@@ -1670,36 +1743,46 @@ def read_config():
'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(),
'skip_migration_check': get_bool('skip_migration_check', False),
'always_lazy_install': get_bool('always_lazy_install', False),
'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
return {
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
result = {
'http_channel_enabled': False,
'preview_method': manager_funcs.get_current_preview_method(),
'git_exe': '',
'use_uv': False,
'use_uv': manager_util.use_uv,
'channel_url': DEFAULT_CHANNEL,
'default_cache_as_channel_url': False,
'share_option': 'all',
'bypass_ssl': False,
'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': '',
'skip_migration_check': False,
'always_lazy_install': False,
'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():
@@ -2082,6 +2165,13 @@ def is_valid_url(url):
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')
@@ -2099,8 +2189,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')
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
@@ -2133,6 +2226,10 @@ async def gitclone_install(url, instant_execution=False, msg_prefix='', no_deps=
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()
@@ -2167,9 +2264,17 @@ def git_pull(path):
current_branch = repo.active_branch
remote_name = current_branch.tracking_branch().remote_name
remote = repo.remote(name=remote_name)
remote.pull()
try:
repo.git.pull('--ff-only')
except git.GitCommandError:
branch_name = current_branch.name
backup_name = f'backup_{time.strftime("%Y%m%d_%H%M%S")}'
repo.create_head(backup_name)
logging.info(f"[ComfyUI-Manager] Cannot fast-forward. Backup created: {backup_name}")
repo.git.reset('--hard', f'{remote_name}/{branch_name}')
logging.info(f"[ComfyUI-Manager] Reset to {remote_name}/{branch_name}")
repo.git.submodule('update', '--init', '--recursive')
repo.close()
@@ -2437,22 +2542,23 @@ def update_to_stable_comfyui(repo_path):
logging.error('\t'+branch.name)
return "fail", None
versions, current_tag, _ = get_comfyui_versions(repo)
if len(versions) == 0 or (len(versions) == 1 and versions[0] == 'nightly'):
versions, current_tag, latest_tag = get_comfyui_versions(repo)
if latest_tag is None:
logging.info("[ComfyUI-Manager] Unable to update to the stable ComfyUI version.")
return "fail", None
if versions[0] == 'nightly':
latest_tag = versions[1]
else:
latest_tag = versions[0]
if current_tag == latest_tag:
tag_ref = next((t for t in repo.tags if t.name == latest_tag), None)
if tag_ref is None:
logging.info(f"[ComfyUI-Manager] Unable to locate tag '{latest_tag}' in repository.")
return "fail", None
if repo.head.commit == tag_ref.commit:
return "skip", None
else:
logging.info(f"[ComfyUI-Manager] Updating ComfyUI: {current_tag} -> {latest_tag}")
repo.git.checkout(latest_tag)
repo.git.checkout(tag_ref.name)
execute_install_script("ComfyUI", repo_path, instant_execution=False, no_deps=False)
return 'updated', latest_tag
except:
traceback.print_exc()
@@ -2584,9 +2690,13 @@ def check_state_of_git_node_pack_single(item, do_fetch=False, do_update_check=Tr
def get_installed_pip_packages():
# extract pip package infos
cmd = manager_util.make_pip_cmd(['freeze'])
pips = subprocess.check_output(cmd, text=True).split('\n')
try:
# extract pip package infos
cmd = manager_util.make_pip_cmd(['freeze'])
pips = subprocess.check_output(cmd, text=True).split('\n')
except Exception as e:
logging.warning("[ComfyUI-Manager] Could not enumerate pip packages for snapshot: %s", e)
return {}
res = {}
for x in pips:
@@ -2649,22 +2759,8 @@ async def get_current_snapshot(custom_nodes_only = False):
cnr_custom_nodes[info['id']] = info['ver']
else:
repo = git.Repo(fullpath)
if repo.head.is_detached:
remote_name = get_remote_name(repo)
else:
current_branch = repo.active_branch
if current_branch.tracking_branch() is None:
remote_name = get_remote_name(repo)
else:
remote_name = current_branch.tracking_branch().remote_name
commit_hash = repo.head.commit.hexsha
url = repo.remotes[remote_name].url
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:
print(f"Failed to extract snapshots for the custom node '{path}'.")
@@ -2885,7 +2981,7 @@ async def get_unified_total_nodes(channel, mode, regsitry_cache_mode='cache'):
if cnr_id is not None:
# cnr or nightly version
cnr_ids.remove(cnr_id)
cnr_ids.discard(cnr_id)
updatable = False
cnr = unified_manager.cnr_map[cnr_id]
@@ -3027,6 +3123,9 @@ async def restore_snapshot(snapshot_path, git_helper_extras=None):
enabled_repos = []
disabled_repos = []
skip_node_packs = []
switched_node_packs = []
installed_node_packs = []
failed = []
await unified_manager.reload('cache')
await unified_manager.get_custom_nodes('default', 'cache')
@@ -3046,6 +3145,11 @@ async def restore_snapshot(snapshot_path, git_helper_extras=None):
info = yaml.load(snapshot_file, Loader=yaml.SafeLoader)
info = info['custom_nodes']
if 'pips' in info and info['pips']:
pips = info['pips']
else:
pips = {}
# for cnr restore
cnr_info = info.get('cnr_custom_nodes')
if cnr_info is not None:
@@ -3072,8 +3176,13 @@ async def restore_snapshot(snapshot_path, git_helper_extras=None):
disabled_repos.append(x)
for x in todo_checkout:
unified_manager.cnr_switch_version(x[0], x[1], instant_execution=True, no_deps=True, return_postinstall=False)
checkout_repos.append(x[1])
ps = unified_manager.cnr_switch_version(x[0], x[1], instant_execution=True, no_deps=True, return_postinstall=False)
if ps.action == 'switch-cnr' and ps.result:
switched_node_packs.append(f"{x[0]}@{x[1]}")
elif ps.action == 'skip':
skip_node_packs.append(f"{x[0]}@{x[1]}")
elif not ps.result:
failed.append(f"{x[0]}@{x[1]}")
# install listed cnr nodes
for k, v in cnr_info.items():
@@ -3081,7 +3190,9 @@ async def restore_snapshot(snapshot_path, git_helper_extras=None):
continue
ps = await unified_manager.install_by_id(k, version_spec=v, instant_execution=True, return_postinstall=True)
cloned_repos.append(k)
if ps.action == 'install-cnr' and ps.result:
installed_node_packs.append(f"{k}@{v}")
if ps is not None and ps.result:
if hasattr(ps, 'postinstall'):
postinstalls.append(ps.postinstall)
@@ -3139,40 +3250,41 @@ async def restore_snapshot(snapshot_path, git_helper_extras=None):
disabled_repos.append(x)
for x in todo_enable:
res = unified_manager.unified_enable(x, 'nightly')
res = unified_manager.unified_enable(x[0], 'nightly')
is_switched = False
if res and res.target:
is_switched = repo_switch_commit(res.target, x[1])
if is_switched:
checkout_repos.append(x)
checkout_repos.append(f"{x[0]}@{x[1]}")
else:
enabled_repos.append(x)
enabled_repos.append(x[0])
for x in todo_checkout:
is_switched = repo_switch_commit(x[0], x[1])
if is_switched:
checkout_repos.append(x)
else:
skip_node_packs.append(x[0])
checkout_repos.append(f"{x[0]}@{x[1]}")
for x in git_info.keys():
normalized_url = git_utils.normalize_url(x)
cnr = unified_manager.repo_cnr_map.get(normalized_url)
if cnr is not None:
pack_id = cnr['id']
await unified_manager.install_by_id(pack_id, 'nightly', instant_execution=True, no_deps=False, return_postinstall=False)
cloned_repos.append(pack_id)
res = await unified_manager.install_by_id(pack_id, 'nightly', instant_execution=True, no_deps=False, return_postinstall=False)
if res.action == 'install-git' and res.result:
cloned_repos.append(pack_id)
elif res.action == 'skip':
skip_node_packs.append(pack_id)
elif not res.result:
failed.append(pack_id)
processed_urls.append(x)
for x in processed_urls:
if x in git_info:
del git_info[x]
# remained nightly will be installed and migrated
# for unknown restore
todo_disable = []
todo_enable = []
@@ -3219,15 +3331,15 @@ async def restore_snapshot(snapshot_path, git_helper_extras=None):
is_switched = repo_switch_commit(res.target, x[1])
if is_switched:
checkout_repos.append(x)
checkout_repos.append(f"{x[0]}@{x[1]}")
else:
enabled_repos.append(x)
enabled_repos.append(x[0])
for x in todo_checkout:
is_switched = repo_switch_commit(x[0], x[1])
if is_switched:
checkout_repos.append(x)
checkout_repos.append(f"{x[0]}@{x[1]}")
else:
skip_node_packs.append(x[0])
@@ -3244,84 +3356,105 @@ async def restore_snapshot(snapshot_path, git_helper_extras=None):
unified_manager.repo_install(repo_url, to_path, instant_execution=True, no_deps=False, return_postinstall=False)
cloned_repos.append(repo_name)
# reload
await unified_manager.migrate_unmanaged_nodes()
manager_util.restore_pip_snapshot(pips, git_helper_extras)
# print summary
for x in cloned_repos:
print(f"[ INSTALLED ] {x}")
for x in installed_node_packs:
print(f"[ INSTALLED ] {x}")
for x in checkout_repos:
print(f"[ CHECKOUT ] {x}")
for x in switched_node_packs:
print(f"[ SWITCHED ] {x}")
for x in enabled_repos:
print(f"[ ENABLED ] {x}")
for x in disabled_repos:
print(f"[ DISABLED ] {x}")
for x in skip_node_packs:
print(f"[ SKIPPED ] {x}")
print(f"[ SKIPPED ] {x}")
for x in failed:
print(f"[ FAILED ] {x}")
# if is_failed:
# print("[bold red]ERROR: Failed to restore snapshot.[/bold red]")
# check need to migrate
need_to_migrate = False
async def check_need_to_migrate():
global need_to_migrate
await unified_manager.reload('cache')
await unified_manager.load_nightly(channel='default', mode='cache')
legacy_custom_nodes = []
for x in unified_manager.active_nodes.values():
if x[0] == 'nightly' and not x[1].endswith('@nightly'):
legacy_custom_nodes.append(x[1])
for x in unified_manager.nightly_inactive_nodes.values():
if not x.endswith('@nightly'):
legacy_custom_nodes.append(x)
if len(legacy_custom_nodes) > 0:
print("\n--------------------- ComfyUI-Manager migration notice --------------------")
print("The following custom nodes were installed using the old management method and require migration:\n")
print("\n".join(legacy_custom_nodes))
print("---------------------------------------------------------------------------\n")
need_to_migrate = True
def get_comfyui_versions(repo=None):
if repo is None:
repo = git.Repo(comfy_path)
repo = repo or git.Repo(comfy_path)
remote_name = None
try:
remote = get_remote_name(repo)
repo.remotes[remote].fetch()
remote_name = get_remote_name(repo)
repo.remotes[remote_name].fetch()
except:
logging.error("[ComfyUI-Manager] Failed to fetch ComfyUI")
versions = [x.name for x in repo.tags if x.name.startswith('v')]
def parse_semver(tag_name):
match = re.match(r'^v(\d+)\.(\d+)\.(\d+)$', tag_name)
return tuple(int(x) for x in match.groups()) if match else None
# nearest tag
versions = sorted(versions, key=lambda v: repo.git.log('-1', '--format=%ct', v), reverse=True)
versions = versions[:4]
def normalize_describe(tag_name):
if not tag_name:
return None
base = tag_name.split('-', 1)[0]
return base if parse_semver(base) else None
current_tag = repo.git.describe('--tags')
# Collect semver tags and sort descending (highest first)
semver_tags = []
for tag in repo.tags:
semver = parse_semver(tag.name)
if semver:
semver_tags.append((semver, tag.name))
semver_tags.sort(key=lambda x: x[0], reverse=True)
semver_tags = [name for _, name in semver_tags]
if current_tag not in versions:
versions = sorted(versions + [current_tag], key=lambda v: repo.git.log('-1', '--format=%ct', v), reverse=True)
versions = versions[:4]
latest_tag = semver_tags[0] if semver_tags else None
main_branch = repo.heads.master
latest_commit = main_branch.commit
latest_tag = repo.git.describe('--tags', latest_commit.hexsha)
try:
described = repo.git.describe('--tags')
except Exception:
described = ''
if latest_tag != versions[0]:
versions.insert(0, 'nightly')
else:
versions[0] = 'nightly'
try:
exact_tag = repo.git.describe('--tags', '--exact-match')
except Exception:
exact_tag = ''
head_is_default = False
if remote_name:
try:
default_head_ref = repo.refs[f'{remote_name}/HEAD']
default_commit = default_head_ref.reference.commit
head_is_default = repo.head.commit == default_commit
except Exception:
head_is_default = False
nearest_semver = normalize_describe(described)
exact_semver = exact_tag if parse_semver(exact_tag) else None
if head_is_default and not exact_tag:
current_tag = 'nightly'
else:
current_tag = exact_tag or described or 'nightly'
# Prepare semver list for display: top 4 plus the current/nearest semver if missing
display_semver_tags = semver_tags[:4]
if exact_semver and exact_semver not in display_semver_tags:
display_semver_tags.append(exact_semver)
elif nearest_semver and nearest_semver not in display_semver_tags:
display_semver_tags.append(nearest_semver)
versions = ['nightly']
if current_tag and not exact_semver and current_tag not in versions and current_tag not in display_semver_tags:
versions.append(current_tag)
for tag in display_semver_tags:
if tag not in versions:
versions.append(tag)
versions = versions[:6]
return versions, current_tag, latest_tag
+5 -1
View File
@@ -55,7 +55,11 @@ def download_url(model_url: str, model_dir: str, filename: str):
return aria2_download_url(model_url, model_dir, filename)
else:
from torchvision.datasets.utils import download_url as torchvision_download_url
return torchvision_download_url(model_url, model_dir, filename)
try:
return torchvision_download_url(model_url, model_dir, filename)
except Exception as e:
logging.error(f"[ComfyUI-Manager] Failed to download: {model_url} / {repr(e)}")
raise
def aria2_find_task(dir: str, filename: str):
+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
+450 -98
View File
@@ -11,7 +11,10 @@ import threading
import re
import shutil
import git
import glob
import json
from datetime import datetime
from contextlib import contextmanager
from server import PromptServer
import manager_core as core
@@ -22,6 +25,7 @@ import asyncio
import queue
import manager_downloader
import manager_migration
logging.info(f"### Loading: ComfyUI-Manager ({core.version_str})")
@@ -34,9 +38,30 @@ SECURITY_MESSAGE_MIDDLE_OR_BELOW = "ERROR: To use this action, a security_level
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."
SECURITY_MESSAGE_FLAG_GIT_URL = "ERROR: This action requires 'allow_git_url_install = true' in config.ini ([default] section). This setting is independent of security_level. Reference: https://github.com/ltdrdata/ComfyUI-Manager#security-policy"
SECURITY_MESSAGE_FLAG_PIP = "ERROR: This action requires 'allow_pip_install = true' in config.ini ([default] section). This setting is independent of security_level. Reference: https://github.com/ltdrdata/ComfyUI-Manager#security-policy"
routes = PromptServer.instance.routes
def has_per_queue_preview():
"""
Check if ComfyUI PR #11261 (per-queue live preview override) is merged
Returns:
bool: True if ComfyUI has per-queue preview feature
"""
try:
import latent_preview
return hasattr(latent_preview, 'set_preview_method')
except ImportError:
return False
# Detect ComfyUI per-queue preview override feature (PR #11261)
COMFYUI_HAS_PER_QUEUE_PREVIEW = has_per_queue_preview()
def handle_stream(stream, prefix):
stream.reconfigure(encoding=locale.getpreferredencoding(), errors='replace')
for msg in stream:
@@ -62,6 +87,19 @@ def is_loopback(address):
except ValueError:
return False
def is_dedicated_install_allowed(flag_value: bool, listen_address: str) -> bool:
"""P-direct predicate (adopter-degraded form): flag AND loopback.
Pure helper for the dedicated install flags
(allow_git_url_install / allow_pip_install) callers pass the
flag value from their own config read and the listener address
from the CLI arguments (request-time evaluation; the import-time
snapshot above is NOT consulted).
"""
return bool(flag_value) and is_loopback(listen_address)
is_local_mode = is_loopback(args.listen)
@@ -181,7 +219,19 @@ def set_preview_method(method):
core.get_config()['preview_method'] = method
set_preview_method(core.get_config()['preview_method'])
if COMFYUI_HAS_PER_QUEUE_PREVIEW:
logging.info(
"[ComfyUI-Manager] ComfyUI per-queue preview override detected (PR #11261). "
"Manager's preview method feature is disabled. "
"Use ComfyUI's --preview-method CLI option or 'Settings > Execution > Live preview method'."
)
elif args.preview_method == latent_preview.LatentPreviewMethod.NoPreviews:
set_preview_method(core.get_config()['preview_method'])
else:
logging.warning(
"[ComfyUI-Manager] Since --preview-method is set, "
"ComfyUI-Manager's preview method feature will be ignored."
)
def set_component_policy(mode):
@@ -273,14 +323,89 @@ import zipfile
import urllib.request
def security_403_response(flag_token=None):
"""Return appropriate 403 response based on ComfyUI version.
When `flag_token` is given (dedicated install flag denials), the
body names the responsible flag instead of "security_level". The
`comfyui_outdated` branch stays the FIRST check regardless, and
no-arg callers keep today's body byte-identical.
"""
if not manager_migration.has_system_user_api():
return web.json_response({"error": "comfyui_outdated"}, status=403)
if flag_token is not None:
return web.json_response({"error": flag_token}, status=403)
return web.json_response({"error": "security_level"}, status=403)
# CORS "simple request" Content-Type set per Fetch spec §3.2.3. Browsers send
# <form method=POST> submissions with one of these three MIME types and do NOT
# trigger a CORS preflight, so a malicious cross-origin page can silently POST
# into state-changing endpoints if we only gate on HTTP method. Blocking these
# three Content-Types on no-body mutation endpoints forces any non-same-origin
# POST to use a non-simple Content-Type (e.g. application/json), which triggers
# a preflight that this server rejects by not advertising an Access-Control-
# Allow-Origin response.
_SIMPLE_FORM_CONTENT_TYPES = frozenset({
'application/x-www-form-urlencoded',
'multipart/form-data',
'text/plain',
})
def _reject_simple_form_content_type(request):
"""Reject Content-Types that enable preflight-less <form method=POST> CSRF.
Applied ONLY to POST handlers that do not consume a request body (e.g.,
/snapshot/save, /manager/queue/{reset,start,update_comfyui},
/manager/reboot). These are vulnerable to cross-origin <form method=POST>
attacks because the handler accepts the request without parsing any body
the attacker needs no ability to forge a valid payload, only to point a
hidden form at the URL.
Handlers that already read a body via ``await request.json()`` are NOT
gated here: a cross-origin <form method=POST> cannot forge a valid JSON
body because the browser refuses to send ``application/json`` without a
CORS preflight, which this server does not answer.
DO NOT add this gate to body-reading handlers redundant and UX-breaking.
DO NOT remove this gate from no-body handlers this is the bypass vector.
aiohttp's ``request.content_type`` normalizes the header (lower-cases,
strips parameters), so ``multipart/form-data; boundary=----X`` is compared
as ``multipart/form-data``.
Returns:
web.Response(status=400) when the request has a simple-form
Content-Type that must be rejected. None when the request is allowed
to proceed (no Content-Type, application/json, or any non-simple
Content-Type).
"""
if request.content_type in _SIMPLE_FORM_CONTENT_TYPES:
return web.Response(
status=400,
text='Invalid Content-Type for this endpoint. Use application/json or omit body.',
)
return None
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.
@@ -399,7 +524,6 @@ async def task_worker():
try:
node_spec = core.unified_manager.resolve_node_spec(node_spec_str)
if node_spec is None:
logging.error(f"Cannot resolve install target: '{node_spec_str}'")
return f"Cannot resolve install target: '{node_spec_str}'"
@@ -429,7 +553,10 @@ async def task_worker():
if res.ver == 'unknown':
url = core.unified_manager.unknown_active_nodes[node_name][0]
title = os.path.basename(url)
try:
title = os.path.basename(url)
except Exception:
title = node_name
else:
url = core.unified_manager.cnr_map[node_name].get('repository')
title = core.unified_manager.cnr_map[node_name]['name']
@@ -575,7 +702,7 @@ async def task_worker():
return 'success'
except Exception as e:
logging.error(f"[ComfyUI-Manager] ERROR: {e}", file=sys.stderr)
logging.error(f"[ComfyUI-Manager] ERROR: {e}")
return f"Model installation error: {model_url}"
@@ -687,16 +814,19 @@ async def fetch_customnode_mappings(request):
return web.json_response(json_obj, content_type='application/json')
@routes.get("/customnode/fetch_updates")
@routes.post("/customnode/fetch_updates")
async def fetch_updates(request):
try:
if request.rel_url.query["mode"] == "local":
json_data = await request.json()
mode = json_data.get("mode", "default")
if mode == "local":
channel = 'local'
else:
channel = core.get_config()['channel_url']
await core.unified_manager.reload(request.rel_url.query["mode"])
await core.unified_manager.get_custom_nodes(channel, request.rel_url.query["mode"])
await core.unified_manager.reload(mode)
await core.unified_manager.get_custom_nodes(channel, mode)
res = core.unified_manager.fetch_or_pull_git_repo(is_pull=False)
@@ -712,28 +842,111 @@ async def fetch_updates(request):
except:
traceback.print_exc()
return web.Response(status=400)
@routes.get("/customnode/get_node_types_in_workflows")
async def get_node_types_in_workflows(request):
try:
# get our username from the request header
user_id = PromptServer.instance.user_manager.get_request_user_id(request)
# get the base workflow directory (TODO: figure out if non-standard directories are possible, and how to find them)
workflow_files_base_path = os.path.abspath(os.path.join(folder_paths.get_user_directory(), user_id, "workflows"))
logging.debug(f"workflows base path: {workflow_files_base_path}")
# workflow directory doesn't actually exist, return 204 (No Content)
if not os.path.isdir(workflow_files_base_path):
logging.debug("workflows base path doesn't exist - nothing to do...")
return web.Response(status=204)
# get all JSON files under the workflow directory
workflow_file_relative_paths: list[str] = glob.glob(pathname="**/*.json", root_dir=workflow_files_base_path, recursive=True)
logging.debug(f"found the following workflows: {workflow_file_relative_paths}")
# set up our list of workflow/node-lists
workflow_node_mappings: list[dict[str, str | list[str]]] = []
# iterate over each found JSON file
for workflow_file_path in workflow_file_relative_paths:
try:
workflow_file_absolute_path = os.path.abspath(os.path.join(workflow_files_base_path, workflow_file_path))
logging.debug(f"starting work on {workflow_file_absolute_path}")
# load the JSON file
workflow_file_data = json.load(open(workflow_file_absolute_path, "r"))
# make sure there's a nodes key (otherwise this might not actually be a workflow file)
if "nodes" not in workflow_file_data:
logging.warning(f"{workflow_file_path} has no 'nodes' key (possibly invalid?) - skipping...")
# skip to next file
continue
# now this looks like a valid file, so let's get to work
new_mapping = {"workflow_file_name": workflow_file_path}
# we can't use an actual set, because you can't use dicts as set members
node_set = []
# iterate over each node in the workflow
for node in workflow_file_data["nodes"]:
if "id" not in node:
logging.warning(f"Found a node with no ID - possibly corrupt/invalid workflow?")
continue
# if there's no type, throw a warning
if "type" not in node:
logging.warning(f"Node type not found in {workflow_file_path} for node ID {node['id']}")
# skip to next node
continue
node_data_to_return = {"type": node["type"]}
if "properties" not in node:
logging.warning(f"Node ${node['id']} has no properties field - can't determine cnr_id")
else:
for property_key in ["cnr_id", "ver"]:
if property_key in node["properties"]:
node_data_to_return[property_key] = node["properties"][property_key]
# add it to the list for this workflow
if not node_data_to_return in node_set:
node_set.append(node_data_to_return)
# annoyingly, Python can't serialize sets to JSON
new_mapping["node_types"] = list(node_set)
workflow_node_mappings.append(new_mapping)
except Exception as e:
logging.warning(f"Couldn't open {workflow_file_path}: {e}")
return web.json_response(workflow_node_mappings, content_type='application/json')
except:
traceback.print_exc()
return web.Response(status=500)
@routes.get("/manager/queue/update_all")
@routes.post("/manager/queue/update_all")
async def update_all(request):
if not is_allowed_security_level('middle'):
logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW)
return web.Response(status=403)
return security_403_response()
with task_worker_lock:
is_processing = task_worker_thread is not None and task_worker_thread.is_alive()
if is_processing:
return web.Response(status=401)
await core.save_snapshot_with_postfix('autosave')
if request.rel_url.query["mode"] == "local":
json_data = await request.json()
mode = json_data.get("mode", "default")
if mode == "local":
channel = 'local'
else:
channel = core.get_config()['channel_url']
await core.unified_manager.reload(request.rel_url.query["mode"])
await core.unified_manager.get_custom_nodes(channel, request.rel_url.query["mode"])
await core.unified_manager.reload(mode)
await core.unified_manager.get_custom_nodes(channel, mode)
for k, v in core.unified_manager.active_nodes.items():
if k == 'comfyui-manager':
@@ -851,7 +1064,7 @@ async def fetch_customnode_list(request):
channel = found
result = dict(channel=channel, node_packs=node_packs)
result = dict(channel=channel, node_packs=node_packs.to_dict())
return web.json_response(result, content_type='application/json')
@@ -929,6 +1142,7 @@ def check_model_installed(json_obj):
@routes.get("/externalmodel/getlist")
async def fetch_externalmodel_list(request):
# The model list is only allowed in the default channel, yet.
json_obj = await core.get_data_by_mode(request.rel_url.query["mode"], 'model-list.json')
check_model_installed(json_obj)
@@ -946,16 +1160,30 @@ async def get_snapshot_list(request):
return web.json_response({'items': items}, content_type='application/json')
@routes.get("/snapshot/remove")
def get_safe_snapshot_path(target):
"""
Safely construct a snapshot file path, preventing path traversal attacks.
"""
if '/' in target or '\\' in target or '..' in target or '\x00' in target:
return None
return os.path.join(core.manager_snapshot_path, f"{target}.json")
@routes.post("/snapshot/remove")
async def remove_snapshot(request):
if not is_allowed_security_level('middle'):
logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW)
return web.Response(status=403)
return security_403_response()
try:
target = request.rel_url.query["target"]
json_data = await request.json()
target = json_data["target"]
path = get_safe_snapshot_path(target)
if path is None:
logging.error(f"[ComfyUI-Manager] Invalid snapshot target: {target}")
return web.Response(text="Invalid snapshot target", status=400)
path = os.path.join(core.manager_snapshot_path, f"{target}.json")
if os.path.exists(path):
os.remove(path)
@@ -964,16 +1192,21 @@ async def remove_snapshot(request):
return web.Response(status=400)
@routes.get("/snapshot/restore")
@routes.post("/snapshot/restore")
async def restore_snapshot(request):
if not is_allowed_security_level('middle'):
logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW)
return web.Response(status=403)
return security_403_response()
try:
target = request.rel_url.query["target"]
json_data = await request.json()
target = json_data["target"]
path = get_safe_snapshot_path(target)
if path is None:
logging.error(f"[ComfyUI-Manager] Invalid snapshot target: {target}")
return web.Response(text="Invalid snapshot target", status=400)
path = os.path.join(core.manager_snapshot_path, f"{target}.json")
if os.path.exists(path):
if not os.path.exists(core.manager_startup_script_path):
os.makedirs(core.manager_startup_script_path)
@@ -998,8 +1231,11 @@ async def get_current_snapshot_api(request):
return web.Response(status=400)
@routes.get("/snapshot/save")
@routes.post("/snapshot/save")
async def save_snapshot(request):
resp = _reject_simple_form_content_type(request)
if resp is not None:
return resp
try:
await core.save_snapshot_with_postfix('snapshot')
return web.Response(status=200)
@@ -1160,8 +1396,11 @@ async def reinstall_custom_node(request):
await install_custom_node(request)
@routes.get("/manager/queue/reset")
@routes.post("/manager/queue/reset")
async def reset_queue(request):
resp = _reject_simple_form_content_type(request)
if resp is not None:
return resp
global task_queue
task_queue = queue.Queue()
return web.Response(status=200)
@@ -1197,12 +1436,31 @@ async def install_custom_node(request):
git_url = None
if json_data['version'] != 'unknown':
selected_version = json_data.get('selected_version')
selected_version = json_data.get('selected_version')
if json_data['version'] != 'unknown' and selected_version != 'unknown':
if skip_post_install:
if cnr_id in core.unified_manager.nightly_inactive_nodes or cnr_id in core.unified_manager.cnr_inactive_nodes:
core.unified_manager.unified_enable(cnr_id)
# Mirror the pair of events (in_progress then done) that the async
# task_worker normally emits for queued operations. The in_progress
# event is what sets item.restart=true on the client row so the
# action cell re-renders as "Restart Required"; without it, the
# "Enable" button remains visible after successful enable. The done
# event drives the completion UI (toast, restart indicator, button
# loading clear).
ui_id = json_data.get('ui_id', cnr_id)
PromptServer.instance.send_sync(
"cm-queue-status",
{'status': 'in_progress',
'target': ui_id,
'ui_target': 'nodepack_manager',
'total_count': 1, 'done_count': 0})
PromptServer.instance.send_sync(
"cm-queue-status",
{'status': 'done',
'nodepack_result': {ui_id: 'success'},
'model_result': {},
'total_count': 1, 'done_count': 1})
return web.Response(status=200)
elif selected_version is None:
selected_version = 'latest'
@@ -1216,6 +1474,9 @@ async def install_custom_node(request):
if git_url is None:
logging.error(f"[ComfyUI-Manager] Following node pack doesn't provide `nightly` version: ${git_url}")
return web.Response(status=404, text=f"Following node pack doesn't provide `nightly` version: ${git_url}")
elif json_data['version'] != 'unknown' and selected_version == 'unknown':
logging.error(f"[ComfyUI-Manager] Invalid installation request: {json_data}")
return web.Response(status=400, text="Invalid installation request")
else:
# unknown
unknown_name = os.path.basename(json_data['files'][0])
@@ -1229,7 +1490,17 @@ async def install_custom_node(request):
else:
return web.Response(status=404, text=f"Following node pack doesn't provide `nightly` version: ${git_url}")
if not is_allowed_security_level(risky_level):
if risky_level == 'high':
# unknown-URL arm: governed by the dedicated flag predicate
# (flag AND loopback, evaluated at request time). The loopback
# term is load-bearing here — the 'middle' entry gate above has
# no network-position term.
if not is_dedicated_install_allowed(core.get_config()['allow_git_url_install'], args.listen):
logging.error(SECURITY_MESSAGE_FLAG_GIT_URL)
return web.Response(status=404, text="A security error has occurred. Please check the terminal logs")
elif not is_allowed_security_level(risky_level):
# 'block' arm stays an unconditional deny (is_allowed_security_level
# returns False for 'block'); 'middle'/'low' arms unchanged.
logging.error(SECURITY_MESSAGE_GENERAL)
return web.Response(status=404, text="A security error has occurred. Please check the terminal logs")
@@ -1241,8 +1512,11 @@ async def install_custom_node(request):
task_worker_thread:threading.Thread = None
@routes.get("/manager/queue/start")
@routes.post("/manager/queue/start")
async def queue_start(request):
resp = _reject_simple_form_content_type(request)
if resp is not None:
return resp
global nodepack_result
global model_result
global task_worker_thread
@@ -1283,11 +1557,21 @@ async def fix_custom_node(request):
@routes.post("/customnode/install/git_url")
async def install_custom_node_git_url(request):
if not is_allowed_security_level('high'):
logging.error(SECURITY_MESSAGE_NORMAL_MINUS)
return web.Response(status=403)
if not is_dedicated_install_allowed(core.get_config()['allow_git_url_install'], args.listen):
logging.error(SECURITY_MESSAGE_FLAG_GIT_URL)
return security_403_response(flag_token='allow_git_url_install')
url = await request.text()
# Read the body as JSON (not raw text): a cross-origin <form method=POST>
# cannot forge an application/json body without a CORS preflight, which this
# server does not answer — same body-handler convention as every other
# request.json() route (see _reject_simple_form_content_type docstring).
# A malformed body or a missing 'url' is a client error (400), not a 500:
# don't let JSONDecodeError/KeyError bubble up as a server fault.
try:
json_data = await request.json()
url = json_data['url']
except (KeyError, ValueError):
return web.Response(status=400, text="Invalid request body: expected JSON object with a 'url' field")
res = await core.gitclone_install(url)
if res.action == 'skip':
@@ -1303,11 +1587,18 @@ async def install_custom_node_git_url(request):
@routes.post("/customnode/install/pip")
async def install_custom_node_pip(request):
if not is_allowed_security_level('high'):
logging.error(SECURITY_MESSAGE_NORMAL_MINUS)
return web.Response(status=403)
if not is_dedicated_install_allowed(core.get_config()['allow_pip_install'], args.listen):
logging.error(SECURITY_MESSAGE_FLAG_PIP)
return security_403_response(flag_token='allow_pip_install')
packages = await request.text()
# JSON body (not raw text) for the same preflight-forcing reason as
# /customnode/install/git_url above; malformed body or missing 'packages'
# is a 400, not a 500.
try:
json_data = await request.json()
packages = json_data['packages']
except (KeyError, ValueError):
return web.Response(status=400, text="Invalid request body: expected JSON object with a 'packages' field")
core.pip_install(packages.split(' '))
return web.Response(status=200)
@@ -1357,8 +1648,11 @@ async def update_custom_node(request):
return web.Response(status=200)
@routes.get("/manager/queue/update_comfyui")
@routes.post("/manager/queue/update_comfyui")
async def update_comfyui(request):
resp = _reject_simple_form_content_type(request)
if resp is not None:
return resp
is_stable = core.get_config()['update_policy'] != 'nightly-comfyui'
task_queue.put(("update-comfyui", ('comfyui', is_stable)))
return web.Response(status=200)
@@ -1375,11 +1669,12 @@ async def comfyui_versions(request):
return web.Response(status=400)
@routes.get("/comfyui_manager/comfyui_switch_version")
@routes.post("/comfyui_manager/comfyui_switch_version")
async def comfyui_switch_version(request):
try:
if "ver" in request.rel_url.query:
core.switch_comfyui(request.rel_url.query['ver'])
json_data = await request.json()
if "ver" in json_data:
core.switch_comfyui(json_data['ver'])
return web.Response(status=200)
except Exception as e:
@@ -1407,17 +1702,20 @@ async def disable_node(request):
return web.Response(status=200)
@routes.get("/manager/migrate_unmanaged_nodes")
async def migrate_unmanaged_nodes(request):
logging.info("[ComfyUI-Manager] Migrating unmanaged nodes...")
await core.unified_manager.migrate_unmanaged_nodes()
logging.info("Done.")
return web.Response(status=200)
async def check_whitelist_for_model(item):
json_obj = await core.get_data_by_mode('cache', '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
@routes.get("/manager/need_to_migrate")
async def need_to_migrate(request):
return web.Response(text=str(core.need_to_migrate), status=200)
json_obj = await core.get_data_by_mode('local', '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
@routes.post("/manager/queue/install_model")
@@ -1428,6 +1726,11 @@ async def install_model(request):
logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW)
return web.Response(status=403, text="A security error has occurred. Please check the terminal logs")
# validate request
if not await check_whitelist_for_model(json_data):
logging.error(f"[ComfyUI-Manager] Invalid model install request is detected: {json_data}")
return web.Response(status=400, text="Invalid model install request is detected")
if not json_data['filename'].endswith('.safetensors') and not is_allowed_security_level('high'):
models_json = await core.get_data_by_mode('cache', 'model-list.json', 'default')
@@ -1448,71 +1751,87 @@ async def install_model(request):
@routes.get("/manager/preview_method")
async def preview_method(request):
if "value" in request.rel_url.query:
set_preview_method(request.rel_url.query['value'])
core.write_config()
else:
return web.Response(text=core.manager_funcs.get_current_preview_method(), status=200)
async def get_preview_method(request):
if COMFYUI_HAS_PER_QUEUE_PREVIEW:
return web.Response(text="DISABLED", status=200)
return web.Response(text=core.manager_funcs.get_current_preview_method(), status=200)
@routes.post("/manager/preview_method")
async def set_preview_method_handler(request):
if COMFYUI_HAS_PER_QUEUE_PREVIEW:
return web.Response(text="DISABLED", status=403)
json_data = await request.json()
set_preview_method(json_data['value'])
core.write_config()
return web.Response(status=200)
@routes.get("/manager/db_mode")
async def db_mode(request):
if "value" in request.rel_url.query:
set_db_mode(request.rel_url.query['value'])
core.write_config()
else:
return web.Response(text=core.get_config()['db_mode'], status=200)
async def get_db_mode(request):
return web.Response(text=core.get_config()['db_mode'], status=200)
@routes.post("/manager/db_mode")
async def set_db_mode_handler(request):
json_data = await request.json()
set_db_mode(json_data['value'])
core.write_config()
return web.Response(status=200)
@routes.get("/manager/policy/component")
async def component_policy(request):
if "value" in request.rel_url.query:
set_component_policy(request.rel_url.query['value'])
core.write_config()
else:
return web.Response(text=core.get_config()['component_policy'], status=200)
async def get_component_policy(request):
return web.Response(text=core.get_config()['component_policy'], status=200)
@routes.post("/manager/policy/component")
async def set_component_policy_handler(request):
json_data = await request.json()
set_component_policy(json_data['value'])
core.write_config()
return web.Response(status=200)
@routes.get("/manager/policy/update")
async def update_policy(request):
if "value" in request.rel_url.query:
set_update_policy(request.rel_url.query['value'])
core.write_config()
else:
return web.Response(text=core.get_config()['update_policy'], status=200)
async def get_update_policy(request):
return web.Response(text=core.get_config()['update_policy'], status=200)
@routes.post("/manager/policy/update")
async def set_update_policy_handler(request):
json_data = await request.json()
set_update_policy(json_data['value'])
core.write_config()
return web.Response(status=200)
@routes.get("/manager/channel_url_list")
async def channel_url_list(request):
async def get_channel_url_list(request):
channels = core.get_channel_dict()
if "value" in request.rel_url.query:
channel_url = channels.get(request.rel_url.query['value'])
if channel_url is not None:
core.get_config()['channel_url'] = channel_url
core.write_config()
else:
selected = 'custom'
selected_url = core.get_config()['channel_url']
selected = 'custom'
selected_url = core.get_config()['channel_url']
for name, url in channels.items():
if url == selected_url:
selected = name
break
for name, url in channels.items():
if url == selected_url:
selected = name
break
res = {'selected': selected,
'list': core.get_channel_list()}
return web.json_response(res, status=200)
res = {'selected': selected,
'list': core.get_channel_list()}
return web.json_response(res, status=200)
@routes.post("/manager/channel_url_list")
async def set_channel_url_list(request):
json_data = await request.json()
channels = core.get_channel_dict()
channel_url = channels.get(json_data['value'])
if channel_url is not None:
core.get_config()['channel_url'] = channel_url
core.write_config()
return web.Response(status=200)
@@ -1569,6 +1888,16 @@ async def get_notice(request):
except:
pass
# Prepend startup notices from manager_migration
for message, level in reversed(manager_migration.startup_notices):
if level == 'error':
style = 'color:red; background-color:white; font-weight:bold'
elif level == 'warning':
style = 'color:orange; background-color:white; font-weight:bold'
else:
style = 'color:blue; background-color:white'
markdown_content = f'<P style="{style}">{message}</P>' + markdown_content
return web.Response(text=markdown_content, status=200)
else:
return web.Response(text="Unable to retrieve Notice", status=200)
@@ -1576,11 +1905,38 @@ async def get_notice(request):
return web.Response(text="Unable to retrieve Notice", status=200)
@routes.get("/manager/reboot")
@routes.get("/manager/startup_alerts")
async def get_startup_alerts(request):
"""Return startup alerts for customAlert display on page load.
Returns JSON array of alerts that should be shown to user immediately.
All startup notices (error, warning, info) are returned.
"""
alerts = []
# Return all startup notices for alert display
for message, level in manager_migration.startup_notices:
# Convert HTML BR to newlines for customAlert
text = message.replace('<BR>', '\n').replace('<br>', '\n')
# Add [ComfyUI-Manager] prefix for customAlert (notice board shows in Manager UI anyway)
text = text.replace('[Security Alert]', '[ComfyUI-Manager] Security Alert:')
text = text.replace('[MIGRATION]', '[ComfyUI-Manager] Migration:')
alerts.append({
'message': text,
'level': level
})
return web.json_response(alerts)
@routes.post("/manager/reboot")
def restart(self):
resp = _reject_simple_form_content_type(self)
if resp is not None:
return resp
if not is_allowed_security_level('middle'):
logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW)
return web.Response(status=403)
return security_403_response()
try:
sys.stdout.close_log()
@@ -1695,6 +2051,7 @@ cm_global.register_api('cm.try-install-custom-node', confirm_try_install)
async def default_cache_update():
core.refresh_channel_dict()
channel_url = core.get_config()['channel_url']
async def get_cache(filename):
try:
@@ -1734,11 +2091,6 @@ async def default_cache_update():
logging.info("[ComfyUI-Manager] All startup tasks have been completed.")
# NOTE: hide migration button temporarily.
# if not core.get_config()['skip_migration_check']:
# await core.check_need_to_migrate()
# else:
# logging.info("[ComfyUI-Manager] Migration check is skipped...")
threading.Thread(target=lambda: asyncio.run(default_cache_update())).start()
+162 -53
View File
@@ -15,6 +15,7 @@ import re
import logging
import platform
import shlex
from functools import lru_cache
cache_lock = threading.Lock()
@@ -23,7 +24,7 @@ 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
bypass_ssl = False
def add_python_path_to_env():
if platform.system() != "Windows":
@@ -34,11 +35,63 @@ def add_python_path_to_env():
os.environ['PATH'] = os.path.dirname(sys.executable)+sep+os.environ['PATH']
@lru_cache(maxsize=2)
def get_pip_cmd(force_uv=False):
"""
Get the base pip command, with automatic fallback to uv if pip is unavailable.
Args:
force_uv (bool): If True, use uv directly without trying pip
Returns:
list: Base command for pip operations
"""
embedded = 'python_embeded' in sys.executable
# Try pip first (unless forcing uv)
if not force_uv:
try:
test_cmd = [sys.executable] + (['-s'] if embedded else []) + ['-m', 'pip', '--version']
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`.")
# Try uv (either forced or pip failed)
import shutil
# Try uv as Python module
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.")
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.")
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")
def make_pip_cmd(cmd):
if use_uv:
return [sys.executable, '-m', 'uv', 'pip'] + cmd
else:
return [sys.executable, '-m', 'pip'] + cmd
"""
Create a pip command by combining the cached base pip command with the given arguments.
Args:
cmd (list): List of pip command arguments (e.g., ['install', 'package'])
Returns:
list: Complete command list ready for subprocess execution
"""
global use_uv
base_cmd = get_pip_cmd(force_uv=use_uv)
return base_cmd + cmd
# DON'T USE StrictVersion - cannot handle pre_release version
@@ -130,7 +183,7 @@ async def get_data(uri, silent=False):
print(f"FETCH DATA from: {uri}", end="")
if uri.startswith("http"):
async with aiohttp.ClientSession(trust_env=True, connector=aiohttp.TCPConnector(verify_ssl=False)) as session:
async with aiohttp.ClientSession(trust_env=True, connector=aiohttp.TCPConnector(verify_ssl=not bypass_ssl)) as session:
headers = {
'Cache-Control': 'no-cache',
'Pragma': 'no-cache',
@@ -250,7 +303,7 @@ def get_installed_packages(renew=False):
pip_map[normalized_name] = y[1]
except subprocess.CalledProcessError:
logging.error("[ComfyUI-Manager] Failed to retrieve the information of installed pip packages.")
return set()
return {}
return pip_map
@@ -301,6 +354,7 @@ def parse_requirement_line(line):
torch_torchvision_torchaudio_version_map = {
'2.7.0': ('0.22.0', '2.7.0'),
'2.6.0': ('0.21.0', '2.6.0'),
'2.5.1': ('0.20.0', '2.5.0'),
'2.5.0': ('0.20.0', '2.5.0'),
@@ -319,6 +373,32 @@ torch_torchvision_torchaudio_version_map = {
}
def torch_rollback(prev):
spec = prev.split('+')
if len(spec) > 1:
platform = spec[1]
else:
cmd = make_pip_cmd(['install', '--force', 'torch', 'torchvision', 'torchaudio'])
subprocess.check_output(cmd, universal_newlines=True)
logging.error(cmd)
return
torch_ver = StrictVersion(spec[0])
torch_ver = f"{torch_ver.major}.{torch_ver.minor}.{torch_ver.patch}"
torch_torchvision_torchaudio_ver = torch_torchvision_torchaudio_version_map.get(torch_ver)
if torch_torchvision_torchaudio_ver is None:
cmd = make_pip_cmd(['install', '--pre', 'torch', 'torchvision', 'torchaudio',
'--index-url', f"https://download.pytorch.org/whl/nightly/{platform}"])
logging.info("[ComfyUI-Manager] restore PyTorch to nightly version")
else:
torchvision_ver, torchaudio_ver = torch_torchvision_torchaudio_ver
cmd = make_pip_cmd(['install', f'torch=={torch_ver}', f'torchvision=={torchvision_ver}', f"torchaudio=={torchaudio_ver}",
'--index-url', f"https://download.pytorch.org/whl/{platform}"])
logging.info(f"[ComfyUI-Manager] restore PyTorch to {torch_ver}+{platform}")
subprocess.check_output(cmd, universal_newlines=True)
class PIPFixer:
def __init__(self, prev_pip_versions, comfyui_path, manager_files_path):
@@ -326,32 +406,6 @@ class PIPFixer:
self.comfyui_path = comfyui_path
self.manager_files_path = manager_files_path
def torch_rollback(self):
spec = self.prev_pip_versions['torch'].split('+')
if len(spec) > 0:
platform = spec[1]
else:
cmd = make_pip_cmd(['install', '--force', 'torch', 'torchvision', 'torchaudio'])
subprocess.check_output(cmd, universal_newlines=True)
logging.error(cmd)
return
torch_ver = StrictVersion(spec[0])
torch_ver = f"{torch_ver.major}.{torch_ver.minor}.{torch_ver.patch}"
torch_torchvision_torchaudio_ver = torch_torchvision_torchaudio_version_map.get(torch_ver)
if torch_torchvision_torchaudio_ver is None:
cmd = make_pip_cmd(['install', '--pre', 'torch', 'torchvision', 'torchaudio',
'--index-url', f"https://download.pytorch.org/whl/nightly/{platform}"])
logging.info("[ComfyUI-Manager] restore PyTorch to nightly version")
else:
torchvision_ver, torchaudio_ver = torch_torchvision_torchaudio_ver
cmd = make_pip_cmd(['install', f'torch=={torch_ver}', f'torchvision=={torchvision_ver}', f"torchaudio=={torchaudio_ver}",
'--index-url', f"https://download.pytorch.org/whl/{platform}"])
logging.info(f"[ComfyUI-Manager] restore PyTorch to {torch_ver}+{platform}")
subprocess.check_output(cmd, universal_newlines=True)
def fix_broken(self):
new_pip_versions = get_installed_packages(True)
@@ -373,7 +427,7 @@ class PIPFixer:
elif self.prev_pip_versions['torch'] != new_pip_versions['torch'] \
or self.prev_pip_versions['torchvision'] != new_pip_versions['torchvision'] \
or self.prev_pip_versions['torchaudio'] != new_pip_versions['torchaudio']:
self.torch_rollback()
torch_rollback(self.prev_pip_versions['torch'])
except Exception as e:
logging.error("[ComfyUI-Manager] Failed to restore PyTorch")
logging.error(e)
@@ -404,7 +458,7 @@ class PIPFixer:
if len(targets) > 0:
for x in targets:
cmd = make_pip_cmd(['install', f"{x}=={versions[0].version_string}", "numpy<2"])
cmd = make_pip_cmd(['install', f"{x}=={versions[0].version_string}"])
subprocess.check_output(cmd, universal_newlines=True)
logging.info(f"[ComfyUI-Manager] 'opencv' dependencies were fixed: {targets}")
@@ -412,19 +466,6 @@ class PIPFixer:
logging.error("[ComfyUI-Manager] Failed to restore opencv")
logging.error(e)
# fix numpy
try:
np = new_pip_versions.get('numpy')
if np is not None:
if StrictVersion(np) >= StrictVersion('2'):
cmd = make_pip_cmd(['install', "numpy<2"])
subprocess.check_output(cmd , universal_newlines=True)
logging.info("[ComfyUI-Manager] 'numpy' dependency were fixed")
except Exception as e:
logging.error("[ComfyUI-Manager] Failed to restore numpy")
logging.error(e)
# fix missing frontend
try:
# NOTE: package name in requirements is 'comfyui-frontend-package'
@@ -439,10 +480,12 @@ class PIPFixer:
lines = file.readlines()
front_line = next((line.strip() for line in lines if line.startswith('comfyui-frontend-package')), None)
cmd = make_pip_cmd(['install', front_line])
subprocess.check_output(cmd , universal_newlines=True)
logging.info("[ComfyUI-Manager] 'comfyui-frontend-package' dependency were fixed")
if front_line is None:
logging.info("[ComfyUI-Manager] Skipped fixing the 'comfyui-frontend-package' dependency because the ComfyUI is outdated.")
else:
cmd = make_pip_cmd(['install', front_line])
subprocess.check_output(cmd , universal_newlines=True)
logging.info("[ComfyUI-Manager] 'comfyui-frontend-package' dependency were fixed")
except Exception as e:
logging.error("[ComfyUI-Manager] Failed to restore comfyui-frontend-package")
logging.error(e)
@@ -461,7 +504,7 @@ class PIPFixer:
normalized_name = parsed['package'].lower().replace('-', '_')
if normalized_name in new_pip_versions:
if 'version' in parsed and 'operator' in parsed:
cur = StrictVersion(new_pip_versions[parsed['package']])
cur = StrictVersion(new_pip_versions[normalized_name])
dest = parsed['version']
op = parsed['operator']
if cur == dest:
@@ -522,3 +565,69 @@ def robust_readlines(fullpath):
print(f"[ComfyUI-Manager] Failed to recognize encoding for: {fullpath}")
return []
def restore_pip_snapshot(pips, options):
non_url = []
local_url = []
non_local_url = []
for k, v in pips.items():
# NOTE: skip torch related packages
if k.startswith("torch==") or k.startswith("torchvision==") or k.startswith("torchaudio==") or k.startswith("nvidia-"):
continue
if v == "":
non_url.append(k)
else:
if v.startswith('file:'):
local_url.append(v)
else:
non_local_url.append(v)
# restore other pips
failed = []
if '--pip-non-url' in options:
# try all at once
res = 1
try:
res = subprocess.check_output(make_pip_cmd(['install'] + non_url))
except Exception:
pass
# fallback
if res != 0:
for x in non_url:
res = 1
try:
res = subprocess.check_output(make_pip_cmd(['install', '--no-deps', x]))
except Exception:
pass
if res != 0:
failed.append(x)
if '--pip-non-local-url' in options:
for x in non_local_url:
res = 1
try:
res = subprocess.check_output(make_pip_cmd(['install', '--no-deps', x]))
except Exception:
pass
if res != 0:
failed.append(x)
if '--pip-local-url' in options:
for x in local_url:
res = 1
try:
res = subprocess.check_output(make_pip_cmd(['install', '--no-deps', x]))
except Exception:
pass
if res != 0:
failed.append(x)
print(f"Installation failed for pip packages: {failed}")
+69 -16
View File
@@ -2,6 +2,8 @@ import sys
import subprocess
import os
import manager_util
def security_check():
print("[START] Security scan")
@@ -51,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
"""
}
@@ -58,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 = {
@@ -66,18 +105,23 @@ https://blog.comfy.org/comfyui-statement-on-the-ultralytics-crypto-miner-situati
"lolMiner": [os.path.join(comfyui_path, 'lolMiner')]
}
installed_pips = subprocess.check_output([sys.executable, '-m', "pip", "freeze"], text=True)
installed_pips = subprocess.check_output(manager_util.make_pip_cmd(["freeze"]), text=True)
detected = set()
try:
anthropic_info = subprocess.check_output([sys.executable, '-m', "pip", "show", "anthropic"], text=True, stderr=subprocess.DEVNULL)
anthropic_reqs = [x for x in anthropic_info.split('\n') if x.startswith("Requires")][0].split(': ')[1]
if "pycrypto" in anthropic_reqs:
location = [x for x in anthropic_info.split('\n') if x.startswith("Location")][0].split(': ')[1]
for fi in os.listdir(location):
if fi.startswith("anthropic"):
guide["ComfyUI_LLMVISION"] = f"\n0.Remove {os.path.join(location, fi)}" + guide["ComfyUI_LLMVISION"]
detected.add("ComfyUI_LLMVISION")
anthropic_info = subprocess.check_output(manager_util.make_pip_cmd(["show", "anthropic"]), text=True, stderr=subprocess.DEVNULL)
requires_lines = [x for x in anthropic_info.split('\n') if x.startswith("Requires")]
if requires_lines:
anthropic_reqs = requires_lines[0].split(": ", 1)[1]
if "pycrypto" in anthropic_reqs:
location_lines = [x for x in anthropic_info.split('\n') if x.startswith("Location")]
if location_lines:
location = location_lines[0].split(": ", 1)[1]
for fi in os.listdir(location):
if fi.startswith("anthropic"):
guide["ComfyUI_LLMVISION"] = (f"\n0.Remove {os.path.join(location, fi)}" + guide["ComfyUI_LLMVISION"])
detected.add("ComfyUI_LLMVISION")
except subprocess.CalledProcessError:
pass
@@ -86,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:
@@ -98,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.")
+63 -14
View File
@@ -335,8 +335,7 @@ async def share_art(request):
content_type = assetFileType
try:
from matrix_client.api import MatrixHttpApi
from matrix_client.client import MatrixClient
from nio import AsyncClient, LoginResponse, UploadResponse
homeserver = 'matrix.org'
if matrix_auth:
@@ -345,20 +344,35 @@ async def share_art(request):
if not homeserver.startswith("https://"):
homeserver = "https://" + homeserver
client = MatrixClient(homeserver)
try:
token = client.login(username=matrix_auth['username'], password=matrix_auth['password'])
if not token:
return web.json_response({"error": "Invalid Matrix credentials."}, content_type='application/json', status=400)
except:
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)
matrix = MatrixHttpApi(homeserver, token=token)
# Upload asset
with open(asset_filepath, 'rb') as f:
mxc_url = matrix.media_upload(f.read(), content_type, filename=filename)['content_uri']
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
workflow_json_mxc_url = matrix.media_upload(prompt['workflow'], 'application/json', filename='workflow.json')['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"
@@ -366,9 +380,44 @@ async def share_art(request):
text_content += f"{description}\n"
if credits:
text_content += f"\ncredits: {credits}\n"
matrix.send_message(comfyui_share_room_id, text_content)
matrix.send_content(comfyui_share_room_id, mxc_url, filename, 'm.image')
matrix.send_content(comfyui_share_room_id, workflow_json_mxc_url, 'workflow.json', 'm.file')
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()
+52
View File
@@ -0,0 +1,52 @@
# ComfyUI-Manager: Frontend (js)
This directory contains the JavaScript frontend implementation for ComfyUI-Manager, providing the user interface components that interact with the backend API.
## Core Components
- **comfyui-manager.js**: Main entry point that initializes the manager UI and integrates with ComfyUI.
- **custom-nodes-manager.js**: Implements the UI for browsing, installing, and managing custom nodes.
- **model-manager.js**: Handles the model management interface for downloading and organizing AI models.
- **components-manager.js**: Manages reusable workflow components system.
- **snapshot.js**: Implements the snapshot system for backing up and restoring installations.
- **node-usage-analyzer.js**: Implements the UI for analyzing node usage in workflows.
## Sharing Components
- **comfyui-share-common.js**: Base functionality for workflow sharing features.
- **comfyui-share-copus.js**: Integration with the ComfyUI Copus sharing platform.
- **comfyui-share-openart.js**: Integration with the OpenArt sharing platform.
- **comfyui-share-youml.js**: Integration with the YouML sharing platform.
## Utility Components
- **cm-api.js**: Client-side API wrapper for communication with the backend.
- **common.js**: Shared utilities and helper functions used across the frontend.
- **node_fixer.js**: Utilities for fixing disconnected links and repairing malformed nodes by recreating them while preserving connections.
- **popover-helper.js**: UI component for popup tooltips and contextual information.
- **turbogrid.esm.js**: Grid component library - https://github.com/cenfun/turbogrid
- **workflow-metadata.js**: Handles workflow metadata parsing, validation and cross-repository compatibility including versioning, dependencies tracking, and resource management.
## Architecture
The frontend follows a modular component-based architecture:
1. **Integration Layer**: Connects with ComfyUI's existing UI system
2. **Manager Components**: Individual functional UI components (node manager, model manager, etc.)
3. **Sharing Components**: Platform-specific sharing implementations
4. **Utility Layer**: Reusable UI components and helpers
## Implementation Details
- The frontend integrates directly with ComfyUI's UI system through `app.js`
- Dialog-based UI for most manager functions to avoid cluttering the main interface
- Asynchronous API calls to handle backend operations without blocking the UI
## Styling
CSS files are included for specific components:
- **custom-nodes-manager.css**: Styling for the node management UI
- **model-manager.css**: Styling for the model management UI
- **node-usage-analyzer.css**: Styling for the node usage analyzer UI
This frontend implementation provides a comprehensive yet user-friendly interface for managing the ComfyUI ecosystem.
+4 -4
View File
@@ -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';
@@ -42,7 +42,7 @@ async function tryInstallCustomNode(event) {
});
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("/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;
}
+227
View File
@@ -0,0 +1,227 @@
import { $el } from "../../scripts/ui.js";
function normalizeContent(content) {
const tmp = document.createElement('div');
if (typeof content === 'string') {
tmp.innerHTML = content;
return Array.from(tmp.childNodes);
}
if (content instanceof Node) {
return content;
}
return content;
}
export function createSettingsCombo(label, content) {
const settingItem = $el("div.setting-item", {}, [
$el("div.flex.flex-row.items-center.gap-2",[
$el("div.form-label.flex.grow.items-center", [
$el("span.text-muted", { textContent: label },)
]),
$el("div.form-input.flex.justify-end",
[content]
)
]
)
]);
return settingItem;
}
export function buildGuiFrame(dialogId, title, iconClass, content, owner) {
const dialog_mask = $el("div.p-dialog-mask.p-overlay-mask.p-overlay-mask-enter", {
parent: document.body,
style: {
position: "fixed",
height: "100%",
width: "100%",
left: "0px",
top: "0px",
display: "flex",
justifyContent: "center",
alignItems: "center",
pointerEvents: "auto",
zIndex: "1000"
},
onclick: (e) => {
if (e.target === dialog_mask) {
owner.close();
}
}
// data-pc-section="mask"
});
const header_actions = $el("div.p-dialog-header-actions", {
// [TODO]
// data-pc-section="headeractions"
}
);
const close_button = $el("button.p-button.p-component.p-button-icon-only.p-button-secondary.p-button-rounded.p-button-text.p-dialog-close-button", {
parent: header_actions,
type: "button",
ariaLabel: "Close",
onclick: () => owner.close(),
// "data-pc-name": "pcclosebutton",
// "data-p-disabled": "false",
// "data-p-severity": "secondary",
// "data-pc-group-section": "headericon",
// "data-pc-extend": "button",
// "data-pc-section": "root",
// [FIXME] Not sure how to do most of the SVG using $el
innerHTML: '<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg" class="p-icon p-button-icon" aria-hidden="true"><path d="M8.01186 7.00933L12.27 2.75116C12.341 2.68501 12.398 2.60524 12.4375 2.51661C12.4769 2.42798 12.4982 2.3323 12.4999 2.23529C12.5016 2.13827 12.4838 2.0419 12.4474 1.95194C12.4111 1.86197 12.357 1.78024 12.2884 1.71163C12.2198 1.64302 12.138 1.58893 12.0481 1.55259C11.9581 1.51625 11.8617 1.4984 11.7647 1.50011C11.6677 1.50182 11.572 1.52306 11.4834 1.56255C11.3948 1.60204 11.315 1.65898 11.2488 1.72997L6.99067 5.98814L2.7325 1.72997C2.59553 1.60234 2.41437 1.53286 2.22718 1.53616C2.03999 1.53946 1.8614 1.61529 1.72901 1.74767C1.59663 1.88006 1.5208 2.05865 1.5175 2.24584C1.5142 2.43303 1.58368 2.61419 1.71131 2.75116L5.96948 7.00933L1.71131 11.2675C1.576 11.403 1.5 11.5866 1.5 11.7781C1.5 11.9696 1.576 12.1532 1.71131 12.2887C1.84679 12.424 2.03043 12.5 2.2219 12.5C2.41338 12.5 2.59702 12.424 2.7325 12.2887L6.99067 8.03052L11.2488 12.2887C11.3843 12.424 11.568 12.5 11.7594 12.5C11.9509 12.5 12.1346 12.424 12.27 12.2887C12.4053 12.1532 12.4813 11.9696 12.4813 11.7781C12.4813 11.5866 12.4053 11.403 12.27 11.2675L8.01186 7.00933Z" fill="currentColor"></path></svg><span class="p-button-label" data-pc-section="label">&nbsp;</span><!---->'
}
);
const dialog_header = $el("div.p-dialog-header",
[
$el("div", [
$el("div",
{
id: "frame-title-container",
},
[
$el("h2.px-4", [
$el(iconClass, {
style: {
"font-size": "1.25rem",
"margin-right": ".5rem"
}
}),
$el("span", { textContent: title })
])
]
)
]),
header_actions
]
);
const contentFrame = $el("div.p-dialog-content", {}, normalizeContent(content));
const manager_dialog = $el("div.p-dialog.p-component.global-dialog", {
id: dialogId,
parent: dialog_mask,
style: {
'display': 'flex',
'flex-direction': 'column',
'pointer-events': 'auto',
'margin': '0px',
},
role: 'dialog',
ariaModal: 'true',
// [TODO]
// ariaLabbelledby: 'cm-title',
// maximized: 'false',
// data-pc-name: 'dialog',
// data-pc-section: 'root',
// data-pd-focustrap: 'true'
},
[ dialog_header, contentFrame ]
);
const hidden_accessible = $el("span.p-hidden-accessible.p-hidden-focusable", {
parent: manager_dialog,
tabindex: "0",
role: "presentation",
ariaHidden: "true",
"data-p-hidden-accessible": "true",
"data-p-hidden-focusable": "true",
"data-pc-section": "firstfocusableelement"
});
return dialog_mask;
}
export function buildGuiFrameCustomHeader(dialogId, customHeader, content, owner) {
const dialog_mask = $el("div.p-dialog-mask.p-overlay-mask.p-overlay-mask-enter", {
parent: document.body,
style: {
position: "fixed",
height: "100%",
width: "100%",
left: "0px",
top: "0px",
display: "flex",
justifyContent: "center",
alignItems: "center",
pointerEvents: "auto",
zIndex: "1000"
},
onclick: (e) => {
if (e.target === dialog_mask) {
owner.close();
}
}
// data-pc-section="mask"
});
const header_actions = $el("div.p-dialog-header-actions", {
// [TODO]
// data-pc-section="headeractions"
}
);
const close_button = $el("button.p-button.p-component.p-button-icon-only.p-button-secondary.p-button-rounded.p-button-text.p-dialog-close-button", {
parent: header_actions,
type: "button",
ariaLabel: "Close",
onclick: () => owner.close(),
// "data-pc-name": "pcclosebutton",
// "data-p-disabled": "false",
// "data-p-severity": "secondary",
// "data-pc-group-section": "headericon",
// "data-pc-extend": "button",
// "data-pc-section": "root",
// [FIXME] Not sure how to do most of the SVG using $el
innerHTML: '<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg" class="p-icon p-button-icon" aria-hidden="true"><path d="M8.01186 7.00933L12.27 2.75116C12.341 2.68501 12.398 2.60524 12.4375 2.51661C12.4769 2.42798 12.4982 2.3323 12.4999 2.23529C12.5016 2.13827 12.4838 2.0419 12.4474 1.95194C12.4111 1.86197 12.357 1.78024 12.2884 1.71163C12.2198 1.64302 12.138 1.58893 12.0481 1.55259C11.9581 1.51625 11.8617 1.4984 11.7647 1.50011C11.6677 1.50182 11.572 1.52306 11.4834 1.56255C11.3948 1.60204 11.315 1.65898 11.2488 1.72997L6.99067 5.98814L2.7325 1.72997C2.59553 1.60234 2.41437 1.53286 2.22718 1.53616C2.03999 1.53946 1.8614 1.61529 1.72901 1.74767C1.59663 1.88006 1.5208 2.05865 1.5175 2.24584C1.5142 2.43303 1.58368 2.61419 1.71131 2.75116L5.96948 7.00933L1.71131 11.2675C1.576 11.403 1.5 11.5866 1.5 11.7781C1.5 11.9696 1.576 12.1532 1.71131 12.2887C1.84679 12.424 2.03043 12.5 2.2219 12.5C2.41338 12.5 2.59702 12.424 2.7325 12.2887L6.99067 8.03052L11.2488 12.2887C11.3843 12.424 11.568 12.5 11.7594 12.5C11.9509 12.5 12.1346 12.424 12.27 12.2887C12.4053 12.1532 12.4813 11.9696 12.4813 11.7781C12.4813 11.5866 12.4053 11.403 12.27 11.2675L8.01186 7.00933Z" fill="currentColor"></path></svg><span class="p-button-label" data-pc-section="label">&nbsp;</span><!---->'
}
);
const _customHeader = normalizeContent(customHeader);
const dialog_header = $el("div.p-dialog-header",
[
$el("div", [
$el("div",
{
id: "frame-title-container",
},
Array.isArray(_customHeader) ? _customHeader : [_customHeader]
)
]),
header_actions
]
);
const contentFrame = $el("div.p-dialog-content", {}, normalizeContent(content));
const manager_dialog = $el("div.p-dialog.p-component.global-dialog", {
id: dialogId,
parent: dialog_mask,
style: {
'display': 'flex',
'flex-direction': 'column',
'pointer-events': 'auto',
'margin': '0px',
},
role: 'dialog',
ariaModal: 'true',
// [TODO]
// ariaLabbelledby: 'cm-title',
// maximized: 'false',
// data-pc-name: 'dialog',
// data-pc-section: 'root',
// data-pd-focustrap: 'true'
},
[ dialog_header, contentFrame ]
);
const hidden_accessible = $el("span.p-hidden-accessible.p-hidden-focusable", {
parent: manager_dialog,
tabindex: "0",
role: "presentation",
ariaHidden: "true",
"data-p-hidden-accessible": "true",
"data-p-hidden-focusable": "true",
"data-pc-section": "firstfocusableelement"
});
return dialog_mask;
}
+255 -161
View File
@@ -13,13 +13,15 @@ import {
import { OpenArtShareDialog } from "./comfyui-share-openart.js";
import {
free_models, install_pip, install_via_git_url, manager_instance,
rebootAPI, migrateAPI, setManagerInstance, show_message, customAlert, customPrompt,
infoToast, showTerminal, setNeedRestart
rebootAPI, setManagerInstance, show_message, customAlert, customPrompt,
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 { NodeUsageAnalyzer } from "./node-usage-analyzer.js";
import { ModelManager } from "./model-manager.js";
import { SnapshotManager } from "./snapshot.js";
import { buildGuiFrame, createSettingsCombo } from "./comfyui-gui-builder.js";
let manager_version = await getVersion();
@@ -44,12 +46,16 @@ docStyle.innerHTML = `
#cm-manager-dialog {
width: 1000px;
height: 455px;
height: auto;
box-sizing: content-box;
z-index: 1000;
overflow-y: auto;
}
#cm-manager-dialog br {
margin-bottom: 1em;
}
.cb-widget {
width: 400px;
height: 25px;
@@ -80,6 +86,7 @@ docStyle.innerHTML = `
}
.cm-menu-container {
padding : calc(var(--spacing)*2);
column-gap: 20px;
display: flex;
flex-wrap: wrap;
@@ -140,8 +147,8 @@ docStyle.innerHTML = `
}
.cm-notice-board {
width: 290px;
height: 230px;
width: auto;
height: 280px;
overflow: auto;
color: var(--input-text);
border: 1px solid var(--descrip-text);
@@ -238,68 +245,50 @@ var is_updating = false;
// copied style from https://github.com/pythongosssss/ComfyUI-Custom-Scripts
const style = `
#workflowgallery-button {
width: 310px;
height: 27px;
height: 50px;
padding: 0px !important;
position: relative;
overflow: hidden;
font-size: 17px !important;
}
#cm-nodeinfo-button {
width: 310px;
height: 27px;
padding: 0px !important;
position: relative;
overflow: hidden;
font-size: 17px !important;
}
#cm-manual-button {
width: 310px;
height: 27px;
position: relative;
overflow: hidden;
}
.cm-button {
width: 310px;
height: 30px;
width: auto;
position: relative;
overflow: hidden;
font-size: 17px !important;
background-color: var(--comfy-menu-secondary-bg);
border-color: var(--border-color);
color: var(--input-text);
}
.cm-button:hover {
filter: brightness(125%);
}
.cm-button-red {
width: 310px;
height: 30px;
position: relative;
overflow: hidden;
font-size: 17px !important;
background-color: #500000 !important;
border-color: #88181b !important;
color: white !important;
}
.cm-button-red:hover {
background-color: #88181b !important;
}
.cm-button-orange {
width: 310px;
height: 30px;
position: relative;
overflow: hidden;
font-size: 17px !important;
font-weight: bold;
background-color: orange !important;
color: black !important;
}
.cm-experimental-button {
width: 290px;
height: 30px;
position: relative;
overflow: hidden;
font-size: 17px !important;
width: 100%;
}
.cm-experimental {
width: 310px;
border: 1px solid #555;
border-radius: 5px;
padding: 10px;
@@ -326,8 +315,14 @@ const style = `
.cm-menu-combo {
cursor: pointer;
width: 310px;
box-sizing: border-box;
padding: 0.5em 0.5em;
border: 1px solid var(--border-color);
border-radius: 6px;
background: var(--comfy-menu-secondary-bg);
}
.cm-menu-combo:hover {
filter: brightness(125%);
}
.cm-small-button {
@@ -476,12 +471,12 @@ async function updateComfyUI() {
set_inprogress_mode();
const response = await api.fetchApi('/manager/queue/update_comfyui');
const response = await api.fetchApi('/manager/queue/update_comfyui', { method: 'POST' });
showTerminal();
is_updating = true;
await api.fetchApi('/manager/queue/start');
await api.fetchApi('/manager/queue/start', { method: 'POST' });
}
function showVersionSelectorDialog(versions, current, onSelect) {
@@ -631,14 +626,14 @@ async function switchComfyUI() {
showVersionSelectorDialog(versions, obj.current, async (selected_version) => {
if(selected_version == 'nightly') {
update_policy_combo.value = 'nightly-comfyui';
api.fetchApi('/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('/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(`/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}`);
}
@@ -753,9 +748,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();
}
});
}
@@ -775,17 +770,22 @@ async function updateAll(update_comfyui) {
if(update_comfyui) {
update_all_button.innerText = "Updating ComfyUI...";
await api.fetchApi('/manager/queue/update_comfyui');
await api.fetchApi('/manager/queue/update_comfyui', { method: 'POST' });
}
const response = await api.fetchApi(`/manager/queue/update_all?mode=${mode}`);
const response = await api.fetchApi('/manager/queue/update_all', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ mode: mode }) });
if (response.status == 401) {
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');
await api.fetchApi('/manager/queue/start', { method: 'POST' });
}
}
@@ -814,7 +814,7 @@ function restartOrStop() {
rebootAPI();
}
else {
api.fetchApi('/manager/queue/reset');
api.fetchApi('/manager/queue/reset', { method: 'POST' });
infoToast('Cancel', 'Remaining tasks will stop after completing the current task.');
}
}
@@ -826,7 +826,7 @@ class ManagerMenuDialog extends ComfyDialog {
const isElectron = 'electronAPI' in window;
update_comfyui_button =
$el("button.cm-button", {
$el("button.p-button.p-component.cm-button", {
type: "button",
textContent: "Update ComfyUI",
style: {
@@ -837,7 +837,7 @@ class ManagerMenuDialog extends ComfyDialog {
});
switch_comfyui_button =
$el("button.cm-button", {
$el("button.p-button.p-component.cm-button", {
type: "button",
textContent: "Switch ComfyUI",
style: {
@@ -848,7 +848,7 @@ class ManagerMenuDialog extends ComfyDialog {
});
restart_stop_button =
$el("button.cm-button-red", {
$el("button.p-button.p-component.cm-button-red", {
type: "button",
textContent: "Restart",
onclick: () => restartOrStop()
@@ -856,7 +856,7 @@ class ManagerMenuDialog extends ComfyDialog {
if(isElectron) {
update_all_button =
$el("button.cm-button", {
$el("button.p-button.p-component.cm-button", {
type: "button",
textContent: "Update All Custom Nodes",
onclick:
@@ -865,7 +865,7 @@ class ManagerMenuDialog extends ComfyDialog {
}
else {
update_all_button =
$el("button.cm-button", {
$el("button.p-button.p-component.cm-button", {
type: "button",
textContent: "Update All",
onclick:
@@ -875,7 +875,7 @@ class ManagerMenuDialog extends ComfyDialog {
const res =
[
$el("button.cm-button", {
$el("button.p-button.p-component.cm-button", {
type: "button",
textContent: "Custom Nodes Manager",
onclick:
@@ -887,7 +887,7 @@ class ManagerMenuDialog extends ComfyDialog {
}
}),
$el("button.cm-button", {
$el("button.p-button.p-component.cm-button", {
type: "button",
textContent: "Install Missing Custom Nodes",
onclick:
@@ -899,7 +899,7 @@ class ManagerMenuDialog extends ComfyDialog {
}
}),
$el("button.cm-button", {
$el("button.p-button.p-component.cm-button", {
type: "button",
textContent: "Custom Nodes In Workflow",
onclick:
@@ -910,9 +910,20 @@ class ManagerMenuDialog extends ComfyDialog {
CustomNodesManager.instance.show(CustomNodesManager.ShowMode.IN_WORKFLOW);
}
}),
$el("br", {}, []),
$el("button.cm-button", {
type: "button",
textContent: "Node Usage Analyzer",
onclick:
() => {
if(!NodeUsageAnalyzer.instance) {
NodeUsageAnalyzer.instance = new NodeUsageAnalyzer(app, self);
}
NodeUsageAnalyzer.instance.show(NodeUsageAnalyzer.SortMode.BY_PACKAGE);
}
}),
$el("div", {}, []),
$el("button.p-button.p-component.cm-button", {
type: "button",
textContent: "Model Manager",
onclick:
@@ -924,7 +935,7 @@ class ManagerMenuDialog extends ComfyDialog {
}
}),
$el("button.cm-button", {
$el("button.p-button.p-component.cm-button", {
type: "button",
textContent: "Install via Git URL",
onclick: async () => {
@@ -936,38 +947,16 @@ class ManagerMenuDialog extends ComfyDialog {
}
}),
$el("br", {}, []),
$el("div", {}, []),
update_all_button,
update_comfyui_button,
switch_comfyui_button,
// fetch_updates_button,
$el("br", {}, []),
$el("div", {}, []),
restart_stop_button,
];
let migration_btn =
$el("button.cm-button-orange", {
type: "button",
textContent: "Migrate to New Node System",
onclick: () => migrateAPI()
});
migration_btn.style.display = 'none';
res.push(migration_btn);
api.fetchApi('/manager/need_to_migrate')
.then(response => response.text())
.then(text => {
if (text === 'True') {
migration_btn.style.display = 'block';
}
})
.catch(error => {
console.error('Error checking migration status:', error);
});
return res;
}
@@ -977,42 +966,126 @@ class ManagerMenuDialog extends ComfyDialog {
let self = this;
// db mode
this.datasrc_combo = document.createElement("select");
this.datasrc_combo.setAttribute("title", "Configure where to retrieve node/model information. If set to 'local,' the channel is ignored, and if set to 'channel (remote),' it fetches the latest information each time the list is opened.");
this.datasrc_combo.className = "cm-menu-combo";
this.datasrc_combo.appendChild($el('option', { value: 'cache', text: 'DB: Channel (1day cache)' }, []));
this.datasrc_combo.appendChild($el('option', { value: 'local', text: 'DB: Local' }, []));
this.datasrc_combo.appendChild($el('option', { value: 'remote', text: 'DB: Channel (remote)' }, []));
this.datasrc_combo.className = "cm-menu-combo p-select p-component p-inputwrapper p-inputwrapper-filled ";
this.datasrc_combo.appendChild($el('option', { value: 'cache', text: 'Channel (1day cache)' }, []));
this.datasrc_combo.appendChild($el('option', { value: 'local', text: 'Local' }, []));
this.datasrc_combo.appendChild($el('option', { value: 'remote', text: 'Channel (remote)' }, []));
api.fetchApi('/manager/db_mode')
.then(response => response.text())
.then(data => { this.datasrc_combo.value = data; });
this.datasrc_combo.addEventListener('change', function (event) {
api.fetchApi(`/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";
preview_combo.appendChild($el('option', { value: 'auto', text: 'Preview method: Auto' }, []));
preview_combo.appendChild($el('option', { value: 'taesd', text: 'Preview method: TAESD (slow)' }, []));
preview_combo.appendChild($el('option', { value: 'latent2rgb', text: 'Preview method: Latent2RGB (fast)' }, []));
preview_combo.appendChild($el('option', { value: 'none', text: 'Preview method: None (very fast)' }, []));
preview_combo.className = "cm-menu-combo p-select p-component p-inputwrapper p-inputwrapper-filled";
// Loading state to prevent flash of enabled state
preview_combo.appendChild($el('option', { value: '', text: 'Loading...', disabled: true }, []));
preview_combo.appendChild($el('option', { value: 'auto', text: 'Auto' }, []));
preview_combo.appendChild($el('option', { value: 'taesd', text: 'TAESD (slow)' }, []));
preview_combo.appendChild($el('option', { value: 'latent2rgb', text: 'Latent2RGB (fast)' }, []));
preview_combo.appendChild($el('option', { value: 'none', text: 'None (very fast)' }, []));
// Start disabled to prevent flash
preview_combo.disabled = true;
preview_combo.value = '';
// Fetch current state
api.fetchApi('/manager/preview_method')
.then(response => response.text())
.then(data => { preview_combo.value = data; });
.then(data => {
// Remove loading option
preview_combo.querySelector('option[value=""]')?.remove();
if (data === "DISABLED") {
// ComfyUI per-queue preview feature is active
preview_combo.disabled = true;
preview_combo.value = 'auto';
// Accessibility attributes
preview_combo.setAttribute("aria-disabled", "true");
preview_combo.setAttribute("aria-label",
"Preview method setting (disabled - managed by ComfyUI). " +
"Use Settings > Execution > Live preview method instead."
);
// Tooltip for mouse users
preview_combo.setAttribute("title",
"This feature is now provided natively by ComfyUI. " +
"Please use 'Settings > Execution > Live preview method' instead."
);
// Visual feedback
preview_combo.style.opacity = '0.6';
preview_combo.style.cursor = 'not-allowed';
} else {
// Manager feature is active
preview_combo.disabled = false;
preview_combo.value = data;
// Accessibility for enabled state
preview_combo.setAttribute("aria-label",
"Preview method setting. Select how latent variables are decoded during preview."
);
}
})
.catch(error => {
console.error('[ComfyUI-Manager] Failed to fetch preview method status:', error);
// Error recovery: fallback to enabled
preview_combo.querySelector('option[value=""]')?.remove();
preview_combo.disabled = false;
preview_combo.value = 'auto';
});
preview_combo.addEventListener('change', function (event) {
api.fetchApi(`/manager/preview_method?value=${event.target.value}`);
// Ignore if disabled
if (preview_combo.disabled) {
event.preventDefault();
return;
}
// Normal operation
api.fetchApi('/manager/preview_method', { 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";
channel_combo.className = "cm-menu-combo p-select p-component p-inputwrapper p-inputwrapper-filled";
api.fetchApi('/manager/channel_url_list')
.then(response => response.json())
.then(async data => {
@@ -1021,12 +1094,12 @@ class ManagerMenuDialog extends ComfyDialog {
for (let i in urls) {
if (urls[i] != '') {
let name_url = urls[i].split('::');
channel_combo.appendChild($el('option', { value: name_url[0], text: `Channel: ${name_url[0]}` }, []));
channel_combo.appendChild($el('option', { value: name_url[0], text: `${name_url[0]}` }, []));
}
}
channel_combo.addEventListener('change', function (event) {
api.fetchApi(`/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;
@@ -1036,11 +1109,13 @@ class ManagerMenuDialog extends ComfyDialog {
}
});
const channelSetttingItem = createSettingsCombo("Channel", channel_combo);
// share
let share_combo = document.createElement("select");
share_combo.setAttribute("title", "Hide the share button in the main menu or set the default action upon clicking it. Additionally, configure the default share site when sharing via the context menu's share button.");
share_combo.className = "cm-menu-combo";
share_combo.className = "cm-menu-combo p-select p-component p-inputwrapper p-inputwrapper-filled";
const share_options = [
['none', 'None'],
['openart', 'OpenArt AI'],
@@ -1051,7 +1126,7 @@ class ManagerMenuDialog extends ComfyDialog {
['all', 'All'],
];
for (const option of share_options) {
share_combo.appendChild($el('option', { value: option[0], text: `Share: ${option[1]}` }, []));
share_combo.appendChild($el('option', { value: option[0], text: `${option[1]}` }, []));
}
api.fetchApi('/manager/share_option')
@@ -1073,12 +1148,14 @@ class ManagerMenuDialog extends ComfyDialog {
}
});
const shareSetttingItem = createSettingsCombo("Share", share_combo);
let component_policy_combo = document.createElement("select");
component_policy_combo.setAttribute("title", "When loading the workflow, configure which version of the component to use.");
component_policy_combo.className = "cm-menu-combo";
component_policy_combo.appendChild($el('option', { value: 'workflow', text: 'Component: Use workflow version' }, []));
component_policy_combo.appendChild($el('option', { value: 'higher', text: 'Component: Use higher version' }, []));
component_policy_combo.appendChild($el('option', { value: 'mine', text: 'Component: Use my version' }, []));
component_policy_combo.className = "cm-menu-combo p-select p-component p-inputwrapper p-inputwrapper-filled";
component_policy_combo.appendChild($el('option', { value: 'workflow', text: 'Use workflow version' }, []));
component_policy_combo.appendChild($el('option', { value: 'higher', text: 'Use higher version' }, []));
component_policy_combo.appendChild($el('option', { value: 'mine', text: 'Use my version' }, []));
api.fetchApi('/manager/policy/component')
.then(response => response.text())
.then(data => {
@@ -1087,19 +1164,18 @@ class ManagerMenuDialog extends ComfyDialog {
});
component_policy_combo.addEventListener('change', function (event) {
api.fetchApi(`/manager/policy/component?value=${event.target.value}`);
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);
});
update_policy_combo = document.createElement("select");
const componentSetttingItem = createSettingsCombo("Component", component_policy_combo);
if(isElectron)
update_policy_combo.style.display = 'none';
update_policy_combo = document.createElement("select");
update_policy_combo.setAttribute("title", "Sets the policy to be applied when performing an update.");
update_policy_combo.className = "cm-menu-combo";
update_policy_combo.appendChild($el('option', { value: 'stable-comfyui', text: 'Update: ComfyUI Stable Version' }, []));
update_policy_combo.appendChild($el('option', { value: 'nightly-comfyui', text: 'Update: ComfyUI Nightly Version' }, []));
update_policy_combo.className = "cm-menu-combo p-select p-component p-inputwrapper p-inputwrapper-filled";
update_policy_combo.appendChild($el('option', { value: 'stable-comfyui', text: 'ComfyUI Stable Version' }, []));
update_policy_combo.appendChild($el('option', { value: 'nightly-comfyui', text: 'ComfyUI Nightly Version' }, []));
api.fetchApi('/manager/policy/update')
.then(response => response.text())
.then(data => {
@@ -1107,23 +1183,25 @@ class ManagerMenuDialog extends ComfyDialog {
});
update_policy_combo.addEventListener('change', function (event) {
api.fetchApi(`/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 }) });
});
return [
$el("br", {}, []),
this.datasrc_combo,
channel_combo,
preview_combo,
share_combo,
component_policy_combo,
update_policy_combo,
$el("br", {}, []),
const updateSetttingItem = createSettingsCombo("Update", update_policy_combo);
if(isElectron)
updateSetttingItem.style.display = 'none';
$el("br", {}, []),
$el("filedset.cm-experimental", {}, [
return [
dbRetrievalSetttingItem,
channelSetttingItem,
previewSetttingItem,
shareSetttingItem,
componentSetttingItem,
updateSetttingItem,
//[TODO] replace mt-2 with wrapper div with flex column gap
$el("filedset.cm-experimental.mt-auto", {}, [
$el("legend.cm-experimental-legend", {}, ["EXPERIMENTAL"]),
$el("button.cm-experimental-button", {
$el("button.p-button.p-component.cm-button.cm-experimental-button", {
type: "button",
textContent: "Snapshot Manager",
onclick:
@@ -1133,7 +1211,7 @@ class ManagerMenuDialog extends ComfyDialog {
SnapshotManager.instance.show();
}
}),
$el("button.cm-experimental-button", {
$el("button.p-button.p-component.cm-button.cm-experimental-button.mt-2", {
type: "button",
textContent: "Install PIP packages",
onclick:
@@ -1151,7 +1229,7 @@ class ManagerMenuDialog extends ComfyDialog {
createControlsRight() {
const elts = [
$el("button.cm-button", {
$el("button.p-button.p-component.cm-button", {
id: 'cm-manual-button',
type: "button",
textContent: "Community Manual",
@@ -1202,11 +1280,11 @@ class ManagerMenuDialog extends ComfyDialog {
})
]),
$el("button", {
$el("button.p-button.p-component.cm-button", {
id: 'workflowgallery-button',
type: "button",
style: {
...(localStorage.getItem("wg_last_visited") ? {height: '50px'} : {})
// ...(localStorage.getItem("wg_last_visited") ? {height: '50px'} : {})
},
onclick: (e) => {
const last_visited_site = localStorage.getItem("wg_last_visited")
@@ -1229,7 +1307,7 @@ class ManagerMenuDialog extends ComfyDialog {
}, [
$el("p", {
id: 'workflowgallery-button-last-visited-label',
textContent: `(${localStorage.getItem("wg_last_visited") ? localStorage.getItem("wg_last_visited").split('/')[2] : ''})`,
textContent: `(${localStorage.getItem("wg_last_visited") ? localStorage.getItem("wg_last_visited").split('/')[2] : 'none selected'})`,
style: {
'text-align': 'center',
'color': 'var(--input-text)',
@@ -1245,13 +1323,12 @@ class ManagerMenuDialog extends ComfyDialog {
})
]),
$el("button.cm-button", {
$el("button.p-button.p-component.cm-button", {
id: 'cm-nodeinfo-button',
type: "button",
textContent: "Nodes Info",
onclick: () => { window.open("https://ltdrdata.github.io/", "comfyui-node-info"); }
}),
$el("br", {}, []),
];
var textarea = document.createElement("div");
@@ -1266,31 +1343,23 @@ class ManagerMenuDialog extends ComfyDialog {
constructor() {
super();
const close_button = $el("button", { id: "cm-close-button", type: "button", textContent: "Close", onclick: () => this.close() });
const content = $el("div.cm-menu-container",
[
$el("div.cm-menu-column.gap-2", [...this.createControlsLeft()]),
$el("div.cm-menu-column.gap-2", [...this.createControlsMid()]),
$el("div.cm-menu-column.gap-2", [...this.createControlsRight()])
]
);
const content =
$el("div.comfy-modal-content",
[
$el("tr.cm-title", {}, [
$el("font", {size:6, color:"white"}, [`ComfyUI Manager ${manager_version}`])]
),
$el("br", {}, []),
$el("div.cm-menu-container",
[
$el("div.cm-menu-column", [...this.createControlsLeft()]),
$el("div.cm-menu-column", [...this.createControlsMid()]),
$el("div.cm-menu-column", [...this.createControlsRight()])
]),
const frame = buildGuiFrame(
'cm-manager-dialog', // dialog id
`ComfyUI Manager ${manager_version}`, // dialog title
"i.mdi.mdi-puzzle", // dialog icon class to show before title
content, // dialog content element
this
); // send this so we can attach close functions
$el("br", {}, []),
close_button,
]
);
content.style.width = '100%';
content.style.height = '100%';
this.element = $el("div.comfy-modal", { id:'cm-manager-dialog', parent: document.body }, [ content ]);
this.element = frame;
}
get isVisible() {
@@ -1298,7 +1367,7 @@ class ManagerMenuDialog extends ComfyDialog {
}
show() {
this.element.style.display = "block";
this.element.style.display = "flex";
}
toggleVisibility() {
@@ -1475,6 +1544,31 @@ app.registerExtension({
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");
+230 -47
View File
@@ -71,7 +71,7 @@ export class CopusShareDialog extends ComfyDialog {
this.allFiles = [];
this.titleNum = 0;
}
createButtons() {
const inputStyle = {
display: "block",
@@ -201,13 +201,15 @@ export class CopusShareDialog extends ComfyDialog {
});
this.LockInput = $el("input", {
type: "text",
placeholder: "",
style: {
placeholder: "0",
style: {
width: "100px",
padding: "7px",
paddingLeft: "30px",
borderRadius: "4px",
border: "1px solid #ddd",
boxSizing: "border-box",
position: "relative",
},
oninput: (event) => {
let input = event.target.value;
@@ -301,7 +303,7 @@ export class CopusShareDialog extends ComfyDialog {
},
[]
);
const titleNumDom = $el(
"label",
{
@@ -342,15 +344,11 @@ export class CopusShareDialog extends ComfyDialog {
["0/70"]
);
// Additional Inputs Section
const additionalInputsSection = $el(
"div",
{ style: { ...sectionStyle, } },
[
$el("label", { style: labelStyle }, ["3️⃣ Title "]),
this.TitleInput,
titleNumDom,
]
);
const additionalInputsSection = $el("div", { style: { ...sectionStyle } }, [
$el("label", { style: labelStyle }, ["3️⃣ Title "]),
this.TitleInput,
titleNumDom,
]);
const SubtitleSection = $el("div", { style: sectionStyle }, [
$el("label", { style: labelStyle }, ["4️⃣ Subtitle "]),
this.SubTitleInput,
@@ -379,7 +377,7 @@ export class CopusShareDialog extends ComfyDialog {
});
const blockChainSection_lock = $el("div", { style: sectionStyle }, [
$el("label", { style: labelStyle }, ["6️⃣ Pay to download"]),
$el("label", { style: labelStyle }, ["6️⃣ Download threshold"]),
$el(
"label",
{
@@ -392,11 +390,42 @@ export class CopusShareDialog extends ComfyDialog {
},
[
this.radioButtonsCheck_lock,
$el("div", { style: { marginLeft: "5px" ,display:'flex',alignItems:'center'} }, [
$el("span", { style: { marginLeft: "5px" } }, ["ON"]),
$el("span", { style: { marginLeft: "20px",marginRight:'10px' ,color:'#fff'} }, ["Price US$"]),
this.LockInput
]),
$el(
"div",
{
style: {
marginLeft: "5px",
display: "flex",
alignItems: "center",
position: "relative",
},
},
[
$el("span", { style: { marginLeft: "5px" } }, ["ON"]),
$el(
"span",
{
style: {
marginLeft: "20px",
marginRight: "10px",
color: "#fff",
},
},
["Unlock with"]
),
$el("img", {
style: {
width: "16px",
height: "16px",
position: "absolute",
right: "75px",
zIndex: "100",
},
src: "https://static.copus.io/images/admin/202507/prod/e2919a1d8f3c2d99d3b8fe27ff94b841.png",
}),
this.LockInput,
]
),
]
),
$el(
@@ -404,14 +433,25 @@ export class CopusShareDialog extends ComfyDialog {
{ style: { display: "flex", alignItems: "center", cursor: "pointer" } },
[
this.radioButtonsCheckOff_lock,
$el("span", { style: { marginLeft: "5px" } }, ["OFF"]),
$el(
"div",
{
style: {
marginLeft: "5px",
display: "flex",
alignItems: "center",
},
},
[$el("span", { style: { marginLeft: "5px" } }, ["OFF"])]
),
]
),
$el(
"p",
{ style: { fontSize: "16px", color: "#fff", margin: "10px 0 0 0" } },
["Get paid from your workflow. You can change the price and withdraw your earnings on Copus."]
[
]
),
]);
@@ -432,7 +472,7 @@ export class CopusShareDialog extends ComfyDialog {
});
const blockChainSection = $el("div", { style: sectionStyle }, [
$el("label", { style: labelStyle }, ["7️⃣ Store on blockchain "]),
$el("label", { style: labelStyle }, ["8️⃣ Store on blockchain "]),
$el(
"label",
{
@@ -463,6 +503,139 @@ export class CopusShareDialog extends ComfyDialog {
),
]);
this.ratingRadioButtonsCheck0 = $el("input", {
type: "radio",
name: "content_rating",
value: "0",
id: "content_rating0",
});
this.ratingRadioButtonsCheck1 = $el("input", {
type: "radio",
name: "content_rating",
value: "1",
id: "content_rating1",
});
this.ratingRadioButtonsCheck2 = $el("input", {
type: "radio",
name: "content_rating",
value: "2",
id: "content_rating2",
});
this.ratingRadioButtonsCheck_1 = $el("input", {
type: "radio",
name: "content_rating",
value: "-1",
id: "content_rating_1",
checked: true,
});
// content rating
const contentRatingSection = $el("div", { style: sectionStyle }, [
$el("label", { style: labelStyle }, ["7️⃣ Content rating "]),
$el(
"label",
{
style: {
marginTop: "10px",
display: "flex",
alignItems: "center",
cursor: "pointer",
},
},
[
this.ratingRadioButtonsCheck0,
$el("img", {
style: {
width: "12px",
height: "12px",
marginLeft: "5px",
},
src: "https://static.copus.io/images/client/202507/test/b9f17da83b054d53cd0cb4508c2c30dc.png",
}),
$el("span", { style: { marginLeft: "5px", color: "#fff" } }, [
"All ages",
]),
]
),
$el(
"p",
{ style: { fontSize: "10px", color: "#fff", marginLeft: "20px" } },
["Safe for all viewers; no profanity, violence, or mature themes."]
),
$el(
"label",
{ style: { display: "flex", alignItems: "center", cursor: "pointer" } },
[
this.ratingRadioButtonsCheck1,
$el("img", {
style: {
width: "12px",
height: "12px",
marginLeft: "5px",
},
src: "https://static.copus.io/images/client/202507/test/7848bc0d3690671df21c7cf00c4cfc81.png",
}),
$el("span", { style: { marginLeft: "5px", color: "#fff" } }, [
"13+ (Teen)",
]),
]
),
$el(
"p",
{ style: { fontSize: "10px", color: "#fff", marginLeft: "20px" } },
[
"Mild language, light themes, or cartoon violence; no explicit content. ",
]
),
$el(
"label",
{ style: { display: "flex", alignItems: "center", cursor: "pointer" } },
[
this.ratingRadioButtonsCheck2,
$el("img", {
style: {
width: "12px",
height: "12px",
marginLeft: "5px",
},
src: "https://static.copus.io/images/client/202507/test/bc51839c208d68d91173e43c23bff039.png",
}),
$el("span", { style: { marginLeft: "5px", color: "#fff" } }, [
"18+ (Explicit)",
]),
]
),
$el(
"p",
{ style: { fontSize: "10px", color: "#fff", marginLeft: "20px" } },
[
"Explicit content, including sexual content, strong violence, or intense themes. ",
]
),
$el(
"label",
{ style: { display: "flex", alignItems: "center", cursor: "pointer" } },
[
this.ratingRadioButtonsCheck_1,
$el("img", {
style: {
width: "12px",
height: "12px",
marginLeft: "5px",
},
src: "https://static.copus.io/images/client/202507/test/5c802fdcaaea4e7bbed37393eec0d5ba.png",
}),
$el("span", { style: { marginLeft: "5px", color: "#fff" } }, [
"Not Rated",
]),
]
),
$el(
"p",
{ style: { fontSize: "10px", color: "#fff", marginLeft: "20px" } },
["No age rating provided."]
),
]);
// Message Section
this.message = $el(
@@ -526,6 +699,7 @@ export class CopusShareDialog extends ComfyDialog {
DescriptionSection,
// contestSection,
blockChainSection_lock,
contentRatingSection,
blockChainSection,
this.message,
buttonsSection,
@@ -534,7 +708,7 @@ export class CopusShareDialog extends ComfyDialog {
return layout;
}
/**
* api
* api
* @param {url} path
* @param {params} options
* @param {statusText} statusText
@@ -587,7 +761,9 @@ export class CopusShareDialog extends ComfyDialog {
url: data,
});
} else {
throw new Error("make sure your API key is correct and try again later");
throw new Error(
"make sure your API key is correct and try again later"
);
}
} catch (e) {
if (e?.response?.status === 413) {
@@ -628,8 +804,15 @@ export class CopusShareDialog extends ComfyDialog {
subTitle: this.SubTitleInput.value,
content: this.descriptionInput.value,
storeOnChain: this.radioButtonsCheck.checked ? true : false,
lockState:this.radioButtonsCheck_lock.checked ? 2 : 0,
unlockPrice:this.LockInput.value,
lockState: this.radioButtonsCheck_lock.checked ? 2 : 0,
unlockPrice: this.LockInput.value,
rating: this.ratingRadioButtonsCheck0.checked
? 0
: this.ratingRadioButtonsCheck1.checked
? 1
: this.ratingRadioButtonsCheck2.checked
? 2
: -1,
};
if (!this.keyInput.value) {
@@ -644,8 +827,8 @@ export class CopusShareDialog extends ComfyDialog {
throw new Error("Title is required");
}
if(this.radioButtonsCheck_lock.checked){
if (!this.LockInput.value){
if (this.radioButtonsCheck_lock.checked) {
if (!this.LockInput.value) {
throw new Error("Price is required");
}
}
@@ -695,23 +878,23 @@ export class CopusShareDialog extends ComfyDialog {
"Uploading workflow..."
);
if (res.status && res.data.status && res.data) {
localStorage.setItem("copus_token",this.keyInput.value);
const { data } = res.data;
if (data) {
const url = `${DEFAULT_HOMEPAGE_URL}/work/${data}`;
this.message.innerHTML = `Workflow has been shared successfully. <a href="${url}" target="_blank">Click here to view it.</a>`;
this.previewImage.src = "";
this.previewImage.style.display = "none";
this.uploadedImages = [];
this.allFilesImages = [];
this.allFiles = [];
this.TitleInput.value = "";
this.SubTitleInput.value = "";
this.descriptionInput.value = "";
this.selectedFile = null;
}
}
if (res.status && res.data.status && res.data) {
localStorage.setItem("copus_token", this.keyInput.value);
const { data } = res.data;
if (data) {
const url = `${DEFAULT_HOMEPAGE_URL}/work/${data}`;
this.message.innerHTML = `Workflow has been shared successfully. <a href="${url}" target="_blank">Click here to view it.</a>`;
this.previewImage.src = "";
this.previewImage.style.display = "none";
this.uploadedImages = [];
this.allFilesImages = [];
this.allFiles = [];
this.TitleInput.value = "";
this.SubTitleInput.value = "";
this.descriptionInput.value = "";
this.selectedFile = null;
}
}
} catch (e) {
throw new Error("Error sharing workflow: " + e.message);
}
@@ -757,7 +940,7 @@ export class CopusShareDialog extends ComfyDialog {
this.element.style.display = "block";
this.previewImage.src = "";
this.previewImage.style.display = "none";
this.keyInput.value = apiToken!=null?apiToken:"";
this.keyInput.value = apiToken != null ? apiToken : "";
this.uploadedImages = [];
this.allFilesImages = [];
this.allFiles = [];
+653 -39
View File
@@ -1,6 +1,7 @@
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
import { $el, ComfyDialog } from "../../scripts/ui.js";
import { getBestPosition, getPositionStyle, getRect } from './popover-helper.js';
function internalCustomConfirm(message, confirmMessage, cancelMessage) {
@@ -99,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));
}
@@ -108,9 +122,9 @@ export async function customConfirm(message) {
let res = await
window['app'].extensionManager.dialog
.confirm({
title: 'Confirm',
message: message
});
title: 'Confirm',
message: message
});
return res;
}
@@ -150,9 +164,9 @@ export async function customPrompt(title, message) {
let res = await
window['app'].extensionManager.dialog
.prompt({
title: title,
message: message
});
title: title,
message: message
});
return res;
}
@@ -162,36 +176,22 @@ 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("/manager/reboot");
}
catch(exception) {}
}
});
return false;
}
export async function migrateAPI() {
let confirmed = await customConfirm("When performing a migration, existing installed custom nodes will be renamed and the server will be restarted. Are you sure you want to apply this?\n\n(If you don't perform the migration, ComfyUI-Manager's start-up time will be longer each time due to re-checking during startup.)")
if (confirmed) {
const isConfirmed = await customConfirm("Are you sure you'd like to reboot the server?");
if (isConfirmed) {
try {
await api.fetchApi("/manager/migrate_unmanaged_nodes");
api.fetchApi("/manager/reboot");
const response = await api.fetchApi("/manager/reboot", { method: 'POST' });
if (response.status == 403) {
await handle403Response(response);
return false;
}
}
catch(exception) {
}
return true;
catch(exception) {}
}
return false;
@@ -223,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("/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;
}
@@ -263,11 +266,12 @@ export async function install_via_git_url(url, manager_dialog) {
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;
}
@@ -278,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();
}
});
}
@@ -404,12 +408,14 @@ export async function fetchData(route, options) {
}
}
// https://cenfun.github.io/open-icons/
export const icons = {
search: '<svg viewBox="0 0 24 24" width="100%" height="100%" pointer-events="none" xmlns="http://www.w3.org/2000/svg"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m21 21-4.486-4.494M19 10.5a8.5 8.5 0 1 1-17 0 8.5 8.5 0 0 1 17 0"/></svg>',
extensions: '<svg viewBox="64 64 896 896" width="100%" height="100%" pointer-events="none" xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" d="M843.5 737.4c-12.4-75.2-79.2-129.1-155.3-125.4S550.9 676 546 752c-153.5-4.8-208-40.7-199.1-113.7 3.3-27.3 19.8-41.9 50.1-49 18.4-4.3 38.8-4.9 57.3-3.2 1.7.2 3.5.3 5.2.5 11.3 2.7 22.8 5 34.3 6.8 34.1 5.6 68.8 8.4 101.8 6.6 92.8-5 156-45.9 159.2-132.7 3.1-84.1-54.7-143.7-147.9-183.6-29.9-12.8-61.6-22.7-93.3-30.2-14.3-3.4-26.3-5.7-35.2-7.2-7.9-75.9-71.5-133.8-147.8-134.4S189.7 168 180.5 243.8s40 146.3 114.2 163.9 149.9-23.3 175.7-95.1c9.4 1.7 18.7 3.6 28 5.8 28.2 6.6 56.4 15.4 82.4 26.6 70.7 30.2 109.3 70.1 107.5 119.9-1.6 44.6-33.6 65.2-96.2 68.6-27.5 1.5-57.6-.9-87.3-5.8-8.3-1.4-15.9-2.8-22.6-4.3-3.9-.8-6.6-1.5-7.8-1.8l-3.1-.6c-2.2-.3-5.9-.8-10.7-1.3-25-2.3-52.1-1.5-78.5 4.6-55.2 12.9-93.9 47.2-101.1 105.8-15.7 126.2 78.6 184.7 276 188.9 29.1 70.4 106.4 107.9 179.6 87 73.3-20.9 119.3-93.4 106.9-168.6M329.1 345.2a83.3 83.3 0 1 1 .01-166.61 83.3 83.3 0 0 1-.01 166.61M695.6 845a83.3 83.3 0 1 1 .01-166.61A83.3 83.3 0 0 1 695.6 845"/></svg>',
conflicts: '<svg viewBox="0 0 400 400" width="100%" height="100%" pointer-events="none" xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" d="m397.2 350.4.2-.2-180-320-.2.2C213.8 24.2 207.4 20 200 20s-13.8 4.2-17.2 10.4l-.2-.2-180 320 .2.2c-1.6 2.8-2.8 6-2.8 9.6 0 11 9 20 20 20h360c11 0 20-9 20-20 0-3.6-1.2-6.8-2.8-9.6M220 340h-40v-40h40zm0-60h-40V120h40z"/></svg>',
passed: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 426.667 426.667"><path fill="#6AC259" d="M213.333,0C95.518,0,0,95.514,0,213.333s95.518,213.333,213.333,213.333c117.828,0,213.333-95.514,213.333-213.333S331.157,0,213.333,0z M174.199,322.918l-93.935-93.931l31.309-31.309l62.626,62.622l140.894-140.898l31.309,31.309L174.199,322.918z"/></svg>',
download: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" width="100%" height="100%" viewBox="0 0 32 32"><path fill="currentColor" d="M26 24v4H6v-4H4v4a2 2 0 0 0 2 2h20a2 2 0 0 0 2-2v-4zm0-10l-1.41-1.41L17 20.17V2h-2v18.17l-7.59-7.58L6 14l10 10l10-10z"></path></svg>'
download: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" width="100%" height="100%" viewBox="0 0 32 32"><path fill="currentColor" d="M26 24v4H6v-4H4v4a2 2 0 0 0 2 2h20a2 2 0 0 0 2-2v-4zm0-10l-1.41-1.41L17 20.17V2h-2v18.17l-7.59-7.58L6 14l10 10l10-10z"></path></svg>',
close: '<svg xmlns="http://www.w3.org/2000/svg" pointer-events="none" width="100%" height="100%" viewBox="0 0 16 16"><g fill="currentColor"><path fill-rule="evenodd" clip-rule="evenodd" d="m7.116 8-4.558 4.558.884.884L8 8.884l4.558 4.558.884-.884L8.884 8l4.558-4.558-.884-.884L8 7.116 3.442 2.558l-.884.884L7.116 8z"/></g></svg>',
arrowRight: '<svg xmlns="http://www.w3.org/2000/svg" pointer-events="none" width="100%" height="100%" viewBox="0 0 20 20"><path fill="currentColor" fill-rule="evenodd" d="m2.542 2.154 7.254 7.26c.136.14.204.302.204.483a.73.73 0 0 1-.204.5l-7.575 7.398c-.383.317-.724.317-1.022 0-.299-.317-.299-.643 0-.98l7.08-6.918-6.754-6.763c-.237-.343-.215-.654.066-.935.281-.28.598-.295.951-.045Zm9 0 7.254 7.26c.136.14.204.302.204.483a.73.73 0 0 1-.204.5l-7.575 7.398c-.383.317-.724.317-1.022 0-.299-.317-.299-.643 0-.98l7.08-6.918-6.754-6.763c-.237-.343-.215-.654.066-.935.281-.28.598-.295.951-.045Z"/></svg>'
}
export function sanitizeHTML(str) {
@@ -503,3 +509,611 @@ export function restoreColumnWidth(gridId, columns) {
});
}
export function getTimeAgo(dateStr) {
const date = new Date(dateStr);
if (!date || !(date instanceof Date) || isNaN(date.getTime())) {
return "";
}
const units = [
{ max: 2760000, value: 60000, name: 'minute', past: 'a minute ago', future: 'in a minute' },
{ max: 72000000, value: 3600000, name: 'hour', past: 'an hour ago', future: 'in an hour' },
{ max: 518400000, value: 86400000, name: 'day', past: 'yesterday', future: 'tomorrow' },
{ max: 2419200000, value: 604800000, name: 'week', past: 'last week', future: 'in a week' },
{ max: 28512000000, value: 2592000000, name: 'month', past: 'last month', future: 'in a month' }
];
const diff = Date.now() - date.getTime();
// less than a minute
if (Math.abs(diff) < 60000)
return 'just now';
for (let i = 0; i < units.length; i++) {
if (Math.abs(diff) < units[i].max) {
return format(diff, units[i].value, units[i].name, units[i].past, units[i].future, diff < 0);
}
}
function format(diff, divisor, unit, past, future, isInTheFuture) {
const val = Math.round(Math.abs(diff) / divisor);
if (isInTheFuture)
return val <= 1 ? future : 'in ' + val + ' ' + unit + 's';
return val <= 1 ? past : val + ' ' + unit + 's ago';
}
return format(diff, 31536000000, 'year', 'last year', 'in a year', diff < 0);
};
export const loadCss = (cssFile) => {
const cssPath = import.meta.resolve(cssFile);
//console.log(cssPath);
const $link = document.createElement("link");
$link.setAttribute("rel", 'stylesheet');
$link.setAttribute("href", cssPath);
document.head.appendChild($link);
};
export const copyText = (text) => {
return new Promise((resolve) => {
let err;
try {
navigator.clipboard.writeText(text);
} catch (e) {
err = e;
}
if (err) {
resolve(false);
} else {
resolve(true);
}
});
};
function renderPopover($elem, target, options = {}) {
// async microtask
queueMicrotask(() => {
const containerRect = getRect(window);
const targetRect = getRect(target);
const elemRect = getRect($elem);
const positionInfo = getBestPosition(
containerRect,
targetRect,
elemRect,
options.positions
);
const style = getPositionStyle(positionInfo, {
bgColor: options.bgColor,
borderColor: options.borderColor,
borderRadius: options.borderRadius
});
$elem.style.top = positionInfo.top + "px";
$elem.style.left = positionInfo.left + "px";
$elem.style.background = style.background;
});
}
let $popover;
export function hidePopover() {
if ($popover) {
$popover.remove();
$popover = null;
}
}
export function showPopover(target, text, className, options) {
hidePopover();
$popover = document.createElement("div");
$popover.className = ['cn-popover', className].filter(it => it).join(" ");
document.body.appendChild($popover);
$popover.innerHTML = text;
$popover.style.display = "block";
renderPopover($popover, target, {
borderRadius: 10,
... options
});
}
let $tooltip;
export function hideTooltip(target) {
if ($tooltip) {
$tooltip.style.display = "none";
$tooltip.innerHTML = "";
$tooltip.style.top = "0px";
$tooltip.style.left = "0px";
}
}
export function showTooltip(target, text, className = 'cn-tooltip', styleMap = {}) {
if (!$tooltip) {
$tooltip = document.createElement("div");
$tooltip.className = className;
$tooltip.style.cssText = `
pointer-events: none;
position: fixed;
z-index: 10001;
padding: 20px;
color: #1e1e1e;
max-width: 350px;
filter: drop-shadow(1px 5px 5px rgb(0 0 0 / 30%));
${Object.keys(styleMap).map(k=>k+":"+styleMap[k]+";").join("")}
`;
document.body.appendChild($tooltip);
}
$tooltip.innerHTML = text;
$tooltip.style.display = "block";
renderPopover($tooltip, target, {
positions: ['top', 'bottom', 'right', 'center'],
bgColor: "#ffffff",
borderColor: "#cccccc",
borderRadius: 5
});
}
function initTooltip () {
const mouseenterHandler = (e) => {
const target = e.target;
const text = target.getAttribute('tooltip');
if (text) {
showTooltip(target, text);
}
};
const mouseleaveHandler = (e) => {
const target = e.target;
const text = target.getAttribute('tooltip');
if (text) {
hideTooltip(target);
}
};
document.body.removeEventListener('mouseenter', mouseenterHandler, true);
document.body.removeEventListener('mouseleave', mouseleaveHandler, true);
document.body.addEventListener('mouseenter', mouseenterHandler, true);
document.body.addEventListener('mouseleave', mouseleaveHandler, true);
}
export async function uninstallNodes(nodeList, options = {}) {
const {
title = `${nodeList.length} custom nodes`,
onProgress = () => {},
onError = () => {},
onSuccess = () => {},
channel = 'default',
mode = 'default'
} = options;
// Check if queue is busy
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 { success: false, error: 'Queue is busy' };
}
// Confirmation dialog for uninstall
const confirmed = await customConfirm(`Are you sure uninstall ${title}?`);
if (!confirmed) {
return { success: false, error: 'User cancelled' };
}
let errorMsg = "";
let target_items = [];
await api.fetchApi('/manager/queue/reset');
for (const nodeItem of nodeList) {
target_items.push(nodeItem);
onProgress(`Uninstall ${nodeItem.title || nodeItem.name} ...`);
const data = nodeItem.originalData || nodeItem;
data.channel = channel;
data.mode = mode;
data.ui_id = nodeItem.hash || md5(nodeItem.name || nodeItem.title);
const res = await api.fetchApi(`/manager/queue/uninstall`, {
method: 'POST',
body: JSON.stringify(data)
});
if (res.status != 200) {
errorMsg = `'${nodeItem.title || nodeItem.name}': `;
if (res.status == 403) {
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 uninstalled.\n`;
} else {
errorMsg += await res.text() + '\n';
}
break;
}
}
if (errorMsg) {
onError(errorMsg);
show_message("[Uninstall Errors]\n" + errorMsg);
return { success: false, error: errorMsg, targets: target_items };
} else {
await api.fetchApi('/manager/queue/start');
onSuccess(target_items);
showTerminal();
return { success: true, targets: target_items };
}
}
// ===========================================================================================
// Workflow Utilities Consolidation
export async function getWorkflowNodeTypes() {
try {
const res = await fetchData('/customnode/get_node_types_in_workflows');
if (res.status === 200) {
return { success: true, data: res.data };
} else if (res.status === 204) {
// No workflows found - return empty list
return { success: true, data: [] };
} else {
return { success: false, error: res.error };
}
} catch (error) {
return { success: false, error: error };
}
}
export function findPackageByCnrId(cnrId, nodePackages, installedOnly = true) {
if (!cnrId || !nodePackages) {
return null;
}
// Tier 1: Direct key match
if (nodePackages[cnrId]) {
const pack = nodePackages[cnrId];
if (!installedOnly || pack.state !== "not-installed") {
return { key: cnrId, pack: pack };
}
}
// Tier 2: Case-insensitive match
const cnrIdLower = cnrId.toLowerCase();
for (const packKey of Object.keys(nodePackages)) {
if (packKey.toLowerCase() === cnrIdLower) {
const pack = nodePackages[packKey];
if (!installedOnly || pack.state !== "not-installed") {
return { key: packKey, pack: pack };
}
}
}
// Tier 3: URL/reference contains match
for (const packKey of Object.keys(nodePackages)) {
const pack = nodePackages[packKey];
// Skip non-installed packages if installedOnly is true
if (installedOnly && pack.state === "not-installed") {
continue;
}
// Check if reference URL contains cnr_id
if (pack.reference && pack.reference.includes(cnrId)) {
return { key: packKey, pack: pack };
}
// Check if any file URL contains cnr_id
if (pack.files && Array.isArray(pack.files)) {
for (const fileUrl of pack.files) {
if (fileUrl.includes(cnrId)) {
return { key: packKey, pack: pack };
}
}
}
}
return null;
}
export async function analyzeWorkflowUsage(nodePackages) {
const result = await getWorkflowNodeTypes();
if (!result.success) {
return { success: false, error: result.error };
}
const workflowNodeList = result.data;
const usageMap = new Map();
const workflowDetailsMap = new Map();
if (workflowNodeList && Array.isArray(workflowNodeList)) {
const cnrIdCounts = new Map();
const cnrIdToWorkflows = new Map();
// Process each workflow
workflowNodeList.forEach((workflowObj, workflowIndex) => {
if (workflowObj.node_types && Array.isArray(workflowObj.node_types)) {
const workflowCnrIds = new Set();
// Get workflow filename
const workflowFilename = workflowObj.workflow_file_name ||
workflowObj.filename ||
workflowObj.file ||
workflowObj.name ||
workflowObj.path ||
`Workflow ${workflowIndex + 1}`;
// Count nodes per cnr_id in this workflow
const workflowCnrIdCounts = new Map();
workflowObj.node_types.forEach(nodeTypeObj => {
const cnrId = nodeTypeObj.cnr_id;
if (cnrId && cnrId !== "comfy-core") {
// Track unique cnr_ids per workflow
workflowCnrIds.add(cnrId);
// Count nodes per cnr_id in this specific workflow
const workflowNodeCount = workflowCnrIdCounts.get(cnrId) || 0;
workflowCnrIdCounts.set(cnrId, workflowNodeCount + 1);
}
});
// Record workflow details for each unique cnr_id found in this workflow
workflowCnrIds.forEach(cnrId => {
// Count occurrences of this cnr_id across all workflows
const currentCount = cnrIdCounts.get(cnrId) || 0;
cnrIdCounts.set(cnrId, currentCount + 1);
// Track workflow details
if (!cnrIdToWorkflows.has(cnrId)) {
cnrIdToWorkflows.set(cnrId, []);
}
cnrIdToWorkflows.get(cnrId).push({
filename: workflowFilename,
nodeCount: workflowCnrIdCounts.get(cnrId) || 0
});
});
}
});
// Map cnr_id to installed packages with workflow details
cnrIdCounts.forEach((count, cnrId) => {
const workflowDetails = cnrIdToWorkflows.get(cnrId) || [];
const foundPackage = findPackageByCnrId(cnrId, nodePackages, true);
if (foundPackage) {
usageMap.set(foundPackage.key, count);
workflowDetailsMap.set(foundPackage.key, workflowDetails);
}
});
}
return {
success: true,
usageMap: usageMap,
workflowDetailsMap: workflowDetailsMap
};
}
// Size formatting utilities - consolidated from model-manager.js and node-usage-analyzer.js
export function formatSize(v) {
const base = 1000;
const units = ['', 'K', 'M', 'G', 'T', 'P'];
const space = '';
const postfix = 'B';
if (v <= 0) {
return `0${space}${postfix}`;
}
for (let i = 0, l = units.length; i < l; i++) {
const min = Math.pow(base, i);
const max = Math.pow(base, i + 1);
if (v > min && v <= max) {
const unit = units[i];
if (unit) {
const n = v / min;
const nl = n.toString().split('.')[0].length;
const fl = Math.max(3 - nl, 1);
v = n.toFixed(fl);
}
v = v + space + unit + postfix;
break;
}
}
return v;
}
// for size sort
export function sizeToBytes(v) {
if (typeof v === "number") {
return v;
}
if (typeof v === "string") {
const n = parseFloat(v);
const unit = v.replace(/[0-9.B]+/g, "").trim().toUpperCase();
if (unit === "K") {
return n * 1000;
}
if (unit === "M") {
return n * 1000 * 1000;
}
if (unit === "G") {
return n * 1000 * 1000 * 1000;
}
if (unit === "T") {
return n * 1000 * 1000 * 1000 * 1000;
}
}
return v;
}
// Flyover component - consolidated from custom-nodes-manager.js and node-usage-analyzer.js
export function createFlyover(container, options = {}) {
const {
enableHover = false,
hoverHandler = null,
context = null
} = options;
const $flyover = document.createElement("div");
$flyover.className = "cn-flyover";
$flyover.innerHTML = `<div class="cn-flyover-header">
<div class="cn-flyover-close">${icons.arrowRight}</div>
<div class="cn-flyover-title"></div>
<div class="cn-flyover-close">${icons.close}</div>
</div>
<div class="cn-flyover-body"></div>`
container.appendChild($flyover);
const $flyoverTitle = $flyover.querySelector(".cn-flyover-title");
const $flyoverBody = $flyover.querySelector(".cn-flyover-body");
let width = '50%';
let visible = false;
let timeHide;
const closeHandler = (e) => {
if ($flyover === e.target || $flyover.contains(e.target)) {
return;
}
clearTimeout(timeHide);
timeHide = setTimeout(() => {
flyover.hide();
}, 100);
}
const displayHandler = () => {
if (visible) {
$flyover.classList.remove("cn-slide-in-right");
} else {
$flyover.classList.remove("cn-slide-out-right");
$flyover.style.width = '0px';
$flyover.style.display = "none";
}
}
const flyover = {
show: (titleHtml, bodyHtml) => {
clearTimeout(timeHide);
if (context && context.element) {
context.element.removeEventListener("click", closeHandler);
}
$flyoverTitle.innerHTML = titleHtml;
$flyoverBody.innerHTML = bodyHtml;
$flyover.style.display = "block";
$flyover.style.width = width;
if(!visible) {
$flyover.classList.add("cn-slide-in-right");
}
visible = true;
setTimeout(() => {
if (context && context.element) {
context.element.addEventListener("click", closeHandler);
}
}, 100);
},
hide: (now) => {
visible = false;
if (context && context.element) {
context.element.removeEventListener("click", closeHandler);
}
if(now) {
displayHandler();
return;
}
$flyover.classList.add("cn-slide-out-right");
}
}
$flyover.addEventListener("animationend", (e) => {
displayHandler();
});
// Add hover handlers if enabled
if (enableHover && hoverHandler) {
$flyover.addEventListener("mouseenter", hoverHandler, true);
$flyover.addEventListener("mouseleave", hoverHandler, true);
}
$flyover.addEventListener("click", (e) => {
if(e.target.classList.contains("cn-flyover-close")) {
flyover.hide();
return;
}
// Forward other click events to the provided handler or context
if (context && context.handleFlyoverClick) {
context.handleFlyoverClick(e);
}
});
return flyover;
}
// Shared UI State Methods - consolidated from multiple managers
export function createUIStateManager(element, selectors) {
return {
showSelection: (msg) => {
const el = element.querySelector(selectors.selection);
if (el) el.innerHTML = msg;
},
showError: (err) => {
const el = element.querySelector(selectors.message);
if (el) {
const msg = err ? `<font color="red">${err}</font>` : "";
el.innerHTML = msg;
}
},
showMessage: (msg, color) => {
const el = element.querySelector(selectors.message);
if (el) {
if (color) {
msg = `<font color="${color}">${msg}</font>`;
}
el.innerHTML = msg;
}
},
showStatus: (msg, color) => {
const el = element.querySelector(selectors.status);
if (el) {
if (color) {
msg = `<font color="${color}">${msg}</font>`;
}
el.innerHTML = msg;
}
},
showLoading: (grid) => {
if (grid) {
grid.showLoading();
grid.showMask({
opacity: 0.05
});
}
},
hideLoading: (grid) => {
if (grid) {
grid.hideLoading();
grid.hideMask();
}
},
showRefresh: () => {
const el = element.querySelector(selectors.refresh);
if (el) el.style.display = "block";
},
showStop: () => {
const el = element.querySelector(selectors.stop);
if (el) el.style.display = "block";
},
hideStop: () => {
const el = element.querySelector(selectors.stop);
if (el) el.style.display = "none";
}
};
}
initTooltip();
+4 -4
View File
@@ -678,7 +678,7 @@ export class ComponentBuilderDialog extends ComfyDialog {
let orig_handleFile = app.handleFile;
async function handleFile(file) {
async function handleFile(file, ...args) {
if (file.name?.endsWith(".json") || file.name?.endsWith(".pack")) {
const reader = new FileReader();
reader.onload = async () => {
@@ -694,7 +694,7 @@ async function handleFile(file) {
await handle_import_components(jsonContent);
}
else {
orig_handleFile.call(app, file);
orig_handleFile.call(app, file, ...args);
}
};
reader.readAsText(file);
@@ -702,7 +702,7 @@ async function handleFile(file) {
return;
}
orig_handleFile.call(app, file);
orig_handleFile.call(app, file, ...args);
}
app.handleFile = handleFile;
@@ -780,7 +780,7 @@ export function set_component_policy(v) {
let graphToPrompt = app.graphToPrompt;
app.graphToPrompt = async function () {
let p = await graphToPrompt.call(app);
let p = await graphToPrompt.apply(app, arguments);
try {
let groupNodes = p.workflow.extra?.groupNodes;
if(groupNodes) {
+729
View File
@@ -0,0 +1,729 @@
.cn-manager {
--grid-font: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
z-index: 1099;
width: 80vw;
height: 75vh;
min-height: 30em;
display: flex;
flex-direction: column;
gap: 10px;
color: var(--fg-color);
font-family: arial, sans-serif;
text-underline-offset: 3px;
outline: none;
margin: calc(var(--spacing)*2);
}
.cn-manager .cn-flex-auto {
flex: auto;
}
.cn-manager button {
width: auto;
position: relative;
overflow: hidden;
font-size: 16px;
color: var(--input-text);
background-color: var(--comfy-input-bg);
border-color: var(--border-color);
margin: 0;
min-width: 100px;
}
.cn-manager button:hover {
filter: brightness(125%);
}
.cn-manager button:disabled,
.cn-manager input:disabled,
.cn-manager select:disabled {
color: gray;
}
.cn-manager button:disabled {
background-color: var(--comfy-input-bg);
}
.cn-manager .cn-manager-restart {
display: none;
background-color: #500000 !important;
border-color: #88181b !important;
color: white !important;
}
.cn-manager .cn-manager-restart:hover {
background-color: #88181b !important;
}
.cn-manager .cn-manager-stop {
display: none;
background-color: #500000;
color: white;
}
.cn-manager .cn-manager-back {
align-items: center;
justify-content: center;
}
.arrow-icon {
height: 1em;
width: 1em;
margin-right: 5px;
transform: translateY(2px);
}
.cn-icon {
display: block;
width: 16px;
height: 16px;
}
.cn-icon svg {
display: block;
margin: 0;
pointer-events: none;
}
.cn-manager-header {
display: flex;
flex-wrap: wrap;
gap: 5px;
align-items: center;
}
.cn-manager-header label {
display: flex;
gap: 5px;
align-items: center;
}
.cn-manager-filter {
height: 28px;
line-height: 28px;
cursor: pointer;
padding: 0.5em 0.5em;
border: 1px solid var(--border-color);
border-radius: 6px;
background: var(--comfy-input-bg);
}
.cn-manager-filter:hover {
filter: brightness(125%);
}
.cn-manager-keywords {
height: 28px;
line-height: 28px;
padding: 0 5px 0 26px;
background: var(--comfy-input-bg);
background-size: 16px;
background-position: 5px center;
background-repeat: no-repeat;
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20viewBox%3D%220%200%2024%2024%22%20width%3D%22100%25%22%20height%3D%22100%25%22%20pointer-events%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill%3D%22none%22%20stroke%3D%22%23888%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20stroke-width%3D%222%22%20d%3D%22m21%2021-4.486-4.494M19%2010.5a8.5%208.5%200%201%201-17%200%208.5%208.5%200%200%201%2017%200%22%2F%3E%3C%2Fsvg%3E");
border: 1px solid var(--border-color);
border-radius: 6px;
outline-color: transparent;
}
.cn-manager-status {
padding-left: 10px;
}
.cn-manager-grid {
flex: auto;
border: 1px solid var(--border-color);
overflow: hidden;
position: relative;
}
.cn-manager-selection {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
}
.cn-manager-message {
position: relative;
}
.cn-manager-footer {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
}
.cn-manager-grid .tg-turbogrid {
font-family: var(--grid-font);
font-size: 15px;
background: var(--bg-color);
}
.cn-manager-grid .tg-turbogrid .tg-highlight::after {
position: absolute;
top: 0;
left: 0;
content: "";
display: block;
width: 100%;
height: 100%;
box-sizing: border-box;
background-color: #80bdff11;
pointer-events: none;
}
.cn-manager-grid .cn-pack-name a {
color: skyblue;
text-decoration: none;
word-break: break-word;
}
.cn-manager-grid .cn-pack-desc a {
color: #5555FF;
font-weight: bold;
text-decoration: none;
}
.cn-manager-grid .tg-cell a:hover {
text-decoration: underline;
}
.cn-manager-grid .cn-pack-version {
line-height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
height: 100%;
gap: 5px;
}
.cn-manager-grid .cn-pack-nodes {
line-height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
gap: 5px;
cursor: pointer;
height: 100%;
}
.cn-manager-grid .cn-pack-nodes:hover {
text-decoration: underline;
}
.cn-manager-grid .cn-pack-conflicts {
color: orange;
}
.cn-popover {
position: fixed;
z-index: 10000;
padding: 20px;
color: #1e1e1e;
filter: drop-shadow(1px 5px 5px rgb(0 0 0 / 30%));
overflow: hidden;
}
.cn-flyover {
position: absolute;
top: 0;
right: 0;
z-index: 1000;
display: none;
width: 50%;
height: 100%;
background-color: var(--comfy-menu-bg);
animation-duration: 0.2s;
animation-fill-mode: both;
flex-direction: column;
}
.cn-flyover::before {
position: absolute;
top: 0;
content: "";
z-index: 10;
display: block;
width: 10px;
height: 100%;
pointer-events: none;
left: -10px;
background-image: linear-gradient(to left, rgb(0 0 0 / 20%), rgb(0 0 0 / 0%));
}
.cn-flyover-header {
height: 45px;
display: flex;
align-items: center;
gap: 5px;
border-bottom: 1px solid var(--border-color);
}
.cn-flyover-close {
display: flex;
align-items: center;
padding: 0 10px;
justify-content: center;
cursor: pointer;
opacity: 0.8;
height: 100%;
}
.cn-flyover-close:hover {
opacity: 1;
}
.cn-flyover-close svg {
display: block;
margin: 0;
pointer-events: none;
width: 20px;
height: 20px;
}
.cn-flyover-title {
display: flex;
align-items: center;
font-weight: bold;
gap: 10px;
flex: auto;
}
.cn-flyover-body {
height: calc(100% - 45px);
overflow-y: auto;
position: relative;
background-color: var(--comfy-menu-secondary-bg);
}
@keyframes cn-slide-in-right {
from {
visibility: visible;
transform: translate3d(100%, 0, 0);
}
to {
transform: translate3d(0, 0, 0);
}
}
.cn-slide-in-right {
animation-name: cn-slide-in-right;
}
@keyframes cn-slide-out-right {
from {
transform: translate3d(0, 0, 0);
}
to {
visibility: hidden;
transform: translate3d(100%, 0, 0);
}
}
.cn-slide-out-right {
animation-name: cn-slide-out-right;
}
.cn-nodes-list {
width: 100%;
}
.cn-nodes-row {
display: flex;
align-items: center;
gap: 10px;
}
.cn-nodes-row:nth-child(odd) {
background-color: rgb(0 0 0 / 5%);
}
.cn-nodes-row:hover {
background-color: rgb(0 0 0 / 10%);
}
.cn-nodes-sn {
text-align: right;
min-width: 35px;
color: var(--drag-text);
flex-shrink: 0;
font-size: 12px;
padding: 8px 5px;
}
.cn-nodes-name {
cursor: pointer;
white-space: nowrap;
flex-shrink: 0;
position: relative;
padding: 8px 5px;
}
.cn-nodes-name::after {
content: attr(action);
position: absolute;
pointer-events: none;
top: 50%;
left: 100%;
transform: translate(5px, -50%);
font-size: 12px;
color: var(--drag-text);
background-color: var(--comfy-input-bg);
border-radius: 10px;
border: 1px solid var(--border-color);
padding: 3px 8px;
display: none;
}
.cn-nodes-name.action::after {
display: block;
}
.cn-nodes-name:hover {
text-decoration: underline;
}
.cn-nodes-conflict .cn-nodes-name,
.cn-nodes-conflict .cn-icon {
color: orange;
}
.cn-conflicts-list {
display: flex;
flex-wrap: wrap;
gap: 5px;
align-items: center;
padding: 5px 0;
}
.cn-conflicts-list b {
font-weight: normal;
color: var(--descrip-text);
}
.cn-nodes-pack {
cursor: pointer;
color: skyblue;
}
.cn-nodes-pack:hover {
text-decoration: underline;
}
.cn-pack-badge {
font-size: 12px;
font-weight: normal;
background-color: var(--comfy-input-bg);
border-radius: 10px;
border: 1px solid var(--border-color);
padding: 3px 8px;
color: var(--error-text);
}
.cn-preview {
min-width: 300px;
max-width: 500px;
min-height: 120px;
overflow: hidden;
font-size: 12px;
pointer-events: none;
padding: 12px;
color: var(--fg-color);
}
.cn-preview-header {
display: flex;
gap: 8px;
align-items: center;
border-bottom: 1px solid var(--comfy-input-bg);
padding: 5px 10px;
}
.cn-preview-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: grey;
position: relative;
filter: drop-shadow(1px 2px 3px rgb(0 0 0 / 30%));
}
.cn-preview-dot.cn-preview-optional::after {
content: "";
position: absolute;
pointer-events: none;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: var(--comfy-input-bg);
border-radius: 50%;
width: 3px;
height: 3px;
}
.cn-preview-dot.cn-preview-grid {
border-radius: 0;
}
.cn-preview-dot.cn-preview-grid::before {
content: '';
position: absolute;
border-left: 1px solid var(--comfy-input-bg);
border-right: 1px solid var(--comfy-input-bg);
width: 4px;
height: 100%;
left: 2px;
top: 0;
z-index: 1;
}
.cn-preview-dot.cn-preview-grid::after {
content: '';
position: absolute;
border-top: 1px solid var(--comfy-input-bg);
border-bottom: 1px solid var(--comfy-input-bg);
width: 100%;
height: 4px;
left: 0;
top: 2px;
z-index: 1;
}
.cn-preview-name {
flex: auto;
font-size: 14px;
}
.cn-preview-io {
display: flex;
justify-content: space-between;
padding: 10px 10px;
}
.cn-preview-column > div {
display: flex;
gap: 10px;
align-items: center;
height: 18px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.cn-preview-input {
justify-content: flex-start;
}
.cn-preview-output {
justify-content: flex-end;
}
.cn-preview-list {
display: flex;
flex-direction: column;
gap: 3px;
padding: 0 10px 10px 10px;
}
.cn-preview-switch {
position: relative;
display: flex;
justify-content: space-between;
align-items: center;
background: var(--bg-color);
border: 2px solid var(--border-color);
border-radius: 10px;
text-wrap: nowrap;
padding: 2px 20px;
gap: 10px;
}
.cn-preview-switch::before,
.cn-preview-switch::after {
position: absolute;
pointer-events: none;
top: 50%;
transform: translate(0, -50%);
color: var(--fg-color);
opacity: 0.8;
}
.cn-preview-switch::before {
content: "◀";
left: 5px;
}
.cn-preview-switch::after {
content: "▶";
right: 5px;
}
.cn-preview-value {
color: var(--descrip-text);
}
.cn-preview-string {
min-height: 30px;
max-height: 300px;
background: var(--bg-color);
color: var(--descrip-text);
border-radius: 3px;
padding: 3px 5px;
overflow-y: auto;
overflow-x: hidden;
}
.cn-preview-description {
margin: 0px 10px 10px 10px;
padding: 6px;
background: var(--border-color);
color: var(--descrip-text);
border-radius: 5px;
font-style: italic;
word-break: break-word;
}
.cn-tag-list {
display: flex;
flex-wrap: wrap;
gap: 5px;
align-items: center;
margin-bottom: 5px;
}
.cn-tag-list > div {
background-color: var(--border-color);
border-radius: 5px;
padding: 0 5px;
}
.cn-install-buttons {
display: flex;
flex-direction: column;
gap: 3px;
padding: 3px;
align-items: center;
justify-content: center;
height: 100%;
}
.cn-install-buttons button {
padding: 4px 8px;
}
.cn-selected-buttons {
display: flex;
gap: 5px;
align-items: center;
padding-right: 20px;
}
.cn-manager .cn-btn-enable {
background-color: #333399;
color: white;
}
.cn-manager .cn-btn-disable {
background-color: #442277;
color: white;
}
.cn-manager .cn-btn-update {
background-color: #1155AA;
color: white;
}
.cn-manager .cn-btn-try-update {
background-color: Gray;
color: white;
}
.cn-manager .cn-btn-try-fix {
background-color: #6495ED;
color: white;
}
.cn-manager .cn-btn-import-failed {
background-color: #AA1111;
font-size: 10px;
font-weight: bold;
color: white;
}
.cn-manager .cn-btn-install {
background-color: black;
color: white;
}
.cn-manager .cn-btn-try-install {
background-color: Gray;
color: white;
}
.cn-manager .cn-btn-uninstall {
background-color: #993333;
color: white;
}
.cn-manager .cn-btn-reinstall {
background-color: #993333;
color: white;
}
.cn-manager .cn-btn-switch {
background-color: #448833;
color: white;
}
@keyframes cn-btn-loading-bg {
0% {
left: 0;
}
100% {
left: -105px;
}
}
.cn-manager button.cn-btn-loading {
position: relative;
overflow: hidden;
border-color: rgb(0 119 207 / 80%);
background-color: var(--comfy-input-bg);
}
.cn-manager button.cn-btn-loading::after {
position: absolute;
top: 0;
left: 0;
content: "";
width: 500px;
height: 100%;
background-image: repeating-linear-gradient(
-45deg,
rgb(0 119 207 / 30%),
rgb(0 119 207 / 30%) 10px,
transparent 10px,
transparent 15px
);
animation: cn-btn-loading-bg 2s linear infinite;
}
.cn-manager-light .cn-pack-name a {
color: blue;
}
.cn-manager-light .cm-warn-note {
background-color: #ccc !important;
}
.cn-manager-light .cn-btn-install {
background-color: #333;
}
+704 -567
View File
File diff suppressed because it is too large Load Diff
+237
View File
@@ -0,0 +1,237 @@
.cmm-manager {
--grid-font: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
z-index: 1099;
width: 80vw;
height: 75vh;
min-height: 30em;
display: flex;
flex-direction: column;
gap: 10px;
color: var(--fg-color);
font-family: arial, sans-serif;
margin: calc(var(--spacing)*2);
}
.cmm-manager .cmm-flex-auto {
flex: auto;
}
.cmm-manager button {
font-size: 16px;
color: var(--input-text);
background-color: var(--comfy-input-bg);
border-color: var(--border-color);
margin: 0;
min-width: 100px;
}
.cmm-manager button:hover {
filter: brightness(125%);
}
.cmm-manager button:disabled,
.cmm-manager input:disabled,
.cmm-manager select:disabled {
color: gray;
}
.cmm-manager button:disabled {
background-color: var(--comfy-input-bg);
}
.cmm-manager .cmm-manager-refresh {
display: none;
background-color: #000080 !important;
color: white;
}
.cmm-manager .cmm-manager-stop {
display: none;
background-color: #500000;
color: white;
}
.cmm-manager-header {
display: flex;
flex-wrap: wrap;
gap: 5px;
align-items: center;
}
.cmm-manager-header label {
display: flex;
gap: 5px;
align-items: center;
}
.cmm-manager-type,
.cmm-manager-base,
.cmm-manager-filter {
height: 28px;
line-height: 28px;
cursor: pointer;
padding: 0.5em 0.5em;
border: 1px solid var(--border-color);
border-radius: 6px;
background: var(--comfy-input-bg);
}
.cmm-manager-type:hover,
.cmm-manager-base:hover,
.cmm-manager-filter:hover {
filter: brightness(125%);
}
.cmm-manager-keywords {
height: 28px;
line-height: 28px;
padding: 0 5px 0 26px;
background: var(--comfy-input-bg);
background-size: 16px;
background-position: 5px center;
background-repeat: no-repeat;
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20viewBox%3D%220%200%2024%2024%22%20width%3D%22100%25%22%20height%3D%22100%25%22%20pointer-events%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill%3D%22none%22%20stroke%3D%22%23888%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20stroke-width%3D%222%22%20d%3D%22m21%2021-4.486-4.494M19%2010.5a8.5%208.5%200%201%201-17%200%208.5%208.5%200%200%201%2017%200%22%2F%3E%3C%2Fsvg%3E");
border: 1px solid var(--border-color);
border-radius: 6px;
outline-color: transparent;
}
.cmm-manager-status {
padding-left: 10px;
}
.cmm-manager-grid {
flex: auto;
border: 1px solid var(--border-color);
overflow: hidden;
}
.cmm-manager-selection {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
}
.cmm-manager-footer {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
}
.cmm-manager-grid .tg-turbogrid {
font-family: var(--grid-font);
font-size: 15px;
background: var(--bg-color);
}
.cmm-manager-grid .cmm-node-name a {
color: skyblue;
text-decoration: none;
word-break: break-word;
}
.cmm-manager-grid .cmm-node-desc a {
color: #5555FF;
font-weight: bold;
text-decoration: none;
}
.cmm-manager-grid .tg-cell a:hover {
text-decoration: underline;
}
.cmm-icon-passed {
width: 20px;
height: 20px;
position: absolute;
left: calc(50% - 10px);
top: calc(50% - 10px);
}
.cmm-manager .cmm-btn-enable {
background-color: blue;
color: white;
}
.cmm-manager .cmm-btn-disable {
background-color: MediumSlateBlue;
color: white;
}
.cmm-manager .cmm-btn-install {
background-color: black;
color: white;
}
.cmm-btn-install {
padding: 4px 8px;
}
.cmm-btn-download {
width: 18px;
height: 18px;
position: absolute;
left: calc(50% - 10px);
top: calc(50% - 10px);
cursor: pointer;
opacity: 0.8;
color: #fff;
}
.cmm-btn-download:hover {
opacity: 1;
}
.cmm-manager-light .cmm-btn-download {
color: #000;
}
@keyframes cmm-btn-loading-bg {
0% {
left: 0;
}
100% {
left: -105px;
}
}
.cmm-manager button.cmm-btn-loading {
position: relative;
overflow: hidden;
border-color: rgb(0 119 207 / 80%);
background-color: var(--comfy-input-bg);
}
.cmm-manager button.cmm-btn-loading::after {
position: absolute;
top: 0;
left: 0;
content: "";
width: 500px;
height: 100%;
background-image: repeating-linear-gradient(
-45deg,
rgb(0 119 207 / 30%),
rgb(0 119 207 / 30%) 10px,
transparent 10px,
transparent 15px
);
animation: cmm-btn-loading-bg 2s linear infinite;
}
.cmm-manager-light .cmm-node-name a {
color: blue;
}
.cmm-manager-light .cm-warn-note {
background-color: #ccc !important;
}
.cmm-manager-light .cmm-btn-install {
background-color: #333;
}
+75 -327
View File
@@ -1,266 +1,30 @@
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
storeColumnWidth, restoreColumnWidth, loadCss, formatSize, sizeToBytes
} from "./common.js";
import { api } from "../../scripts/api.js";
// https://cenfun.github.io/turbogrid/api.html
import TG from "./turbogrid.esm.js";
import { buildGuiFrameCustomHeader, createSettingsCombo } from "./comfyui-gui-builder.js";
loadCss("./model-manager.css");
const gridId = "model";
const pageCss = `
.cmm-manager {
--grid-font: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
z-index: 1099;
width: 80%;
height: 80%;
display: flex;
flex-direction: column;
gap: 10px;
color: var(--fg-color);
font-family: arial, sans-serif;
}
.cmm-manager .cmm-flex-auto {
flex: auto;
}
.cmm-manager button {
font-size: 16px;
color: var(--input-text);
background-color: var(--comfy-input-bg);
border-radius: 8px;
border-color: var(--border-color);
border-style: solid;
margin: 0;
padding: 4px 8px;
min-width: 100px;
}
.cmm-manager button:disabled,
.cmm-manager input:disabled,
.cmm-manager select:disabled {
color: gray;
}
.cmm-manager button:disabled {
background-color: var(--comfy-input-bg);
}
.cmm-manager .cmm-manager-refresh {
display: none;
background-color: #000080;
color: white;
}
.cmm-manager .cmm-manager-stop {
display: none;
background-color: #500000;
color: white;
}
.cmm-manager-header {
display: flex;
flex-wrap: wrap;
gap: 5px;
align-items: center;
padding: 0 5px;
}
.cmm-manager-header label {
display: flex;
gap: 5px;
align-items: center;
}
.cmm-manager-type,
.cmm-manager-base,
.cmm-manager-filter {
height: 28px;
line-height: 28px;
}
.cmm-manager-keywords {
height: 28px;
line-height: 28px;
padding: 0 5px 0 26px;
background-size: 16px;
background-position: 5px center;
background-repeat: no-repeat;
background-image: url("data:image/svg+xml;charset=utf8,${encodeURIComponent(icons.search.replace("currentColor", "#888"))}");
}
.cmm-manager-status {
padding-left: 10px;
}
.cmm-manager-grid {
flex: auto;
border: 1px solid var(--border-color);
overflow: hidden;
}
.cmm-manager-selection {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
}
.cmm-manager-message {
}
.cmm-manager-footer {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
}
.cmm-manager-grid .tg-turbogrid {
font-family: var(--grid-font);
font-size: 15px;
background: var(--bg-color);
}
.cmm-manager-grid .cmm-node-name a {
color: skyblue;
text-decoration: none;
word-break: break-word;
}
.cmm-manager-grid .cmm-node-desc a {
color: #5555FF;
font-weight: bold;
text-decoration: none;
}
.cmm-manager-grid .tg-cell a:hover {
text-decoration: underline;
}
.cmm-icon-passed {
width: 20px;
height: 20px;
position: absolute;
left: calc(50% - 10px);
top: calc(50% - 10px);
}
.cmm-manager .cmm-btn-enable {
background-color: blue;
color: white;
}
.cmm-manager .cmm-btn-disable {
background-color: MediumSlateBlue;
color: white;
}
.cmm-manager .cmm-btn-install {
background-color: black;
color: white;
}
.cmm-btn-download {
width: 18px;
height: 18px;
position: absolute;
left: calc(50% - 10px);
top: calc(50% - 10px);
cursor: pointer;
opacity: 0.8;
color: #fff;
}
.cmm-btn-download:hover {
opacity: 1;
}
.cmm-manager-light .cmm-btn-download {
color: #000;
}
@keyframes cmm-btn-loading-bg {
0% {
left: 0;
}
100% {
left: -105px;
}
}
.cmm-manager button.cmm-btn-loading {
position: relative;
overflow: hidden;
border-color: rgb(0 119 207 / 80%);
background-color: var(--comfy-input-bg);
}
.cmm-manager button.cmm-btn-loading::after {
position: absolute;
top: 0;
left: 0;
content: "";
width: 500px;
height: 100%;
background-image: repeating-linear-gradient(
-45deg,
rgb(0 119 207 / 30%),
rgb(0 119 207 / 30%) 10px,
transparent 10px,
transparent 15px
);
animation: cmm-btn-loading-bg 2s linear infinite;
}
.cmm-manager-light .cmm-node-name a {
color: blue;
}
.cmm-manager-light .cm-warn-note {
background-color: #ccc !important;
}
.cmm-manager-light .cmm-btn-install {
background-color: #333;
}
`;
const pageHtml = `
<div class="cmm-manager-header">
<label>Filter
<select class="cmm-manager-filter"></select>
</label>
<label>Type
<select class="cmm-manager-type"></select>
</label>
<label>Base
<select class="cmm-manager-base"></select>
</label>
<input class="cmm-manager-keywords" type="search" placeholder="Search" />
<div class="cmm-manager-status"></div>
<div class="cmm-flex-auto"></div>
</div>
<div class="cmm-manager-grid"></div>
<div class="cmm-manager-selection"></div>
<div class="cmm-manager-message"></div>
<div class="cmm-manager-footer">
<button class="cmm-manager-back">
<svg class="arrow-icon" width="14" height="14" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 8H18M2 8L8 2M2 8L8 14" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Back
</button>
<button class="cmm-manager-refresh">Refresh</button>
<button class="cmm-manager-stop">Stop</button>
<div class="cmm-flex-auto"></div>
<div class="cmm-manager cmm-manager-dark">
<div class="cmm-manager-grid"></div>
<div class="cmm-manager-selection"></div>
<div class="cmm-manager-message"></div>
<div class="cmm-manager-footer">
<button class="cmm-manager-refresh p-button p-component">Refresh</button>
<button class="cmm-manager-stop p-button p-component">Stop</button>
<div class="cmm-flex-auto"></div>
</div>
</div>
`;
@@ -283,19 +47,23 @@ export class ModelManager {
}
init() {
const header = $el("div.cmm-manager-header", {}, [
createSettingsCombo("Filter", $el("select.cmm-manager-filter")),
createSettingsCombo("Type", $el("select.cmm-manager-type")),
createSettingsCombo("Base", $el("select.cmm-manager-base")),
$el("input.cmm-manager-keywords.p-inputtext.p-component", { type: "search", placeholder: "Search" }),
$el("div.cmm-manager-status"),
$el("div.cmm-flex-auto")
]);
if (!document.querySelector(`style[context="${this.id}"]`)) {
const $style = document.createElement("style");
$style.setAttribute("context", this.id);
$style.innerHTML = pageCss;
document.head.appendChild($style);
}
const frame = buildGuiFrameCustomHeader(
'cmm-manager-dialog', // dialog id
header, // custom header element
pageHtml, // dialog content element
this
); // send this so we can attach close functions
this.element = $el("div", {
parent: document.body,
className: "comfy-modal cmm-manager"
});
this.element.innerHTML = pageHtml;
this.element = frame;
this.initFilter();
this.bindEvents();
this.initGrid();
@@ -308,10 +76,13 @@ export class ModelManager {
value: ""
}, {
label: "Installed",
value: "True"
value: "installed"
}, {
label: "Not Installed",
value: "False"
value: "not_installed"
}, {
label: "In Workflow",
value: "in_workflow"
}];
this.typeList = [{
@@ -399,7 +170,7 @@ export class ModelManager {
".cmm-manager-stop": {
click: () => {
api.fetchApi('/manager/queue/reset');
api.fetchApi('/manager/queue/reset', { method: 'POST' });
infoToast('Cancel', 'Remaining tasks will stop after completing the current task.');
}
},
@@ -481,12 +252,31 @@ export class ModelManager {
rowFilter: (rowItem) => {
const searchableColumns = ["name", "type", "base", "description", "filename", "save_path"];
const models_extensions = ['.ckpt', '.pt', '.pt2', '.bin', '.pth', '.safetensors', '.pkl', '.sft'];
let shouldShown = grid.highlightKeywordsFilter(rowItem, searchableColumns, this.keywords);
if (shouldShown) {
if(this.filter && rowItem.installed !== this.filter) {
return false;
if(this.filter) {
if (this.filter == "in_workflow") {
rowItem.in_workflow = null;
if (Array.isArray(app.graph._nodes)) {
app.graph._nodes.forEach((item, i) => {
if (Array.isArray(item.widgets_values)) {
item.widgets_values.forEach((_item, i) => {
if (rowItem.in_workflow === null && _item !== null && models_extensions.includes("." + _item.toString().split('.').pop())) {
let filename = _item.match(/([^\/]+)(?=\.\w+$)/)[0];
if (grid.highlightKeywordsFilter(rowItem, searchableColumns, filename)) {
rowItem.in_workflow = "True";
grid.highlightKeywordsFilter(rowItem, searchableColumns, "");
}
}
});
}
});
}
}
return ((this.filter == "installed" && rowItem.installed == "True") || (this.filter == "not_installed" && rowItem.installed == "False") || (this.filter == "in_workflow" && rowItem.in_workflow == "True"));
}
if(this.type && rowItem.type !== this.type) {
@@ -552,7 +342,7 @@ export class ModelManager {
if (installed === "True") {
return `<div class="cmm-icon-passed">${icons.passed}</div>`;
}
return `<button class="cmm-btn-install" mode="install">Install</button>`;
return `<button class="cmm-btn-install p-button p-component" mode="install">Install</button>`;
}
}, {
id: 'url',
@@ -561,7 +351,7 @@ export class ModelManager {
sortable: false,
align: 'center',
formatter: (url, rowItem, columnItem) => {
return `<a class="cmm-btn-download" title="Download file" href="${url}" target="_blank">${icons.download}</a>`;
return `<a class="cmm-btn-download" tooltip="Download file" href="${url}" target="_blank">${icons.download}</a>`;
}
}, {
id: 'size',
@@ -569,7 +359,7 @@ export class ModelManager {
width: 100,
formatter: (size) => {
if (typeof size === "number") {
return this.formatSize(size);
return formatSize(size);
}
return size;
}
@@ -625,7 +415,7 @@ export class ModelManager {
}
this.selectedModels = selectedList;
this.showSelection(`<span>Selected <b>${selectedList.length}</b> models <button class="cmm-btn-install" mode="install">Install</button>`);
this.showSelection(`<span>Selected <b>${selectedList.length}</b> models <button class="cmm-btn-install p-button p-component" mode="install">Install</button>`);
}
focusInstall(item) {
@@ -654,7 +444,7 @@ export class ModelManager {
let needRefresh = false;
let errorMsg = "";
await api.fetchApi('/manager/queue/reset');
await api.fetchApi('/manager/queue/reset', { method: 'POST' });
let target_items = [];
@@ -682,7 +472,16 @@ export class ModelManager {
errorMsg = `'${item.name}': `;
if(res.status == 403) {
errorMsg += `This action is not allowed with this security level configuration.\n`;
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';
}
@@ -704,7 +503,7 @@ export class ModelManager {
}
}
else {
await api.fetchApi('/manager/queue/start');
await api.fetchApi('/manager/queue/start', { method: 'POST' });
this.showStop();
showTerminal();
}
@@ -783,7 +582,7 @@ export class ModelManager {
models.forEach((item, i) => {
const { type, base, name, reference, installed } = item;
item.originalData = JSON.parse(JSON.stringify(item));
item.size = this.sizeToBytes(item.size);
item.size = sizeToBytes(item.size);
item.hash = md5(name + reference);
item.id = i + 1;
@@ -860,7 +659,6 @@ export class ModelManager {
const { models } = res.data;
this.modelList = this.getModelList(models);
// console.log("models", this.modelList);
this.updateFilter();
@@ -872,56 +670,6 @@ export class ModelManager {
// ===========================================================================================
formatSize(v) {
const base = 1000;
const units = ['', 'K', 'M', 'G', 'T', 'P'];
const space = '';
const postfix = 'B';
if (v <= 0) {
return `0${space}${postfix}`;
}
for (let i = 0, l = units.length; i < l; i++) {
const min = Math.pow(base, i);
const max = Math.pow(base, i + 1);
if (v > min && v <= max) {
const unit = units[i];
if (unit) {
const n = v / min;
const nl = n.toString().split('.')[0].length;
const fl = Math.max(3 - nl, 1);
v = n.toFixed(fl);
}
v = v + space + unit + postfix;
break;
}
}
return v;
}
// for size sort
sizeToBytes(v) {
if (typeof v === "number") {
return v;
}
if (typeof v === "string") {
const n = parseFloat(v);
const unit = v.replace(/[0-9.B]+/g, "").trim().toUpperCase();
if (unit === "K") {
return n * 1000;
}
if (unit === "M") {
return n * 1000 * 1000;
}
if (unit === "G") {
return n * 1000 * 1000 * 1000;
}
if (unit === "T") {
return n * 1000 * 1000 * 1000 * 1000;
}
}
return v;
}
showSelection(msg) {
this.element.querySelector(".cmm-manager-selection").innerHTML = msg;
}
@@ -1022,4 +770,4 @@ export class ModelManager {
close() {
this.element.style.display = "none";
}
}
}
+699
View File
@@ -0,0 +1,699 @@
.nu-manager {
--grid-font: -apple-system, BlinkMacSystemFont, "Segue UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
z-index: 1099;
width: 80%;
height: 80%;
display: flex;
flex-direction: column;
gap: 10px;
color: var(--fg-color);
font-family: arial, sans-serif;
text-underline-offset: 3px;
outline: none;
}
.nu-manager .nu-flex-auto {
flex: auto;
}
.nu-manager button {
font-size: 16px;
color: var(--input-text);
background-color: var(--comfy-input-bg);
border-radius: 8px;
border-color: var(--border-color);
border-style: solid;
margin: 0;
padding: 4px 8px;
min-width: 100px;
}
.nu-manager button:disabled,
.nu-manager input:disabled,
.nu-manager select:disabled {
color: gray;
}
.nu-manager button:disabled {
background-color: var(--comfy-input-bg);
}
.nu-manager .nu-manager-restart {
display: none;
background-color: #500000;
color: white;
}
.nu-manager .nu-manager-stop {
display: none;
background-color: #500000;
color: white;
}
.nu-manager .nu-manager-back {
align-items: center;
justify-content: center;
}
.arrow-icon {
height: 1em;
width: 1em;
margin-right: 5px;
transform: translateY(2px);
}
.cn-icon {
display: block;
width: 16px;
height: 16px;
}
.cn-icon svg {
display: block;
margin: 0;
pointer-events: none;
}
.nu-manager-header {
display: flex;
flex-wrap: wrap;
gap: 5px;
align-items: center;
padding: 0 5px;
}
.nu-manager-header label {
display: flex;
gap: 5px;
align-items: center;
}
.nu-manager-filter {
height: 28px;
line-height: 28px;
}
.nu-manager-keywords {
height: 28px;
line-height: 28px;
padding: 0 5px 0 26px;
background-size: 16px;
background-position: 5px center;
background-repeat: no-repeat;
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20viewBox%3D%220%200%2024%2024%22%20width%3D%22100%25%22%20height%3D%22100%25%22%20pointer-events%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill%3D%22none%22%20stroke%3D%22%23888%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20stroke-width%3D%222%22%20d%3D%22m21%2021-4.486-4.494M19%2010.5a8.5%208.5%200%201%201-17%200%208.5%208.5%200%200%201%2017%200%22%2F%3E%3C%2Fsvg%3E");
}
.nu-manager-status {
padding-left: 10px;
}
.nu-manager-grid {
flex: auto;
border: 1px solid var(--border-color);
overflow: hidden;
position: relative;
}
.nu-manager-selection {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
}
.nu-manager-message {
position: relative;
}
.nu-manager-footer {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
}
.nu-manager-grid .tg-turbogrid {
font-family: var(--grid-font);
font-size: 15px;
background: var(--bg-color);
}
.nu-manager-grid .tg-turbogrid .tg-highlight::after {
position: absolute;
top: 0;
left: 0;
content: "";
display: block;
width: 100%;
height: 100%;
box-sizing: border-box;
background-color: #80bdff11;
pointer-events: none;
}
.nu-manager-grid .nu-pack-name a {
color: skyblue;
text-decoration: none;
word-break: break-word;
}
.nu-manager-grid .cn-pack-desc a {
color: #5555FF;
font-weight: bold;
text-decoration: none;
}
.nu-manager-grid .tg-cell a:hover {
text-decoration: underline;
}
.nu-manager-grid .cn-pack-version {
line-height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
height: 100%;
gap: 5px;
}
.nu-manager-grid .cn-pack-nodes {
line-height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
gap: 5px;
cursor: pointer;
height: 100%;
}
.nu-manager-grid .cn-pack-nodes:hover {
text-decoration: underline;
}
.nu-manager-grid .cn-pack-conflicts {
color: orange;
}
.cn-popover {
position: fixed;
z-index: 10000;
padding: 20px;
color: #1e1e1e;
filter: drop-shadow(1px 5px 5px rgb(0 0 0 / 30%));
overflow: hidden;
}
.cn-flyover {
position: absolute;
top: 0;
right: 0;
z-index: 1000;
display: none;
width: 50%;
height: 100%;
background-color: var(--comfy-menu-bg);
animation-duration: 0.2s;
animation-fill-mode: both;
flex-direction: column;
}
.cn-flyover::before {
position: absolute;
top: 0;
content: "";
z-index: 10;
display: block;
width: 10px;
height: 100%;
pointer-events: none;
left: -10px;
background-image: linear-gradient(to left, rgb(0 0 0 / 20%), rgb(0 0 0 / 0%));
}
.cn-flyover-header {
height: 45px;
display: flex;
align-items: center;
gap: 5px;
border-bottom: 1px solid var(--border-color);
}
.cn-flyover-close {
display: flex;
align-items: center;
padding: 0 10px;
justify-content: center;
cursor: pointer;
opacity: 0.8;
height: 100%;
}
.cn-flyover-close:hover {
opacity: 1;
}
.cn-flyover-close svg {
display: block;
margin: 0;
pointer-events: none;
width: 20px;
height: 20px;
}
.cn-flyover-title {
display: flex;
align-items: center;
font-weight: bold;
gap: 10px;
flex: auto;
}
.cn-flyover-body {
height: calc(100% - 45px);
overflow-y: auto;
position: relative;
background-color: var(--comfy-menu-secondary-bg);
}
@keyframes cn-slide-in-right {
from {
visibility: visible;
transform: translate3d(100%, 0, 0);
}
to {
transform: translate3d(0, 0, 0);
}
}
.cn-slide-in-right {
animation-name: cn-slide-in-right;
}
@keyframes cn-slide-out-right {
from {
transform: translate3d(0, 0, 0);
}
to {
visibility: hidden;
transform: translate3d(100%, 0, 0);
}
}
.cn-slide-out-right {
animation-name: cn-slide-out-right;
}
.cn-nodes-list {
width: 100%;
}
.cn-nodes-row {
display: flex;
align-items: center;
gap: 10px;
}
.cn-nodes-row:nth-child(odd) {
background-color: rgb(0 0 0 / 5%);
}
.cn-nodes-row:hover {
background-color: rgb(0 0 0 / 10%);
}
.cn-nodes-sn {
text-align: right;
min-width: 35px;
color: var(--drag-text);
flex-shrink: 0;
font-size: 12px;
padding: 8px 5px;
}
.cn-nodes-name {
cursor: pointer;
white-space: nowrap;
flex-shrink: 0;
position: relative;
padding: 8px 5px;
}
.cn-nodes-name::after {
content: attr(action);
position: absolute;
pointer-events: none;
top: 50%;
left: 100%;
transform: translate(5px, -50%);
font-size: 12px;
color: var(--drag-text);
background-color: var(--comfy-input-bg);
border-radius: 10px;
border: 1px solid var(--border-color);
padding: 3px 8px;
display: none;
}
.cn-nodes-name.action::after {
display: block;
}
.cn-nodes-name:hover {
text-decoration: underline;
}
.cn-nodes-conflict .cn-nodes-name,
.cn-nodes-conflict .cn-icon {
color: orange;
}
.cn-conflicts-list {
display: flex;
flex-wrap: wrap;
gap: 5px;
align-items: center;
padding: 5px 0;
}
.cn-conflicts-list b {
font-weight: normal;
color: var(--descrip-text);
}
.cn-nodes-pack {
cursor: pointer;
color: skyblue;
}
.cn-nodes-pack:hover {
text-decoration: underline;
}
.cn-pack-badge {
font-size: 12px;
font-weight: normal;
background-color: var(--comfy-input-bg);
border-radius: 10px;
border: 1px solid var(--border-color);
padding: 3px 8px;
color: var(--error-text);
}
.cn-preview {
min-width: 300px;
max-width: 500px;
min-height: 120px;
overflow: hidden;
font-size: 12px;
pointer-events: none;
padding: 12px;
color: var(--fg-color);
}
.cn-preview-header {
display: flex;
gap: 8px;
align-items: center;
border-bottom: 1px solid var(--comfy-input-bg);
padding: 5px 10px;
}
.cn-preview-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: grey;
position: relative;
filter: drop-shadow(1px 2px 3px rgb(0 0 0 / 30%));
}
.cn-preview-dot.cn-preview-optional::after {
content: "";
position: absolute;
pointer-events: none;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: var(--comfy-input-bg);
border-radius: 50%;
width: 3px;
height: 3px;
}
.cn-preview-dot.cn-preview-grid {
border-radius: 0;
}
.cn-preview-dot.cn-preview-grid::before {
content: '';
position: absolute;
border-left: 1px solid var(--comfy-input-bg);
border-right: 1px solid var(--comfy-input-bg);
width: 4px;
height: 100%;
left: 2px;
top: 0;
z-index: 1;
}
.cn-preview-dot.cn-preview-grid::after {
content: '';
position: absolute;
border-top: 1px solid var(--comfy-input-bg);
border-bottom: 1px solid var(--comfy-input-bg);
width: 100%;
height: 4px;
left: 0;
top: 2px;
z-index: 1;
}
.cn-preview-name {
flex: auto;
font-size: 14px;
}
.cn-preview-io {
display: flex;
justify-content: space-between;
padding: 10px 10px;
}
.cn-preview-column > div {
display: flex;
gap: 10px;
align-items: center;
height: 18px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.cn-preview-input {
justify-content: flex-start;
}
.cn-preview-output {
justify-content: flex-end;
}
.cn-preview-list {
display: flex;
flex-direction: column;
gap: 3px;
padding: 0 10px 10px 10px;
}
.cn-preview-switch {
position: relative;
display: flex;
justify-content: space-between;
align-items: center;
background: var(--bg-color);
border: 2px solid var(--border-color);
border-radius: 10px;
text-wrap: nowrap;
padding: 2px 20px;
gap: 10px;
}
.cn-preview-switch::before,
.cn-preview-switch::after {
position: absolute;
pointer-events: none;
top: 50%;
transform: translate(0, -50%);
color: var(--fg-color);
opacity: 0.8;
}
.cn-preview-switch::before {
content: "◀";
left: 5px;
}
.cn-preview-switch::after {
content: "▶";
right: 5px;
}
.cn-preview-value {
color: var(--descrip-text);
}
.cn-preview-string {
min-height: 30px;
max-height: 300px;
background: var(--bg-color);
color: var(--descrip-text);
border-radius: 3px;
padding: 3px 5px;
overflow-y: auto;
overflow-x: hidden;
}
.cn-preview-description {
margin: 0px 10px 10px 10px;
padding: 6px;
background: var(--border-color);
color: var(--descrip-text);
border-radius: 5px;
font-style: italic;
word-break: break-word;
}
.cn-tag-list {
display: flex;
flex-wrap: wrap;
gap: 5px;
align-items: center;
margin-bottom: 5px;
}
.cn-tag-list > div {
background-color: var(--border-color);
border-radius: 5px;
padding: 0 5px;
}
.cn-install-buttons {
display: flex;
flex-direction: column;
gap: 3px;
padding: 3px;
align-items: center;
justify-content: center;
height: 100%;
}
.cn-selected-buttons {
display: flex;
gap: 5px;
align-items: center;
padding-right: 20px;
}
.nu-manager .cn-btn-enable {
background-color: #333399;
color: white;
}
.nu-manager .cn-btn-disable {
background-color: #442277;
color: white;
}
.nu-manager .cn-btn-update {
background-color: #1155AA;
color: white;
}
.nu-manager .cn-btn-try-update {
background-color: Gray;
color: white;
}
.nu-manager .cn-btn-try-fix {
background-color: #6495ED;
color: white;
}
.nu-manager .cn-btn-import-failed {
background-color: #AA1111;
font-size: 10px;
font-weight: bold;
color: white;
}
.nu-manager .cn-btn-install {
background-color: black;
color: white;
}
.nu-manager .cn-btn-try-install {
background-color: Gray;
color: white;
}
.nu-manager .cn-btn-uninstall {
background-color: #993333;
color: white;
}
.nu-manager .cn-btn-reinstall {
background-color: #993333;
color: white;
}
.nu-manager .cn-btn-switch {
background-color: #448833;
color: white;
}
@keyframes nu-btn-loading-bg {
0% {
left: 0;
}
100% {
left: -105px;
}
}
.nu-manager button.nu-btn-loading {
position: relative;
overflow: hidden;
border-color: rgb(0 119 207 / 80%);
background-color: var(--comfy-input-bg);
}
.nu-manager button.nu-btn-loading::after {
position: absolute;
top: 0;
left: 0;
content: "";
width: 500px;
height: 100%;
background-image: repeating-linear-gradient(
-45deg,
rgb(0 119 207 / 30%),
rgb(0 119 207 / 30%) 10px,
transparent 10px,
transparent 15px
);
animation: nu-btn-loading-bg 2s linear infinite;
}
.nu-manager-light .nu-pack-name a {
color: blue;
}
.nu-manager-light .cm-warn-note {
background-color: #ccc !important;
}
.nu-manager-light .cn-btn-install {
background-color: #333;
}
+742
View File
@@ -0,0 +1,742 @@
import { app } from "../../scripts/app.js";
import { $el } from "../../scripts/ui.js";
import {
manager_instance,
fetchData, md5, show_message, customAlert, infoToast, showTerminal,
storeColumnWidth, restoreColumnWidth, loadCss, uninstallNodes,
analyzeWorkflowUsage, sizeToBytes, createFlyover, createUIStateManager
} from "./common.js";
import { api } from "../../scripts/api.js";
// https://cenfun.github.io/turbogrid/api.html
import TG from "./turbogrid.esm.js";
loadCss("./node-usage-analyzer.css");
const gridId = "model";
const pageHtml = `
<div class="nu-manager-header">
<div class="nu-manager-status"></div>
<input type="text" class="nu-manager-keywords" placeholder="Filter keywords..." />
<div class="nu-flex-auto"></div>
</div>
<div class="nu-manager-grid"></div>
<div class="nu-manager-selection"></div>
<div class="nu-manager-message"></div>
<div class="nu-manager-footer">
<button class="nu-manager-back">
<svg class="arrow-icon" width="14" height="14" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 8H18M2 8L8 2M2 8L8 14" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Back
</button>
<button class="nu-manager-refresh">Refresh</button>
<button class="nu-manager-stop">Stop</button>
<div class="nu-flex-auto"></div>
</div>
`;
export class NodeUsageAnalyzer {
static instance = null;
static SortMode = {
BY_PACKAGE: 'by_package'
};
constructor(app, manager_dialog) {
this.app = app;
this.manager_dialog = manager_dialog;
this.id = "nu-manager";
this.filter = '';
this.type = '';
this.base = '';
this.keywords = '';
this.init();
// Initialize shared UI state manager
this.ui = createUIStateManager(this.element, {
selection: ".nu-manager-selection",
message: ".nu-manager-message",
status: ".nu-manager-status",
refresh: ".nu-manager-refresh",
stop: ".nu-manager-stop"
});
api.addEventListener("cm-queue-status", this.onQueueStatus);
}
init() {
this.element = $el("div", {
parent: document.body,
className: "comfy-modal nu-manager"
});
this.element.innerHTML = pageHtml;
this.bindEvents();
this.initGrid();
}
bindEvents() {
const eventsMap = {
".nu-manager-selection": {
click: (e) => {
const target = e.target;
const mode = target.getAttribute("mode");
if (mode === "install") {
this.installModels(this.selectedModels, target);
} else if (mode === "uninstall") {
this.uninstallModels(this.selectedModels, target);
}
}
},
".nu-manager-refresh": {
click: () => {
app.refreshComboInNodes();
}
},
".nu-manager-stop": {
click: () => {
api.fetchApi('/manager/queue/reset');
infoToast('Cancel', 'Remaining tasks will stop after completing the current task.');
}
},
".nu-manager-back": {
click: (e) => {
this.close()
manager_instance.show();
}
}
};
Object.keys(eventsMap).forEach(selector => {
const target = this.element.querySelector(selector);
if (target) {
const events = eventsMap[selector];
if (events) {
Object.keys(events).forEach(type => {
target.addEventListener(type, events[type]);
});
}
}
});
}
// ===========================================================================================
initGrid() {
const container = this.element.querySelector(".nu-manager-grid");
const grid = new TG.Grid(container);
this.grid = grid;
this.flyover = createFlyover(container, { context: this });
grid.bind('onUpdated', (e, d) => {
this.ui.showStatus(`${grid.viewRows.length.toLocaleString()} installed packages`);
});
grid.bind('onSelectChanged', (e, changes) => {
this.renderSelected();
});
grid.bind("onColumnWidthChanged", (e, columnItem) => {
storeColumnWidth(gridId, columnItem)
});
grid.bind('onClick', (e, d) => {
const { rowItem } = d;
const target = d.e.target;
const mode = target.getAttribute("mode");
if (mode === "install") {
this.installModels([rowItem], target);
return;
}
if (mode === "uninstall") {
this.uninstallModels([rowItem], target);
return;
}
// Handle click on usage count
if (d.columnItem.id === "used_in_count" && rowItem.used_in_count > 0) {
this.showUsageDetails(rowItem);
return;
}
});
grid.setOption({
theme: 'dark',
selectVisible: true,
selectMultiple: true,
selectAllVisible: true,
textSelectable: true,
scrollbarRound: true,
frozenColumn: 1,
rowNotFound: "No Results",
rowHeight: 40,
bindWindowResize: true,
bindContainerResize: true,
cellResizeObserver: (rowItem, columnItem) => {
const autoHeightColumns = ['name', 'description'];
return autoHeightColumns.includes(columnItem.id)
}
});
}
renderGrid() {
// update theme
const colorPalette = this.app.ui.settings.settingsValues['Comfy.ColorPalette'];
Array.from(this.element.classList).forEach(cn => {
if (cn.startsWith("nu-manager-")) {
this.element.classList.remove(cn);
}
});
this.element.classList.add(`nu-manager-${colorPalette}`);
const options = {
theme: colorPalette === "light" ? "" : "dark"
};
const rows = this.modelList || [];
const columns = [{
id: 'title',
name: 'Title',
width: 200,
minWidth: 100,
maxWidth: 500,
classMap: 'nu-pack-name',
formatter: function (name, rowItem, columnItem, cellNode) {
return `<a href=${rowItem.reference} target="_blank"><b>${name}</b></a>`;
}
}, {
id: 'used_in_count',
name: 'Used in',
width: 100,
formatter: function (usedCount, rowItem, columnItem) {
if (!usedCount || usedCount === 0) {
return '0';
}
const plural = usedCount > 1 ? 's' : '';
return `<div class="cn-pack-nodes" style="cursor: pointer;">${usedCount} workflow${plural}</div>`;
}
}, {
id: 'action',
name: 'Action',
width: 160,
minWidth: 140,
maxWidth: 200,
sortable: false,
align: 'center',
formatter: function (action, rowItem, columnItem) {
// Only show uninstall button for installed packages
if (rowItem.originalData && rowItem.originalData.state && rowItem.originalData.state !== "not-installed") {
return `<div class="cn-install-buttons"><button class="nu-btn-uninstall" mode="uninstall">Uninstall</button></div>`;
}
return '';
}
}];
restoreColumnWidth(gridId, columns);
this.grid.setData({
options,
rows,
columns
});
this.grid.render();
}
updateGrid() {
if (this.grid) {
this.grid.update();
}
}
showUsageDetails(rowItem) {
const workflowList = rowItem.workflowDetails;
if (!workflowList || workflowList.length === 0) {
return;
}
let titleHtml = `<div class="cn-nodes-pack">${rowItem.title}</div>`;
const list = [];
list.push(`<div class="cn-nodes-list">`);
workflowList.forEach((workflow, i) => {
list.push(`<div class="cn-nodes-row">`);
list.push(`<div class="cn-nodes-sn">${i + 1}</div>`);
list.push(`<div class="cn-nodes-name">${workflow.filename}</div>`);
list.push(`<div class="cn-nodes-details">${workflow.nodeCount} node${workflow.nodeCount > 1 ? 's' : ''}</div>`);
list.push(`</div>`);
});
list.push("</div>");
const bodyHtml = list.join("");
this.flyover.show(titleHtml, bodyHtml);
}
renderSelected() {
const selectedList = this.grid.getSelectedRows();
if (!selectedList.length) {
this.ui.showSelection("");
return;
}
const installedSelected = selectedList.filter(item =>
item.originalData && item.originalData.state && item.originalData.state !== "not-installed"
);
if (installedSelected.length === 0) {
this.ui.showSelection(`<span>Selected <b>${selectedList.length}</b> packages (none can be uninstalled)</span>`);
return;
}
this.selectedModels = installedSelected;
this.ui.showSelection(`
<div class="nu-selected-buttons">
<span>Selected <b>${installedSelected.length}</b> installed packages</span>
<button class="nu-btn-uninstall" mode="uninstall">Uninstall Selected</button>
</div>
`);
}
// ===========================================================================================
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("nu-btn-loading");
this.ui.showError("");
let needRefresh = false;
let errorMsg = "";
await api.fetchApi('/manager/queue/reset');
let target_items = [];
for (const item of list) {
this.grid.scrollRowIntoView(item);
target_items.push(item);
this.ui.showStatus(`Install ${item.name} ...`);
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 (res.status != 200) {
errorMsg = `'${item.name}': `;
if (res.status == 403) {
errorMsg += `This action is not allowed with this security level configuration.\n`;
} else {
errorMsg += await res.text() + '\n';
}
break;
}
}
this.install_context = { btn: btn, targets: target_items };
if (errorMsg) {
this.ui.showError(errorMsg);
show_message("[Installation Errors]\n" + errorMsg);
// reset
for (let k in target_items) {
const item = target_items[k];
this.grid.updateCell(item, "installed");
}
}
else {
await api.fetchApi('/manager/queue/start');
this.ui.showStop();
showTerminal();
}
}
async uninstallModels(list, btn) {
btn.classList.add("nu-btn-loading");
this.ui.showError("");
const result = await uninstallNodes(list, {
title: list.length === 1 ? list[0].title || list[0].name : `${list.length} custom nodes`,
channel: 'default',
mode: 'default',
onProgress: (msg) => {
this.showStatus(msg);
},
onError: (errorMsg) => {
this.showError(errorMsg);
},
onSuccess: (targets) => {
this.showStatus(`Uninstalled ${targets.length} custom node(s) successfully`);
this.showMessage(`To apply the uninstalled custom nodes, please restart ComfyUI and refresh browser.`, "red");
// Update the grid to reflect changes
for (let item of targets) {
if (item.originalData) {
item.originalData.state = "not-installed";
}
this.grid.updateRow(item);
}
}
});
if (result.success) {
this.showStop();
}
btn.classList.remove("nu-btn-loading");
}
async onQueueStatus(event) {
let self = NodeUsageAnalyzer.instance;
if (event.detail.status == 'in_progress' && (event.detail.ui_target == 'model_manager' || event.detail.ui_target == 'nodepack_manager')) {
const hash = event.detail.target;
const item = self.grid.getRowItemBy("hash", hash);
if (item) {
item.refresh = true;
self.grid.setRowSelected(item, false);
item.selectable = false;
self.grid.updateRow(item);
}
}
else if (event.detail.status == 'done') {
self.hideStop();
self.onQueueCompleted(event.detail);
}
}
async onQueueCompleted(info) {
let result = info.model_result || info.nodepack_result;
if (!result || result.length == 0) {
return;
}
let self = NodeUsageAnalyzer.instance;
if (!self.install_context) {
return;
}
let btn = self.install_context.btn;
self.hideLoading();
btn.classList.remove("nu-btn-loading");
let errorMsg = "";
for (let hash in result) {
let v = result[hash];
if (v != 'success' && v != 'skip')
errorMsg += v + '\n';
}
for (let k in self.install_context.targets) {
let item = self.install_context.targets[k];
if (info.model_result) {
self.grid.updateCell(item, "installed");
} else if (info.nodepack_result) {
// Handle uninstall completion
if (item.originalData) {
item.originalData.state = "not-installed";
}
self.grid.updateRow(item);
}
}
if (errorMsg) {
self.showError(errorMsg);
show_message("Operation Error:\n" + errorMsg);
} else {
if (info.model_result) {
self.showStatus(`Install ${Object.keys(result).length} models successfully`);
self.showRefresh();
self.showMessage(`To apply the installed model, please click the 'Refresh' button.`, "red");
} else if (info.nodepack_result) {
self.showStatus(`Uninstall ${Object.keys(result).length} custom node(s) successfully`);
self.showMessage(`To apply the uninstalled custom nodes, please restart ComfyUI and refresh browser.`, "red");
}
}
infoToast('Tasks done', `[ComfyUI-Manager] All tasks in the queue have been completed.\n${info.done_count}/${info.total_count}`);
self.install_context = undefined;
}
getModelList(models) {
const typeMap = new Map();
const baseMap = new Map();
models.forEach((item, i) => {
const { type, base, name, reference, installed } = item;
// CRITICAL FIX: Do NOT overwrite originalData - it contains the needed state field!
item.size = sizeToBytes(item.size);
item.hash = md5(name + reference);
if (installed === "True") {
item.selectable = false;
}
typeMap.set(type, type);
baseMap.set(base, base);
});
const typeList = [];
typeMap.forEach(type => {
typeList.push({
label: type,
value: type
});
});
typeList.sort((a, b) => {
const au = a.label.toUpperCase();
const bu = b.label.toUpperCase();
if (au !== bu) {
return au > bu ? 1 : -1;
}
return 0;
});
this.typeList = [{
label: "All",
value: ""
}].concat(typeList);
const baseList = [];
baseMap.forEach(base => {
baseList.push({
label: base,
value: base
});
});
baseList.sort((a, b) => {
const au = a.label.toUpperCase();
const bu = b.label.toUpperCase();
if (au !== bu) {
return au > bu ? 1 : -1;
}
return 0;
});
this.baseList = [{
label: "All",
value: ""
}].concat(baseList);
return models;
}
// ===========================================================================================
async loadData() {
this.showLoading();
this.showStatus(`Analyzing node usage ...`);
const mode = manager_instance.datasrc_combo.value;
const nodeListRes = await fetchData(`/customnode/getlist?mode=${mode}&skip_update=true`);
if (nodeListRes.error) {
this.showError("Failed to get custom node list.");
this.hideLoading();
return;
}
const { channel, node_packs } = nodeListRes.data;
delete node_packs['comfyui-manager'];
this.installed_custom_node_packs = node_packs;
// Use the consolidated workflow analysis utility
const result = await analyzeWorkflowUsage(node_packs);
if (!result.success) {
if (result.error.toString().includes('204')) {
this.showMessage("No workflows were found for analysis.");
} else {
this.showError(result.error);
this.hideLoading();
return;
}
}
// Transform node_packs into models format - ONLY INSTALLED PACKAGES
const models = [];
Object.keys(node_packs).forEach((packKey, index) => {
const pack = node_packs[packKey];
// Only include installed packages (filter out "not-installed" packages)
if (pack.state === "not-installed") {
return; // Skip non-installed packages
}
const usedCount = result.usageMap?.get(packKey) || 0;
const workflowDetails = result.workflowDetailsMap?.get(packKey) || [];
models.push({
title: pack.title || packKey,
reference: pack.reference || pack.files?.[0] || '#',
used_in_count: usedCount,
workflowDetails: workflowDetails,
name: packKey,
originalData: pack
});
});
// Sort by usage count (descending) then by title
models.sort((a, b) => {
if (b.used_in_count !== a.used_in_count) {
return b.used_in_count - a.used_in_count;
}
return a.title.localeCompare(b.title);
});
this.modelList = this.getModelList(models);
this.renderGrid();
this.hideLoading();
}
// ===========================================================================================
showSelection(msg) {
this.element.querySelector(".nu-manager-selection").innerHTML = msg;
}
showError(err) {
this.showMessage(err, "red");
}
showMessage(msg, color) {
if (color) {
msg = `<font color="${color}">${msg}</font>`;
}
this.element.querySelector(".nu-manager-message").innerHTML = msg;
}
showStatus(msg, color) {
if (color) {
msg = `<font color="${color}">${msg}</font>`;
}
this.element.querySelector(".nu-manager-status").innerHTML = msg;
}
showLoading() {
// this.setDisabled(true);
if (this.grid) {
this.grid.showLoading();
this.grid.showMask({
opacity: 0.05
});
}
}
hideLoading() {
// this.setDisabled(false);
if (this.grid) {
this.grid.hideLoading();
this.grid.hideMask();
}
}
setDisabled(disabled) {
const $close = this.element.querySelector(".nu-manager-close");
const $refresh = this.element.querySelector(".nu-manager-refresh");
const $stop = this.element.querySelector(".nu-manager-stop");
const list = [
".nu-manager-header input",
".nu-manager-header select",
".nu-manager-footer button",
".nu-manager-selection button"
].map(s => {
return Array.from(this.element.querySelectorAll(s));
})
.flat()
.filter(it => {
return it !== $close && it !== $refresh && it !== $stop;
});
list.forEach($elem => {
if (disabled) {
$elem.setAttribute("disabled", "disabled");
} else {
$elem.removeAttribute("disabled");
}
});
Array.from(this.element.querySelectorAll(".nu-btn-loading")).forEach($elem => {
$elem.classList.remove("nu-btn-loading");
});
}
showRefresh() {
this.element.querySelector(".nu-manager-refresh").style.display = "block";
}
showStop() {
this.element.querySelector(".nu-manager-stop").style.display = "block";
}
hideStop() {
this.element.querySelector(".nu-manager-stop").style.display = "none";
}
setKeywords(keywords = "") {
this.keywords = keywords;
this.element.querySelector(".nu-manager-keywords").value = keywords;
}
show(sortMode) {
this.element.style.display = "flex";
this.setKeywords("");
this.showSelection("");
this.showMessage("");
this.loadData();
}
close() {
this.element.style.display = "none";
}
}
+1
View File
@@ -153,6 +153,7 @@ app.registerExtension({
app.canvas.graph.add(new_node, false);
node_info_copy(this, new_node, true);
app.canvas.graph.remove(this);
requestAnimationFrame(() => app.canvas.setDirty(true, true))
},
});
});
+619
View File
@@ -0,0 +1,619 @@
const hasOwn = function(obj, key) {
return Object.prototype.hasOwnProperty.call(obj, key);
};
const isNum = function(num) {
if (typeof num !== 'number' || isNaN(num)) {
return false;
}
const isInvalid = function(n) {
if (n === Number.MAX_VALUE || n === Number.MIN_VALUE || n === Number.NEGATIVE_INFINITY || n === Number.POSITIVE_INFINITY) {
return true;
}
return false;
};
if (isInvalid(num)) {
return false;
}
return true;
};
const toNum = (num) => {
if (typeof (num) !== 'number') {
num = parseFloat(num);
}
if (isNaN(num)) {
num = 0;
}
num = Math.round(num);
return num;
};
const clamp = function(value, min, max) {
return Math.max(min, Math.min(max, value));
};
const isWindow = (obj) => {
return Boolean(obj && obj === obj.window);
};
const isDocument = (obj) => {
return Boolean(obj && obj.nodeType === 9);
};
const isElement = (obj) => {
return Boolean(obj && obj.nodeType === 1);
};
// ===========================================================================================
export const toRect = (obj) => {
if (obj) {
return {
left: toNum(obj.left || obj.x),
top: toNum(obj.top || obj.y),
width: toNum(obj.width),
height: toNum(obj.height)
};
}
return {
left: 0,
top: 0,
width: 0,
height: 0
};
};
export const getElement = (selector) => {
if (typeof selector === 'string' && selector) {
if (selector.startsWith('#')) {
return document.getElementById(selector.slice(1));
}
return document.querySelector(selector);
}
if (isDocument(selector)) {
return selector.body;
}
if (isElement(selector)) {
return selector;
}
};
export const getRect = (target, fixed) => {
if (!target) {
return toRect();
}
if (isWindow(target)) {
return {
left: 0,
top: 0,
width: window.innerWidth,
height: window.innerHeight
};
}
const elem = getElement(target);
if (!elem) {
return toRect(target);
}
const br = elem.getBoundingClientRect();
const rect = toRect(br);
// fix offset
if (!fixed) {
rect.left += window.scrollX;
rect.top += window.scrollY;
}
rect.width = elem.offsetWidth;
rect.height = elem.offsetHeight;
return rect;
};
// ===========================================================================================
const calculators = {
bottom: (info, containerRect, targetRect) => {
info.space = containerRect.top + containerRect.height - targetRect.top - targetRect.height - info.height;
info.top = targetRect.top + targetRect.height;
info.left = Math.round(targetRect.left + targetRect.width * 0.5 - info.width * 0.5);
},
top: (info, containerRect, targetRect) => {
info.space = targetRect.top - info.height - containerRect.top;
info.top = targetRect.top - info.height;
info.left = Math.round(targetRect.left + targetRect.width * 0.5 - info.width * 0.5);
},
right: (info, containerRect, targetRect) => {
info.space = containerRect.left + containerRect.width - targetRect.left - targetRect.width - info.width;
info.top = Math.round(targetRect.top + targetRect.height * 0.5 - info.height * 0.5);
info.left = targetRect.left + targetRect.width;
},
left: (info, containerRect, targetRect) => {
info.space = targetRect.left - info.width - containerRect.left;
info.top = Math.round(targetRect.top + targetRect.height * 0.5 - info.height * 0.5);
info.left = targetRect.left - info.width;
}
};
// with order
export const getDefaultPositions = () => {
return Object.keys(calculators);
};
const calculateSpace = (info, containerRect, targetRect) => {
const calculator = calculators[info.position];
calculator(info, containerRect, targetRect);
if (info.space >= 0) {
info.passed += 1;
}
};
// ===========================================================================================
const calculateAlignOffset = (info, containerRect, targetRect, alignType, sizeType) => {
const popoverStart = info[alignType];
const popoverSize = info[sizeType];
const containerStart = containerRect[alignType];
const containerSize = containerRect[sizeType];
const targetStart = targetRect[alignType];
const targetSize = targetRect[sizeType];
const targetCenter = targetStart + targetSize * 0.5;
// size overflow
if (popoverSize > containerSize) {
const overflow = (popoverSize - containerSize) * 0.5;
info[alignType] = containerStart - overflow;
info.offset = targetCenter - containerStart + overflow;
return;
}
const space1 = popoverStart - containerStart;
const space2 = (containerStart + containerSize) - (popoverStart + popoverSize);
// both side passed, default to center
if (space1 >= 0 && space2 >= 0) {
if (info.passed) {
info.passed += 2;
}
info.offset = popoverSize * 0.5;
return;
}
// one side passed
if (info.passed) {
info.passed += 1;
}
if (space1 < 0) {
const min = containerStart;
info[alignType] = min;
info.offset = targetCenter - min;
return;
}
// space2 < 0
const max = containerStart + containerSize - popoverSize;
info[alignType] = max;
info.offset = targetCenter - max;
};
const calculateHV = (info, containerRect) => {
if (['top', 'bottom'].includes(info.position)) {
info.top = clamp(info.top, containerRect.top, containerRect.top + containerRect.height - info.height);
return ['left', 'width'];
}
info.left = clamp(info.left, containerRect.left, containerRect.left + containerRect.width - info.width);
return ['top', 'height'];
};
const calculateOffset = (info, containerRect, targetRect) => {
const [alignType, sizeType] = calculateHV(info, containerRect);
calculateAlignOffset(info, containerRect, targetRect, alignType, sizeType);
info.offset = clamp(info.offset, 0, info[sizeType]);
};
// ===========================================================================================
const calculateDistance = (info, previousPositionInfo) => {
if (!previousPositionInfo) {
return;
}
// no change if position no change with previous
if (info.position === previousPositionInfo.position) {
return;
}
const ax = info.left + info.width * 0.5;
const ay = info.top + info.height * 0.5;
const bx = previousPositionInfo.left + previousPositionInfo.width * 0.5;
const by = previousPositionInfo.top + previousPositionInfo.height * 0.5;
const dx = Math.abs(ax - bx);
const dy = Math.abs(ay - by);
info.distance = Math.round(Math.sqrt(dx * dx + dy * dy));
};
// ===========================================================================================
const calculatePositionInfo = (info, containerRect, targetRect, previousPositionInfo) => {
calculateSpace(info, containerRect, targetRect);
calculateOffset(info, containerRect, targetRect);
calculateDistance(info, previousPositionInfo);
};
// ===========================================================================================
const calculateBestPosition = (containerRect, targetRect, infoMap, withOrder, previousPositionInfo) => {
// position space: +1
// align space:
// two side passed: +2
// one side passed: +1
const safePassed = 3;
if (previousPositionInfo) {
const prevInfo = infoMap[previousPositionInfo.position];
if (prevInfo) {
calculatePositionInfo(prevInfo, containerRect, targetRect);
if (prevInfo.passed >= safePassed) {
return prevInfo;
}
prevInfo.calculated = true;
}
}
const positionList = [];
Object.values(infoMap).forEach((info) => {
if (!info.calculated) {
calculatePositionInfo(info, containerRect, targetRect, previousPositionInfo);
}
positionList.push(info);
});
positionList.sort((a, b) => {
if (a.passed !== b.passed) {
return b.passed - a.passed;
}
if (withOrder && a.passed >= safePassed && b.passed >= safePassed) {
return a.index - b.index;
}
if (a.space !== b.space) {
return b.space - a.space;
}
return a.index - b.index;
});
// logTable(positionList);
return positionList[0];
};
// const logTable = (() => {
// let time_id;
// return (info) => {
// clearTimeout(time_id);
// time_id = setTimeout(() => {
// console.table(info);
// }, 10);
// };
// })();
// ===========================================================================================
const getAllowPositions = (positions, defaultAllowPositions) => {
if (!positions) {
return;
}
if (Array.isArray(positions)) {
positions = positions.join(',');
}
positions = String(positions).split(',').map((it) => it.trim().toLowerCase()).filter((it) => it);
positions = positions.filter((it) => defaultAllowPositions.includes(it));
if (!positions.length) {
return;
}
return positions;
};
const isPositionChanged = (info, previousPositionInfo) => {
if (!previousPositionInfo) {
return true;
}
if (info.left !== previousPositionInfo.left) {
return true;
}
if (info.top !== previousPositionInfo.top) {
return true;
}
return false;
};
// ===========================================================================================
// const log = (name, time) => {
// if (time > 0.1) {
// console.log(name, time);
// }
// };
export const getBestPosition = (containerRect, targetRect, popoverRect, positions, previousPositionInfo) => {
const defaultAllowPositions = getDefaultPositions();
let withOrder = true;
let allowPositions = getAllowPositions(positions, defaultAllowPositions);
if (!allowPositions) {
allowPositions = defaultAllowPositions;
withOrder = false;
}
// console.log('withOrder', withOrder);
// const start_time = performance.now();
const infoMap = {};
allowPositions.forEach((k, i) => {
infoMap[k] = {
position: k,
index: i,
top: 0,
left: 0,
width: popoverRect.width,
height: popoverRect.height,
space: 0,
offset: 0,
passed: 0,
distance: 0
};
});
// log('infoMap', performance.now() - start_time);
const bestPosition = calculateBestPosition(containerRect, targetRect, infoMap, withOrder, previousPositionInfo);
// check left/top
bestPosition.changed = isPositionChanged(bestPosition, previousPositionInfo);
return bestPosition;
};
// ===========================================================================================
const getTemplatePath = (width, height, arrowOffset, arrowSize, borderRadius) => {
const p = (px, py) => {
return [px, py].join(',');
};
const px = function(num, alignEnd) {
const floor = Math.floor(num);
let n = num < floor + 0.5 ? floor + 0.5 : floor + 1.5;
if (alignEnd) {
n -= 1;
}
return n;
};
const pxe = function(num) {
return px(num, true);
};
const ls = [];
const innerLeft = px(arrowSize);
const innerRight = pxe(width - arrowSize);
arrowOffset = clamp(arrowOffset, innerLeft, innerRight);
const innerTop = px(arrowSize);
const innerBottom = pxe(height - arrowSize);
const startPoint = p(innerLeft, innerTop + borderRadius);
const arrowPoint = p(arrowOffset, 1);
const LT = p(innerLeft, innerTop);
const RT = p(innerRight, innerTop);
const AOT = p(arrowOffset - arrowSize, innerTop);
const RRT = p(innerRight - borderRadius, innerTop);
ls.push(`M${startPoint}`);
ls.push(`V${innerBottom - borderRadius}`);
ls.push(`Q${p(innerLeft, innerBottom)} ${p(innerLeft + borderRadius, innerBottom)}`);
ls.push(`H${innerRight - borderRadius}`);
ls.push(`Q${p(innerRight, innerBottom)} ${p(innerRight, innerBottom - borderRadius)}`);
ls.push(`V${innerTop + borderRadius}`);
if (arrowOffset < innerLeft + arrowSize + borderRadius) {
ls.push(`Q${RT} ${RRT}`);
ls.push(`H${arrowOffset + arrowSize}`);
ls.push(`L${arrowPoint}`);
if (arrowOffset < innerLeft + arrowSize) {
ls.push(`L${LT}`);
ls.push(`L${startPoint}`);
} else {
ls.push(`L${AOT}`);
ls.push(`Q${LT} ${startPoint}`);
}
} else if (arrowOffset > innerRight - arrowSize - borderRadius) {
if (arrowOffset > innerRight - arrowSize) {
ls.push(`L${RT}`);
} else {
ls.push(`Q${RT} ${p(arrowOffset + arrowSize, innerTop)}`);
}
ls.push(`L${arrowPoint}`);
ls.push(`L${AOT}`);
ls.push(`H${innerLeft + borderRadius}`);
ls.push(`Q${LT} ${startPoint}`);
} else {
ls.push(`Q${RT} ${RRT}`);
ls.push(`H${arrowOffset + arrowSize}`);
ls.push(`L${arrowPoint}`);
ls.push(`L${AOT}`);
ls.push(`H${innerLeft + borderRadius}`);
ls.push(`Q${LT} ${startPoint}`);
}
return ls.join('');
};
const getPathData = function(position, width, height, arrowOffset, arrowSize, borderRadius) {
const handlers = {
bottom: () => {
const d = getTemplatePath(width, height, arrowOffset, arrowSize, borderRadius);
return {
d,
transform: ''
};
},
top: () => {
const d = getTemplatePath(width, height, width - arrowOffset, arrowSize, borderRadius);
return {
d,
transform: `rotate(180,${width * 0.5},${height * 0.5})`
};
},
left: () => {
const d = getTemplatePath(height, width, arrowOffset, arrowSize, borderRadius);
const x = (width - height) * 0.5;
const y = (height - width) * 0.5;
return {
d,
transform: `translate(${x} ${y}) rotate(90,${height * 0.5},${width * 0.5})`
};
},
right: () => {
const d = getTemplatePath(height, width, height - arrowOffset, arrowSize, borderRadius);
const x = (width - height) * 0.5;
const y = (height - width) * 0.5;
return {
d,
transform: `translate(${x} ${y}) rotate(-90,${height * 0.5},${width * 0.5})`
};
}
};
return handlers[position]();
};
// ===========================================================================================
// position style cache
const styleCache = {
// position: '',
// top: {},
// bottom: {},
// left: {},
// right: {}
};
export const getPositionStyle = (info, options = {}) => {
const o = {
bgColor: '#fff',
borderColor: '#ccc',
borderRadius: 5,
arrowSize: 10
};
Object.keys(o).forEach((k) => {
if (hasOwn(options, k)) {
const d = o[k];
const v = options[k];
if (typeof d === 'string') {
// string
if (typeof v === 'string' && v) {
o[k] = v;
}
} else {
// number
if (isNum(v) && v >= 0) {
o[k] = v;
}
}
}
});
const key = [
info.width,
info.height,
info.offset,
o.arrowSize,
o.borderRadius,
o.bgColor,
o.borderColor
].join('-');
const positionCache = styleCache[info.position];
if (positionCache && key === positionCache.key) {
const st = positionCache.style;
st.changed = styleCache.position !== info.position;
styleCache.position = info.position;
return st;
}
// console.log(options);
const data = getPathData(info.position, info.width, info.height, info.offset, o.arrowSize, o.borderRadius);
// console.log(data);
const viewBox = [0, 0, info.width, info.height].join(' ');
const svg = [
`<svg viewBox="${viewBox}" xmlns="http://www.w3.org/2000/svg">`,
`<path d="${data.d}" fill="${o.bgColor}" stroke="${o.borderColor}" transform="${data.transform}" />`,
'</svg>'
].join('');
// console.log(svg);
const backgroundImage = `url("data:image/svg+xml;charset=utf8,${encodeURIComponent(svg)}")`;
const background = `${backgroundImage} center no-repeat`;
const padding = `${o.arrowSize + o.borderRadius}px`;
const style = {
background,
backgroundImage,
padding,
changed: true
};
styleCache.position = info.position;
styleCache[info.position] = {
key,
style
};
return style;
};
+65
View File
@@ -0,0 +1,65 @@
.snapshot-manager {
--grid-font: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
z-index: 1099;
width: 80vw;
height: 75vh;
min-height: 30em;
display: flex;
flex-direction: column;
gap: 10px;
color: var(--fg-color);
font-family: arial, sans-serif;
text-underline-offset: 3px;
outline: none;
margin: calc(var(--spacing)*2);
}
.snapshot-manager button {
width: auto;
position: relative;
overflow: hidden;
font-size: 16px;
color: var(--input-text);
background-color: var(--comfy-input-bg);
border-color: var(--border-color);
margin: 0;
min-width: 100px;
padding: 4px 8px;
}
.snapshot-manager .snapshot-restore-btn {
background-color: #00158f !important;
border-color: #2025b9 !important;
color: white !important;
}
.snapshot-manager .snapshot-remove-btn {
background-color: #970000 !important;
border-color: #be2127 !important;
color: white !important;
}
.snapshot-manager button:hover {
filter: brightness(125%);
}
.snapshot-manager .data-btns {
display: flex;
flex-direction: column;
gap: calc(var(--spacing)*2);
padding: calc(var(--spacing)*2);
align-items: center;
justify-content: center;
height: 100%;
}
.snapshot-footer {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
}
.snapshot-manager .cn-flex-auto {
flex: auto;
}
+43 -32
View File
@@ -1,16 +1,18 @@
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 } from "./common.js";
import { manager_instance, rebootAPI, show_message, handle403Response, loadCss } from "./common.js";
import { buildGuiFrame } from "./comfyui-gui-builder.js";
loadCss("./snapshot.css");
async function restore_snapshot(target) {
if(SnapshotManager.instance) {
try {
const response = await api.fetchApi(`/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;
}
@@ -27,7 +29,7 @@ async function restore_snapshot(target) {
}
finally {
await SnapshotManager.instance.invalidateControl();
SnapshotManager.instance.updateMessage("<BR>To apply the snapshot, please <button id='cm-reboot-button2' class='cm-small-button'>RESTART</button> ComfyUI. And refresh browser.", 'cm-reboot-button2');
SnapshotManager.instance.updateMessage("<BR>To apply the snapshot, please <button id='cm-reboot-button2' class='p-button p-component'>RESTART</button> ComfyUI. And refresh browser.", 'cm-reboot-button2');
}
}
}
@@ -35,10 +37,10 @@ async function restore_snapshot(target) {
async function remove_snapshot(target) {
if(SnapshotManager.instance) {
try {
const response = await api.fetchApi(`/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;
}
@@ -61,7 +63,7 @@ async function remove_snapshot(target) {
async function save_current_snapshot() {
try {
const response = await api.fetchApi('/snapshot/save', { cache: "no-store" });
const response = await api.fetchApi('/snapshot/save', { method: 'POST', cache: "no-store" });
app.ui.dialog.close();
return true;
}
@@ -88,6 +90,8 @@ export class SnapshotManager extends ComfyDialog {
message_box = null;
data = null;
content = $el("div.snapshot-manager");
clear() {
this.restore_buttons = [];
this.message_box = null;
@@ -96,9 +100,18 @@ export class SnapshotManager extends ComfyDialog {
constructor(app, manager_dialog) {
super();
this.manager_dialog = manager_dialog;
// this.manager_dialog = manager_dialog;
this.search_keyword = '';
this.element = $el("div.comfy-modal", { parent: document.body }, []);
const frame = buildGuiFrame(
'snapshot-manager-dialog', // dialog id
'Snapshot Manager', // title
'i.mdi.mdi-puzzle', // icon class
this.content, // dialog content element
this
); // send this so we can attach close functions
this.element = frame;
}
async remove_item() {
@@ -109,7 +122,7 @@ export class SnapshotManager extends ComfyDialog {
createControls() {
return [
$el("button.cm-small-button", {
$el("button.p-button.p-component", {
type: "button",
textContent: "Close",
onclick: () => { this.close(); }
@@ -132,8 +145,8 @@ export class SnapshotManager extends ComfyDialog {
this.clear();
this.data = (await getSnapshotList()).items;
while (this.element.children.length) {
this.element.removeChild(this.element.children[0]);
while (this.content.children.length) {
this.content.removeChild(this.content.children[0]);
}
await this.createGrid();
@@ -145,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();
}
@@ -204,20 +217,21 @@ export class SnapshotManager extends ComfyDialog {
data2.innerHTML = `&nbsp;${data}`;
var data_button = document.createElement('td');
data_button.style.textAlign = "center";
data_button.className = "data-btns";
var restoreBtn = document.createElement('button');
restoreBtn.className = "snapshot-restore-btn p-button p-component";
restoreBtn.innerHTML = 'Restore';
restoreBtn.style.width = "100px";
restoreBtn.style.backgroundColor = 'blue';
restoreBtn.addEventListener('click', function() {
restore_snapshot(data);
});
var removeBtn = document.createElement('button');
removeBtn.className = "snapshot-remove-btn p-button p-component";
removeBtn.innerHTML = 'Remove';
removeBtn.style.width = "100px";
removeBtn.style.backgroundColor = 'red';
removeBtn.addEventListener('click', function() {
remove_snapshot(data);
@@ -241,13 +255,14 @@ export class SnapshotManager extends ComfyDialog {
let self = this;
const panel = document.createElement('div');
panel.style.width = "100%";
panel.style.height = "100%";
panel.appendChild(grid);
function handleResize() {
const parentHeight = self.element.clientHeight;
const gridHeight = parentHeight - 200;
grid.style.height = gridHeight + "px";
// grid.style.height = gridHeight + "px";
}
window.addEventListener("resize", handleResize);
@@ -256,25 +271,17 @@ export class SnapshotManager extends ComfyDialog {
grid.style.width = "100%";
grid.style.height = "100%";
grid.style.overflowY = "scroll";
this.element.style.height = "85%";
this.element.style.width = "80%";
this.element.appendChild(panel);
this.content.appendChild(panel);
handleResize();
}
async createBottomControls() {
var close_button = document.createElement("button");
close_button.className = "cm-small-button";
close_button.innerHTML = "Close";
close_button.onclick = () => { this.close(); }
close_button.style.display = "inline-block";
var save_button = document.createElement("button");
save_button.className = "cm-small-button";
save_button.className = "p-button p-component";
save_button.innerHTML = "Save snapshot";
save_button.onclick = () => { save_current_snapshot(); }
save_button.style.display = "inline-block";
save_button.style.horizontalAlign = "right";
save_button.style.width = "170px";
@@ -282,15 +289,19 @@ export class SnapshotManager extends ComfyDialog {
this.message_box.style.height = '60px';
this.message_box.style.verticalAlign = 'middle';
this.element.appendChild(this.message_box);
this.element.appendChild(close_button);
this.element.appendChild(save_button);
const footer = $el("div.snapshot-footer");
const spacer = $el("div.cn-flex-auto");
footer.appendChild(spacer);
footer.appendChild(save_button);
this.content.appendChild(this.message_box);
this.content.appendChild(footer);
}
async show() {
try {
this.invalidateControl();
this.element.style.display = "block";
this.element.style.display = "flex";
this.element.style.zIndex = 1099;
}
catch(exception) {
+2 -2
View File
@@ -70,8 +70,8 @@ class WorkflowMetadataExtension {
if (cnr_id === "comfy-core") return; // don't allow hijacking comfy-core name
if (cnr_id) nodeProperties.cnr_id = cnr_id;
else nodeProperties.aux_id = aux_id;
if (ver) nodeProperties.ver = ver;
} else if (["nodes", "comfy_extras"].includes(moduleType)) {
if (ver) nodeProperties.ver = ver.trim();
} else if (["nodes", "comfy_extras", "comfy_api_nodes"].includes(moduleType)) {
nodeProperties.cnr_id = "comfy-core";
nodeProperties.ver = this.comfyCoreVersion;
}
+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()
+1239 -4
View File
File diff suppressed because it is too large Load Diff
+95
View File
@@ -0,0 +1,95 @@
# ComfyUI-Manager: Node Database (node_db)
This directory contains the JSON database files that power ComfyUI-Manager's legacy node registry system. While the manager is gradually transitioning to the online Custom Node Registry (CNR), these local JSON files continue to provide important metadata about custom nodes, models, and their integrations.
## Directory Structure
The node_db directory is organized into several subdirectories, each serving a specific purpose:
- **dev/**: Development channel files with latest additions and experimental nodes
- **legacy/**: Historical/legacy nodes that may require special handling
- **new/**: New nodes that have passed initial verification but are still being evaluated
- **forked/**: Forks of existing nodes with modifications
- **tutorial/**: Example and tutorial nodes designed for learning purposes
## Core Database Files
Each subdirectory contains a standard set of JSON files:
- **custom-node-list.json**: Primary database of custom nodes with metadata
- **extension-node-map.json**: Maps between extensions and individual nodes they provide
- **model-list.json**: Catalog of models that can be downloaded through the manager
- **alter-list.json**: Alternative implementations of nodes for compatibility or functionality
- **github-stats.json**: GitHub repository statistics for node popularity metrics
## Database Schema
### custom-node-list.json
```json
{
"custom_nodes": [
{
"title": "Node display name",
"name": "Repository name",
"reference": "Original repository if forked",
"files": ["GitHub URL or other source location"],
"install_type": "git",
"description": "Description of the node's functionality",
"pip": ["optional pip dependencies"],
"js": ["optional JavaScript files"],
"tags": ["categorization tags"]
}
]
}
```
### extension-node-map.json
```json
{
"extension-id": [
["list", "of", "node", "classes"],
{
"author": "Author name",
"description": "Extension description",
"nodename_pattern": "Optional regex pattern for node name matching"
}
]
}
```
## Transition to Custom Node Registry (CNR)
This local database system is being progressively replaced by the online Custom Node Registry (CNR), which provides:
- Real-time updates without manual JSON maintenance
- Improved versioning support
- Better security validation
- Enhanced metadata
The Manager supports both systems simultaneously during the transition period.
## Implementation Details
- The database follows a channel-based architecture for different sources
- Multiple database modes are supported: Channel, Local, and Remote
- The system supports differential updates to minimize bandwidth usage
- Security levels are enforced for different node installations based on source
## Usage in the Application
The Manager's backend uses these database files to:
1. Provide browsable lists of available nodes and models
2. Resolve dependencies for installation
3. Track updates and new versions
4. Map node classes to their source repositories
5. Assess risk levels for installation security
## Maintenance Scripts
Each subdirectory contains a `scan.sh` script that assists with:
- Scanning repositories for new nodes
- Updating metadata
- Validating database integrity
- Generating proper JSON structures
This database system enables a flexible, secure, and comprehensive management system for the ComfyUI ecosystem while the transition to CNR continues.
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
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/bash
rm ~/.tmp/dev/*.py > /dev/null 2>&1
python ../../scanner.py ~/.tmp/dev
python ../../scanner.py ~/.tmp/dev $*
+50
View File
@@ -1,5 +1,45 @@
{
"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",
"reference": "https://github.com/synchronicity-labs/sync-comfyui",
"files": [
"https://github.com/synchronicity-labs/sync-comfyui"
],
"install_type": "git-clone",
"description": "This custom node allows you to perform audio-video lip synchronization inside ComfyUI using a simple interface."
},
{
"author": "joaomede",
"title": "ComfyUI-Unload-Model-Fork",
"reference": "https://github.com/joaomede/ComfyUI-Unload-Model-Fork",
"files": [
"https://github.com/joaomede/ComfyUI-Unload-Model-Fork"
],
"install_type": "git-clone",
"description": "For unloading a model or all models, using the memory management that is already present in ComfyUI. Copied from [a/https://github.com/willblaschko/ComfyUI-Unload-Models](https://github.com/willblaschko/ComfyUI-Unload-Models) but without the unnecessary extra stuff."
},
{
"author": "SanDiegoDude",
"title": "ComfyUI-HiDream-Sampler [WIP]",
"reference": "https://github.com/SanDiegoDude/ComfyUI-HiDream-Sampler",
"files": [
"https://github.com/SanDiegoDude/ComfyUI-HiDream-Sampler"
],
"install_type": "git-clone",
"description": "A collection of enhanced nodes for ComfyUI that provide powerful additional functionality to your workflows.\nNOTE: The files in the repo are not organized."
},
{
"author": "PramaLLC",
"title": "ComfyUI BEN - Background Erase Network",
@@ -139,6 +179,16 @@
],
"install_type": "git-clone",
"description": "A fork of KJNodes for ComfyUI.\nVarious quality of life -nodes for ComfyUI, mostly just visual stuff to improve usability"
},
{
"author": "huixingyun",
"title": "ComfyUI-SoundFlow",
"reference": "https://github.com/huixingyun/ComfyUI-SoundFlow",
"files": [
"https://github.com/huixingyun/ComfyUI-SoundFlow"
],
"install_type": "git-clone",
"description": "forked from https://github.com/fredconex/ComfyUI-SoundFlow (removed)"
}
]
}
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
File diff suppressed because it is too large Load Diff
+71 -1
View File
@@ -1,5 +1,25 @@
{
"custom_nodes": [
{
"author": "Comfy-Org",
"title": "ComfyUI React Extension Template",
"reference": "https://github.com/Comfy-Org/ComfyUI-React-Extension-Template",
"files": [
"https://github.com/Comfy-Org/ComfyUI-React-Extension-Template"
],
"install_type": "git-clone",
"description": "A minimal template for creating React/TypeScript frontend extensions for ComfyUI, with complete boilerplate setup including internationalization and unit testing."
},
{
"author": "comfyui-wiki",
"title": "ComfyUI-i18n-demo",
"reference": "https://github.com/comfyui-wiki/ComfyUI-i18n-demo",
"files": [
"https://github.com/comfyui-wiki/ComfyUI-i18n-demo"
],
"install_type": "git-clone",
"description": "ComfyUI custom node develop i18n support demo "
},
{
"author": "Suzie1",
"title": "Guide To Making Custom Nodes in ComfyUI",
@@ -311,6 +331,56 @@
],
"description": "ComfyUI node for creating some Turtle Graphic demos.",
"install_type": "git-clone"
}
},
{
"author": "cozy-comfyui",
"title": "cozy_ex_dynamic",
"reference": "https://github.com/cozy-comfyui/cozy_ex_dynamic",
"files": [
"https://github.com/cozy-comfyui/cozy_ex_dynamic"
],
"description": "Dynamic Node examples for ComfyUI",
"install_type": "git-clone"
},
{
"author": "Jonathon-Doran",
"title": "remote-combo-demo",
"reference": "https://github.com/Jonathon-Doran/remote-combo-demo",
"files": [
"https://github.com/Jonathon-Doran/remote-combo-demo"
],
"install_type": "git-clone",
"description": "A minimal test suite demonstrating how remote COMBO inputs behave in ComfyUI, with and without force_input"
},
{
"author": "J1mB091",
"title": "ComfyUI-J1mB091 Custom Nodes",
"reference": "https://github.com/J1mB091/ComfyUI-J1mB091",
"files": [
"https://github.com/J1mB091/ComfyUI-J1mB091"
],
"install_type": "git-clone",
"description": "Vibe Coded ComfyUI Custom Nodes"
},
{
"author": "aiforhumans",
"title": "XDev Nodes - Complete Toolkit",
"reference": "https://github.com/aiforhumans/comfyui-xdev-nodes",
"files": [
"https://github.com/aiforhumans/comfyui-xdev-nodes"
],
"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)"
}
]
}
+1086
View File
File diff suppressed because it is too large Load Diff
+49 -33
View File
@@ -21,24 +21,26 @@ import cm_global
import manager_downloader
import folder_paths
import datetime
if hasattr(datetime, 'datetime'):
from datetime import datetime
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 datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]
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: '{datetime.__file__}'")
logging.error(f"[ComfyUI-Manager] fallback timestamp mode\n datetime module is invalid: '{dt.__file__}'")
def current_timestamp():
return str(time.time()).split('.')[0]
security_check.security_check()
manager_util.add_python_path_to_env()
cm_global.pip_blacklist = {'torch', 'torchsde', 'torchvision'}
cm_global.pip_downgrade_blacklist = ['torch', 'torchsde', 'torchvision', 'transformers', 'safetensors', 'kornia']
cm_global.pip_blacklist = {'torch', 'torchaudio', 'torchsde', 'torchvision'}
cm_global.pip_downgrade_blacklist = ['torch', 'torchaudio', 'torchsde', 'torchvision', 'transformers', 'safetensors', 'kornia']
def skip_pip_spam(x):
@@ -83,7 +85,15 @@ 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 = os.path.abspath(os.path.join(folder_paths.get_user_directory(), 'default', 'ComfyUI-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")
@@ -116,14 +126,14 @@ def check_file_logging():
read_config()
read_uv_mode()
security_check.security_check()
check_file_logging()
cm_global.pip_overrides = {'numpy': 'numpy<2', 'ultralytics': 'ultralytics==8.3.40'}
cm_global.pip_overrides = {}
if os.path.exists(manager_pip_overrides_path):
with open(manager_pip_overrides_path, 'r', encoding="UTF-8", errors="ignore") as json_file:
cm_global.pip_overrides = json.load(json_file)
cm_global.pip_overrides['numpy'] = 'numpy<2'
cm_global.pip_overrides['ultralytics'] = 'ultralytics==8.3.40' # for security
if os.path.exists(manager_pip_blacklist_path):
@@ -336,7 +346,12 @@ try:
log_file.write(message)
else:
log_file.write(f"[{timestamp}] {message}")
log_file.flush()
try:
log_file.flush()
except Exception:
pass
self.last_char = message if message == '' else message[-1]
if not file_only:
@@ -349,13 +364,19 @@ try:
original_stderr.flush()
def flush(self):
log_file.flush()
try:
log_file.flush()
except Exception:
pass
with std_log_lock:
if self.is_stdout:
original_stdout.flush()
else:
original_stderr.flush()
try:
if self.is_stdout:
original_stdout.flush()
else:
original_stderr.flush()
except (OSError, ValueError):
pass
def close(self):
self.flush()
@@ -506,7 +527,8 @@ check_bypass_ssl()
# Perform install
processed_install = set()
script_list_path = os.path.join(folder_paths.user_directory, "default", "ComfyUI-Manager", "startup-scripts", "install-scripts.txt")
# 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)
@@ -618,6 +640,7 @@ def execute_lazy_install_script(repo_path, executable):
lines = manager_util.robust_readlines(requirements_path)
for line in lines:
package_name = remap_pip_package(line.strip())
package_name = package_name.split('#')[0].strip()
if package_name and not is_installed(package_name):
if '--index-url' in package_name:
s = package_name.split('--index-url')
@@ -689,14 +712,6 @@ def execute_lazy_cnr_switch(target, zip_url, from_path, to_path, no_deps, custom
file.write('\n'.join(list(extracted)))
def execute_migration(moves):
import shutil
for x in moves:
if os.path.exists(x[0]) and not os.path.exists(x[1]):
shutil.move(x[0], x[1])
print(f"[ComfyUI-Manager] MIGRATION: '{x[0]}' -> '{x[1]}'")
script_executed = False
def execute_startup_script():
@@ -754,9 +769,6 @@ def execute_startup_script():
execute_lazy_cnr_switch(script[0], script[2], script[3], script[4], script[5], script[6])
execute_lazy_install_script(script[3], script[7])
elif script[1] == "#LAZY-MIGRATION":
execute_migration(script[2])
elif script[1] == "#LAZY-DELETE-NODEPACK":
execute_lazy_delete(script[2])
@@ -793,7 +805,11 @@ def execute_startup_script():
# 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()
+4 -3
View File
@@ -1,13 +1,14 @@
[project]
name = "comfyui-manager"
description = "ComfyUI-Manager provides features to install and manage custom nodes for ComfyUI, as well as various functionalities to assist with ComfyUI."
version = "3.30.3"
version = "3.41"
license = { file = "LICENSE.txt" }
dependencies = ["GitPython", "PyGithub", "matrix-client==0.4.0", "transformers", "huggingface-hub>0.20", "typer", "rich", "typing-extensions", "toml", "uv", "chardet"]
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://comfyregistry.org
# Used by Comfy Registry https://registry.comfy.org
[tool.comfy]
PublisherId = "drltdata"
+2 -2
View File
@@ -1,8 +1,8 @@
GitPython
PyGithub
matrix-client==0.4.0
matrix-nio
transformers
huggingface-hub>0.20
huggingface-hub
typer
rich
typing-extensions
+1211 -85
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -4,7 +4,7 @@ git clone https://github.com/ltdrdata/ComfyUI-Manager comfyui-manager
cd ..
python -m venv venv
source venv/bin/activate
python -m pip install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu121
python -m pip install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu130
python -m pip install -r requirements.txt
python -m pip install -r custom_nodes/comfyui-manager/requirements.txt
cd ..
+1 -1
View File
@@ -4,7 +4,7 @@ git clone https://github.com/ltdrdata/ComfyUI-Manager comfyui-manager
cd ..
python -m venv venv
call venv/Scripts/activate
python -m pip install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu121
python -m pip install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu130
python -m pip install -r requirements.txt
python -m pip install -r custom_nodes/comfyui-manager/requirements.txt
cd ..
+41
View File
@@ -0,0 +1,41 @@
"""Test-runner guard for the GOAL #32 tests/ modules.
WHY THIS FILE EXISTS (collection hazard, not test logic):
The repo root contains ``__init__.py`` the ComfyUI plugin entrypoint
which at import time appends ``glob/`` to sys.path and imports
``manager_server`` (which needs ``folder_paths`` / ``comfy.cli_args`` /
a constructed ``PromptServer``). pytest 8 collects any ancestor
directory that carries an ``__init__.py`` as a ``Package`` node and
IMPORTS that ``__init__.py`` during test setup (observed module name:
``__init__``). Outside a live ComfyUI process that import can never
succeed, so EVERY test under tests/ errors at setup including the
pre-existing tests/test_csrf_content_type_helper.py whenever pytest's
rootdir ends up at or above the repo root (e.g. running inside a git
worktree nested under the parent checkout).
The guard below pre-seeds ``sys.modules`` with an inert stub whose
``__file__`` matches the real path, so pytest's
``import_path(<repo-root>/__init__.py)`` resolves to the stub without
executing the plugin entrypoint. Conftest files load before the setup
phase, so the stub is always in place in time. This does NOT touch
production code and does NOT alter what the tests import themselves
(they use AST-extraction / subprocess isolation per the
tests/test_csrf_content_type_helper.py precedent ``glob/`` is never
added to the runner's sys.path).
"""
import sys
import types
from pathlib import Path
_REPO_ROOT = Path(__file__).resolve().parent.parent
_ROOT_INIT = _REPO_ROOT / "__init__.py"
if _ROOT_INIT.exists() and "__init__" not in sys.modules:
_stub = types.ModuleType("__init__")
_stub.__file__ = str(_ROOT_INIT)
_stub.__doc__ = (
"Inert stand-in for the ComfyUI-Manager plugin entrypoint; "
"see tests/conftest.py for rationale."
)
sys.modules["__init__"] = _stub
+169
View File
@@ -0,0 +1,169 @@
#!/usr/bin/env bash
# setup_e2e_env.sh — E2E environment builder for GOAL #60 (T1, spec §2).
#
# Builds the DISPOSABLE test ComfyUI root used by
# tests/e2e/test_e2e_install_flags.py. ENV BUILD ONLY: donor steps 4-5
# (editable pip install of the Manager + custom_nodes symlink) are
# deliberately DROPPED — the Manager is mounted via `git worktree add`
# by the `mount_worktree` session fixture in the test module, which is
# the SOLE owner of mount create/reuse/teardown (spec §2 T1
# single-ownership rule). This script never touches the Manager repo.
#
# Idempotent: re-run is a no-op when the marker + key artifacts exist
# (E2E-SC-01).
#
# Input env vars:
# E2E_COMFYUI_ROOT — target directory (default: mktemp -d)
# COMFYUI_BRANCH — ComfyUI clone ref (default: master; Q-2)
# PYTHON — python executable for version probe (default: python3)
#
# Output (last line of stdout):
# E2E_COMFYUI_ROOT=/path/to/environment
#
# Exit: 0=success, 1=failure
set -euo pipefail
COMFYUI_REPO="https://github.com/comfyanonymous/ComfyUI.git"
PYTORCH_CPU_INDEX="https://download.pytorch.org/whl/cpu"
# Minimal seed config. use_uv=false: the venv is seeded with pip
# (`uv venv --seed`), so the Manager's make_pip_cmd resolves to
# `<venv-python> -m pip` and the suite's pip-uninstall hygiene helpers
# work without uv on PATH at server runtime. The install flags are NOT
# seeded here — stage_flags.sh stages them per launch identity (SC-06).
CONFIG_INI_CONTENT="[default]
file_logging = false
use_uv = false"
log() { echo "[setup_e2e] $*"; }
err() { echo "[setup_e2e] ERROR: $*" >&2; }
die() { err "$@"; exit 1; }
validate_prerequisites() {
local py="${PYTHON:-python3}"
local missing=()
command -v git >/dev/null 2>&1 || missing+=("git")
command -v uv >/dev/null 2>&1 || missing+=("uv")
command -v "$py" >/dev/null 2>&1 || missing+=("$py")
if [[ ${#missing[@]} -gt 0 ]]; then
die "Missing prerequisites: ${missing[*]}"
fi
local py_version major minor
py_version=$("$py" -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')")
major="${py_version%%.*}"
minor="${py_version##*.}"
if [[ "$major" -lt 3 ]] || { [[ "$major" -eq 3 ]] && [[ "$minor" -lt 9 ]]; }; then
die "Python 3.9+ required, found $py_version"
fi
log "Prerequisites OK (python=$py_version)"
}
check_already_setup() {
local root="$1"
if [[ -f "$root/.e2e_setup_complete" ]] \
&& [[ -d "$root/comfyui" ]] \
&& [[ -d "$root/venv" ]] \
&& [[ -f "$root/comfyui/user/__manager/config.ini" ]]; then
log "Environment already set up at $root (marker exists). Skipping. (E2E-SC-01 idempotence)"
echo "E2E_COMFYUI_ROOT=$root"
exit 0
fi
}
verify_setup() {
local root="$1"
local venv_py="$root/venv/bin/python"
local errors=0
log "Running verification checks..."
[[ -f "$root/comfyui/main.py" ]] || { err "Verification FAIL: comfyui/main.py not found"; ((errors+=1)); }
[[ -x "$venv_py" ]] || { err "Verification FAIL: venv python not executable"; ((errors+=1)); }
[[ -f "$root/comfyui/user/__manager/config.ini" ]] || { err "Verification FAIL: config.ini not found"; ((errors+=1)); }
# venv must carry pip (uv venv --seed) — the suite's hygiene helpers
# and the Manager's reservation-consuming boot both call `-m pip`.
if ! "$venv_py" -m pip --version >/dev/null 2>&1; then
err "Verification FAIL: venv pip not available"
((errors+=1))
fi
# comfy is a local package inside the ComfyUI checkout
if ! PYTHONPATH="$root/comfyui" "$venv_py" -c "import comfy" 2>/dev/null; then
err "Verification FAIL: 'import comfy' failed"
((errors+=1))
fi
# [D2] half-check at setup time: the Manager must NOT be pip-installed
# into this venv (the worktree mount is the only delivery mechanism).
if "$venv_py" -m pip show comfyui-manager >/dev/null 2>&1 \
|| "$venv_py" -m pip show ComfyUI-Manager >/dev/null 2>&1; then
err "Verification FAIL: a pip-installed Manager exists in the venv (wrong layout for [D2])"
((errors+=1))
fi
if [[ "$errors" -gt 0 ]]; then
die "Verification failed with $errors error(s)"
fi
log "Verification OK: all checks passed"
}
# ===== Main =====
validate_prerequisites
PYTHON="${PYTHON:-python3}"
COMFYUI_BRANCH="${COMFYUI_BRANCH:-master}"
CREATED_BY_US=false
if [[ -z "${E2E_COMFYUI_ROOT:-}" ]]; then
E2E_COMFYUI_ROOT="$(mktemp -d -t e2e_comfyui_XXXXXX)"
CREATED_BY_US=true
log "Created E2E_COMFYUI_ROOT=$E2E_COMFYUI_ROOT"
else
mkdir -p "$E2E_COMFYUI_ROOT"
log "Using E2E_COMFYUI_ROOT=$E2E_COMFYUI_ROOT"
fi
check_already_setup "$E2E_COMFYUI_ROOT"
cleanup_on_failure() {
local exit_code=$?
if [[ "$exit_code" -ne 0 ]] && [[ "$CREATED_BY_US" == "true" ]]; then
err "Setup failed. Cleaning up $E2E_COMFYUI_ROOT"
rm -rf "$E2E_COMFYUI_ROOT"
fi
}
trap cleanup_on_failure EXIT
log "Step 1/5: Cloning ComfyUI (branch=$COMFYUI_BRANCH)..."
if [[ -d "$E2E_COMFYUI_ROOT/comfyui/.git" ]]; then
log " ComfyUI already cloned, skipping"
else
git clone --depth=1 --branch "$COMFYUI_BRANCH" "$COMFYUI_REPO" "$E2E_COMFYUI_ROOT/comfyui"
fi
log "Step 2/5: Creating virtual environment (seeded with pip)..."
if [[ -d "$E2E_COMFYUI_ROOT/venv" ]]; then
log " venv already exists, skipping"
else
uv venv --seed "$E2E_COMFYUI_ROOT/venv"
fi
VENV_PY="$E2E_COMFYUI_ROOT/venv/bin/python"
log "Step 3/5: Installing ComfyUI dependencies (CPU-only torch index)..."
uv pip install \
--python "$VENV_PY" \
-r "$E2E_COMFYUI_ROOT/comfyui/requirements.txt" \
--extra-index-url "$PYTORCH_CPU_INDEX"
log "Step 4/5: Writing seed config.ini + HOME isolation dirs..."
mkdir -p "$E2E_COMFYUI_ROOT/comfyui/user/__manager"
echo "$CONFIG_INI_CONTENT" > "$E2E_COMFYUI_ROOT/comfyui/user/__manager/config.ini"
mkdir -p "$E2E_COMFYUI_ROOT/home/.config"
mkdir -p "$E2E_COMFYUI_ROOT/home/.local/share"
mkdir -p "$E2E_COMFYUI_ROOT/logs"
mkdir -p "$E2E_COMFYUI_ROOT/comfyui/custom_nodes"
log "Step 5/5: Verifying setup..."
verify_setup "$E2E_COMFYUI_ROOT"
# Marker written ONLY after verification passes (E2E-SC-01)
date -Iseconds > "$E2E_COMFYUI_ROOT/.e2e_setup_complete"
trap - EXIT
log "Setup complete."
echo "E2E_COMFYUI_ROOT=$E2E_COMFYUI_ROOT"
+82
View File
@@ -0,0 +1,82 @@
#!/usr/bin/env bash
# stage_flags.sh — Per-launch-identity config staging (T4, spec §1.4).
#
# Analog of the donor's start_comfyui_strict.sh config patching, split
# out as a pure staging script (launch is a separate step; the pytest
# fixture owns restore+delete of the backup at teardown — donor
# symmetry, spec §1.4).
#
# Modes (arg $1):
# deny — REMOVE both flag keys (L-D: flags ABSENT also live-proves
# "missing key reads false")
# allow — set allow_git_url_install = true AND allow_pip_install = true
# (L-A / L-P)
#
# Backup: config.ini.before-flags, created ONLY if not already present
# (crashed-run-safe — preserves the true baseline across crashed runs).
# Restore + DELETE of the backup happens in the pytest fixture teardown,
# NOT here (E2E-SC-06/07).
#
# Input env vars:
# E2E_COMFYUI_ROOT — (required)
#
# Exit: 0=staged, 1=failure
set -euo pipefail
MODE="${1:-}"
log() { echo "[stage_flags] $*"; }
err() { echo "[stage_flags] ERROR: $*" >&2; }
die() { err "$@"; exit 1; }
[[ -n "${E2E_COMFYUI_ROOT:-}" ]] || die "E2E_COMFYUI_ROOT is not set"
[[ "$MODE" == "deny" || "$MODE" == "allow" ]] || die "usage: stage_flags.sh deny|allow"
CONFIG="$E2E_COMFYUI_ROOT/comfyui/user/__manager/config.ini"
BACKUP="$CONFIG.before-flags"
[[ -f "$CONFIG" ]] || die "config not found at $CONFIG (run setup_e2e_env.sh first)"
# Backup ONLY if absent (crashed-run-safe; SC-06)
if [[ ! -f "$BACKUP" ]]; then
cp "$CONFIG" "$BACKUP"
log "Backed up original config to $BACKUP"
else
log "Backup already present at $BACKUP (preserving original baseline)"
fi
stage_key() {
local key="$1" value="$2"
if grep -qE "^${key}\s*=" "$CONFIG"; then
sed -i -E "s|^${key}\s*=.*|${key} = ${value}|" "$CONFIG"
else
# The append-after target MUST exist, otherwise the sed below is a
# silent no-op and the flag never lands (false-PASS for the allow arm).
grep -qE "^\[default\]" "$CONFIG" \
|| die "no [default] section in $CONFIG — cannot stage '${key}' (would silently no-op)"
sed -i -E "/^\[default\]/a ${key} = ${value}" "$CONFIG"
fi
}
remove_key() {
local key="$1"
sed -i -E "/^${key}\s*=/d" "$CONFIG"
}
case "$MODE" in
deny)
remove_key "allow_git_url_install"
remove_key "allow_pip_install"
log "Staged DENY config (both flags ABSENT — missing key reads false)"
;;
allow)
stage_key "allow_git_url_install" "true"
stage_key "allow_pip_install" "true"
log "Staged ALLOW config (both flags = true)"
;;
esac
# The staged value takes effect ONLY on the NEXT launch (restart-only
# cached_config — by construction, no hot-reload assumption; SC-06).
log "Staged config at $CONFIG (effective on next launch)"
+142
View File
@@ -0,0 +1,142 @@
#!/usr/bin/env bash
# start_comfyui.sh — Foreground-blocking ComfyUI launcher (T2, spec §1.3).
#
# Starts ComfyUI in the background and blocks until the server answers
# GET /system_stats (or timeout). Deltas vs the donor script (binding,
# spec §1.3):
# - NO --enable-manager: the Manager is a custom-node-style plugin
# activated by ComfyUI's custom_nodes scan of the worktree mount.
# - NO COMFYUI_MANAGER_SKIP_MANAGER_REQUIREMENTS: that belt does not
# exist in this repo (Q-6 verified); watchdog row E2E-SC-42 covers
# the residual.
# - --listen is ALWAYS passed explicitly (LISTEN env): the predicate
# under test is request-time `flag AND is_loopback(args.listen)` —
# the listener value is LOAD-BEARING.
# - Per-launch log isolation (E2E-SC-04, MANDATORY): each launch
# identity writes a FRESH log file comfyui.<port>.<launch-id>.log,
# so a deny-copy substring from L-D is unfindable by L-A/R-A log
# assertions (stale-substring false-PASS class).
# - Readiness = poll GET /system_stats == 200; child exit code 0
# during the wait = Manager-triggered restart -> KEEP polling;
# non-zero exit -> fail fast with log tail.
#
# Input env vars:
# E2E_COMFYUI_ROOT — (required) root from setup_e2e_env.sh
# PORT — listen port (default: 8189)
# TIMEOUT — max seconds to readiness (default: 120)
# LISTEN — listener address (default: 127.0.0.1)
# LAUNCH_ID — launch identity tag for the log file (default: default)
#
# Output (last line on success):
# COMFYUI_PID=<pid> PORT=<port> LOG_FILE=<path>
#
# Exit: 0=ready, 1=timeout/failure
set -euo pipefail
PORT="${PORT:-8189}"
TIMEOUT="${TIMEOUT:-120}"
LISTEN="${LISTEN:-127.0.0.1}"
LAUNCH_ID="${LAUNCH_ID:-default}"
log() { echo "[start_comfyui] $*"; }
err() { echo "[start_comfyui] ERROR: $*" >&2; }
die() { err "$@"; exit 1; }
[[ -n "${E2E_COMFYUI_ROOT:-}" ]] || die "E2E_COMFYUI_ROOT is not set"
[[ -d "$E2E_COMFYUI_ROOT/comfyui" ]] || die "ComfyUI not found at $E2E_COMFYUI_ROOT/comfyui"
[[ -x "$E2E_COMFYUI_ROOT/venv/bin/python" ]] || die "venv python not found"
[[ -f "$E2E_COMFYUI_ROOT/.e2e_setup_complete" ]] || die "Setup marker not found. Run setup_e2e_env.sh first."
PY="$E2E_COMFYUI_ROOT/venv/bin/python"
COMFY_DIR="$E2E_COMFYUI_ROOT/comfyui"
LOG_DIR="$E2E_COMFYUI_ROOT/logs"
LOG_FILE="$LOG_DIR/comfyui.${PORT}.${LAUNCH_ID}.log"
# Port-namespaced PID file (donor WI-CC incident: shared PID file caused
# a cross-port kill).
PID_FILE="$LOG_DIR/comfyui.${PORT}.pid"
mkdir -p "$LOG_DIR"
# --- Pre-launch port clear ---
if ss -tlnp 2>/dev/null | grep -q ":${PORT}\b"; then
log "Port $PORT is in use. Attempting to stop existing process..."
if [[ -f "$PID_FILE" ]]; then
OLD_PID="$(cat "$PID_FILE")"
if kill -0 "$OLD_PID" 2>/dev/null; then
kill "$OLD_PID" 2>/dev/null || true
sleep 2
fi
fi
if ss -tlnp 2>/dev/null | grep -q ":${PORT}\b"; then
pkill -f "main\\.py.*--port $PORT" 2>/dev/null || true
sleep 2
fi
if ss -tlnp 2>/dev/null | grep -q ":${PORT}\b"; then
die "Port $PORT is still in use after cleanup attempt"
fi
log "Port $PORT cleared."
fi
# --- Launch (FRESH per-launch log file) ---
log "Starting ComfyUI on port $PORT (listen=$LISTEN, launch_id=$LAUNCH_ID)..."
: > "$LOG_FILE"
PYTHONUNBUFFERED=1 \
HOME="$E2E_COMFYUI_ROOT/home" \
nohup "$PY" -u "$COMFY_DIR/main.py" \
--cpu \
--port "$PORT" \
--listen "$LISTEN" \
> "$LOG_FILE" 2>&1 &
COMFYUI_PID=$!
echo "$COMFYUI_PID" > "$PID_FILE"
log "ComfyUI PID=$COMFYUI_PID, log=$LOG_FILE"
# --- Block until ready: poll /system_stats (restart-tolerant) ---
log "Waiting up to ${TIMEOUT}s for ComfyUI readiness (GET /system_stats)..."
DEADLINE=$(( $(date +%s) + TIMEOUT ))
READY=0
while [[ "$(date +%s)" -lt "$DEADLINE" ]]; do
if curl -sf --max-time 2 "http://127.0.0.1:${PORT}/system_stats" >/dev/null 2>&1; then
READY=1
break
fi
# Child exit handling: exit 0 = Manager-triggered restart -> keep
# polling (a restarted process will bind the port); non-zero -> fail fast.
if ! kill -0 "$COMFYUI_PID" 2>/dev/null; then
if wait "$COMFYUI_PID" 2>/dev/null; then
: # exit 0 — restart class, keep polling
else
RC=$?
err "ComfyUI exited with code $RC. Last 30 lines of log:"
tail -n 30 "$LOG_FILE" >&2
rm -f "$PID_FILE"
exit 1
fi
fi
sleep 1
done
if [[ "$READY" -ne 1 ]]; then
err "Timeout (${TIMEOUT}s) waiting for ComfyUI. Last 30 lines of log:"
tail -n 30 "$LOG_FILE" >&2
kill "$COMFYUI_PID" 2>/dev/null || true
rm -f "$PID_FILE"
exit 1
fi
# A Manager-triggered restart (child exit 0 above) leaves COMFYUI_PID pointing
# at the dead original child while a FRESH process now owns the port. Re-resolve
# the live listener PID so PID_FILE (consumed by stop_comfyui.sh) targets the
# process that must actually be killed at teardown.
LISTENER_PID="$(ss -tlnp 2>/dev/null | grep ":${PORT}\b" | grep -oE 'pid=[0-9]+' | head -n1 | cut -d= -f2)"
if [[ -n "$LISTENER_PID" && "$LISTENER_PID" != "$COMFYUI_PID" ]]; then
log "Listener PID $LISTENER_PID differs from launched PID $COMFYUI_PID (restart). Updating PID file."
COMFYUI_PID="$LISTENER_PID"
echo "$COMFYUI_PID" > "$PID_FILE"
fi
log "ComfyUI is ready."
echo "COMFYUI_PID=$COMFYUI_PID PORT=$PORT LOG_FILE=$LOG_FILE"
+101
View File
@@ -0,0 +1,101 @@
#!/usr/bin/env bash
# stop_comfyui.sh — Graceful ComfyUI shutdown (T3, spec §1.3 stop contract).
#
# Donor mirror: SIGTERM -> 10s grace -> SIGKILL -> port-pattern pkill
# fallback -> port-free verify (incl. the legacy-PID-file warning from
# the donor WI-CC cross-port-kill incident).
#
# Input env vars:
# E2E_COMFYUI_ROOT — (required) path to the E2E environment
# PORT — ComfyUI port (default: 8189)
#
# Exit: 0=stopped, 1=failed
set -euo pipefail
PORT="${PORT:-8189}"
GRACE_PERIOD=10
log() { echo "[stop_comfyui] $*"; }
err() { echo "[stop_comfyui] ERROR: $*" >&2; }
die() { err "$@"; exit 1; }
[[ -n "${E2E_COMFYUI_ROOT:-}" ]] || die "E2E_COMFYUI_ROOT is not set"
# Ownership guard: only signal a PID whose cmdline references THIS E2E root.
# On shared runners a bare "main.py --port N" pattern (or a reused PID) could
# otherwise match an unrelated process; the launcher invokes
# "$E2E_COMFYUI_ROOT/venv/bin/python $E2E_COMFYUI_ROOT/comfyui/main.py", so the
# root path is always present in our process's cmdline.
belongs_to_root() {
local pid="$1"
[[ -n "$pid" && -r "/proc/$pid/cmdline" ]] || return 1
tr '\0' ' ' < "/proc/$pid/cmdline" 2>/dev/null | grep -qF "$E2E_COMFYUI_ROOT"
}
# PIDs currently listening on PORT (deduped).
listener_pids() {
ss -tlnp 2>/dev/null | grep ":${PORT}\b" | grep -oE 'pid=[0-9]+' | cut -d= -f2 | sort -u
}
PID_FILE="$E2E_COMFYUI_ROOT/logs/comfyui.${PORT}.pid"
LEGACY_PID_FILE="$E2E_COMFYUI_ROOT/logs/comfyui.pid"
if [[ -f "$LEGACY_PID_FILE" ]] && [[ ! -f "$PID_FILE" ]]; then
log "WARN: found legacy unported PID file $LEGACY_PID_FILE but no ${PID_FILE}. Cross-port risk — ignoring legacy file."
fi
COMFYUI_PID=""
if [[ -f "$PID_FILE" ]]; then
COMFYUI_PID="$(cat "$PID_FILE")"
log "Read PID=$COMFYUI_PID from $PID_FILE"
fi
if [[ -n "$COMFYUI_PID" ]] && kill -0 "$COMFYUI_PID" 2>/dev/null; then
if belongs_to_root "$COMFYUI_PID"; then
log "Sending SIGTERM to PID $COMFYUI_PID..."
kill "$COMFYUI_PID" 2>/dev/null || true
elapsed=0
while kill -0 "$COMFYUI_PID" 2>/dev/null && [[ "$elapsed" -lt "$GRACE_PERIOD" ]]; do
sleep 1
elapsed=$((elapsed + 1))
done
if kill -0 "$COMFYUI_PID" 2>/dev/null; then
log "Process still alive after ${GRACE_PERIOD}s. Sending SIGKILL..."
kill -9 "$COMFYUI_PID" 2>/dev/null || true
sleep 1
fi
else
log "WARN: PID $COMFYUI_PID does NOT belong to $E2E_COMFYUI_ROOT (reused/stale PID). Refusing to kill it."
fi
fi
# Fallback: kill the port listener(s) (covers Manager-restarted processes whose
# PID differs from the recorded one) — but ONLY those verified to belong to this
# E2E root, never a broad pattern pkill that could hit unrelated CI processes.
if ss -tlnp 2>/dev/null | grep -q ":${PORT}\b"; then
log "Port $PORT still in use. Killing verified-own listener(s)..."
for pid in $(listener_pids); do
if belongs_to_root "$pid"; then
log "Sending SIGTERM to listener PID $pid..."
kill "$pid" 2>/dev/null || true
else
log "WARN: PID $pid on port $PORT is not part of $E2E_COMFYUI_ROOT — leaving it alone."
fi
done
sleep 2
for pid in $(listener_pids); do
if belongs_to_root "$pid"; then
log "Listener PID $pid still alive. Sending SIGKILL..."
kill -9 "$pid" 2>/dev/null || true
fi
done
sleep 1
fi
rm -f "$PID_FILE"
if ss -tlnp 2>/dev/null | grep -q ":${PORT}\b"; then
die "Port $PORT is still in use after shutdown"
fi
log "ComfyUI stopped."
+752
View File
@@ -0,0 +1,752 @@
"""GOAL #60 — Real-server E2E for the dedicated install flags (worktree-mounted Manager).
Boots a REAL ComfyUI server from a disposable test root
(`E2E_COMFYUI_ROOT`, built by tests/e2e/scripts/setup_e2e_env.sh) with
the Manager mounted via `git worktree add --detach` (NEVER pip-installed
[D2]) and exercises both dedicated-flag surfaces over live HTTP.
Usage:
bash tests/e2e/scripts/setup_e2e_env.sh # once (E2E-SC-01)
E2E_COMFYUI_ROOT=/path/to/root pytest tests/e2e/test_e2e_install_flags.py -v
Per-row map (goal60-scenarios.md, 24 rows spec §3 BINDING):
SC-01 setup_e2e_env.sh (pre-suite script; idempotent build + marker not a pytest test)
SC-02 mount_worktree fixture create path (SHA pin, .git-file, no-pip, printed SHA)
SC-03 mount_worktree fixture reuse path + path-prefix scoping invariant
SC-04 _start_server via start_comfyui.sh (readiness poll, restart tolerance,
per-launch log comfyui.<port>.<launch-id>.log)
SC-05 test_00_smoke_manager_version in EVERY server-up class (+abort guard)
SC-06 _stage via stage_flags.sh (backup-if-absent; restart-only by construction)
SC-07 class fixture finalizers (stop + port-free + config restore + backup DELETE)
+ mount_worktree finalizer (unmount + prune + absence assert)
SC-10 TestDenyArms.test_01_sa_deny SC-11 TestDenyArms.test_02_sb_deny
SC-12 TestAllowArms.test_01_git_url_allow
SC-13 TestAllowArms.test_02_pip_allow_reserved (anti-false-PASS VERBATIM)
SC-14 TestAllowArms.test_03_restart_consumes_reservation (R-A through the holder)
SC-20 _pre_guards in both class fixtures (before any request)
SC-21 TestAllowArms.test_04_clone_residual_cleanup (+ installed-index cross-check)
SC-22 TestAllowArms.test_05_pip_residual_uninstall
SC-23 _reservation_guard UNCONDITIONAL fixture-teardown guard (failure path)
SC-24 TestZeroResidual.test_99_zero_residual_sweep (unmount half lives in the
mount_worktree finalizer it cannot be asserted from inside the session)
SC-30 module-level pytestmark (env unset -> all SKIP; unit suite unaffected)
SC-31 module-level pytestmark (marker absent -> all SKIP)
SC-32 needs_network marker on fixture-dependent (allow-arm / public) rows
SC-33 collection safety by construction: stdlib + pytest + requests
(via pytest.importorskip) ONLY no glob/ imports, no server imports,
HTTP only at test time
SC-40 TestPublicListener (opt-in E2E_PUBLIC_LISTEN=1; L-P @ 0.0.0.0)
SC-41 batch S-C/S-C' E2E — DEFERRED (Q-5; spec FREEZE item 3; recorded here)
SC-42 TestAllowArms.test_06_requirements_watchdog (L-A launch log)
Fixture-lifecycle ownership (spec §3 BINDING block):
- every class server fixture DECLARES mount_worktree (mount-before-launch);
- process handle lives in a MUTABLE ServerHolder owned by the fixture;
teardown stops the CURRENT holder content (whatever launch identity is live);
- SC-14 restarts THROUGH the holder (stop L-A -> launch R-A -> replace handle);
- stop-before-next-class is structural (pytest class-fixture scoping);
- `requests` is imported via pytest.importorskip (absence degrades to SKIP).
T6 note: no tests/e2e/conftest.py all fixtures are single-module, so the
optional T6 file is not demanded (spec §2 T6 condition not met).
"""
from __future__ import annotations
import configparser
import os
import shutil
import socket
import subprocess
import sys
import time
import uuid
from pathlib import Path
import pytest
# ---------------------------------------------------------------------------
# Skip gates (E2E-SC-30/31) — BEFORE anything env-dependent
# ---------------------------------------------------------------------------
E2E_ROOT = os.environ.get("E2E_COMFYUI_ROOT", "")
_MARKER_OK = bool(E2E_ROOT) and os.path.isfile(os.path.join(E2E_ROOT, ".e2e_setup_complete"))
pytestmark = pytest.mark.skipif(
not _MARKER_OK,
reason="E2E_COMFYUI_ROOT not set or E2E environment not ready (.e2e_setup_complete missing)",
)
# requests: test-extra — absence degrades to SKIP, never a collection error
# (spec §3 binding block item 5; [D4]).
requests = pytest.importorskip("requests")
# ---------------------------------------------------------------------------
# Constants / paths
# ---------------------------------------------------------------------------
PORT = int(os.environ.get("PORT", "8189"))
TIMEOUT = int(os.environ.get("TIMEOUT", "120"))
BASE_URL = f"http://127.0.0.1:{PORT}"
THIS_DIR = Path(__file__).resolve().parent
SCRIPTS_DIR = THIS_DIR / "scripts"
MANAGER_REPO = THIS_DIR.parents[1] # repo root of the checkout running the suite
ROOT = Path(E2E_ROOT) if E2E_ROOT else Path(".")
COMFY_DIR = ROOT / "comfyui"
CN_DIR = COMFY_DIR / "custom_nodes"
MOUNT = CN_DIR / "comfyui-manager"
CFG = COMFY_DIR / "user" / "__manager" / "config.ini"
CFG_BACKUP = Path(str(CFG) + ".before-flags")
SCRIPTS_FILE = COMFY_DIR / "user" / "__manager" / "startup-scripts" / "install-scripts.txt"
LOGS_DIR = ROOT / "logs"
VENV_PY = ROOT / "venv" / "bin" / "python"
# Owned fixtures ONLY (goal60-scenarios.md Conventions; [D3])
NODEPACK_URL = "https://github.com/ltdrdata/nodepack-test1-do-not-install"
PACK_NAME = "nodepack-test1-do-not-install"
# pip stimulus uses the git+ scheme: pip/uv require it for VCS URLs — a
# plain GitHub repo URL serves HTML and cannot install (verified by probe;
# spec amendment requested via leader pushback 2026-06-08; the SC-13/14
# oracle itself is encoded VERBATIM).
PIP_URL = "git+https://github.com/ltdrdata/pip-test1-do-not-install"
PIP_PKG = "pip-test1-do-not-install"
PIP_IMPORT = "pip_test1_do_not_install"
PIP_MARKER = "pip-test1-do-not-install:ok"
# Amendment A2 (live-run finding, leader-approved): the S-A nodepack fixture
# is deliberately NOT zero-dep — it pins python-slugify==8.0.4 in its
# requirements as the invariant-4 ride-along proof vehicle. The SC-42
# watchdog therefore allowlists exactly that requirement, and the
# transitive-dep residual class is swept at allow-class teardown + SC-24.
# (The S-B pip fixture IS zero-dep as documented.)
NODEPACK_PINNED_REQ = "python-slugify==8.0.4"
TRANSITIVE_DEPS = ("python-slugify", "text-unidecode")
POLL_TIMEOUT = 60
POLL_INTERVAL = 1.0
# Distinctive substrings of the flag-naming denial constants @ d45c8e6b
DENY_COPY_GIT = "'allow_git_url_install = true' in config.ini"
DENY_COPY_PIP = "'allow_pip_install = true' in config.ini"
# Old security_level-attributing copy (must NOT appear on flag denials)
OLD_COPY_GENERAL = "is not allowed in this security_level"
OLD_COPY_NORMAL_MINUS = "set the security level to"
# ---------------------------------------------------------------------------
# Network probe (E2E-SC-32) — evaluated ONLY when the env gate is open, so
# collection without the env performs no network IO (SC-33).
# ---------------------------------------------------------------------------
def _network_available() -> bool:
try:
with socket.create_connection(("github.com", 443), timeout=5):
return True
except OSError:
return False
_NETWORK = _network_available() if _MARKER_OK else False
needs_network = pytest.mark.skipif(
not _NETWORK,
reason="github.com unreachable — network-dependent fixture row skipped (E2E-SC-32)",
)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _run(cmd, check=False, timeout=180, env=None, cwd=None):
return subprocess.run(
cmd, capture_output=True, text=True, timeout=timeout, check=check,
env=env, cwd=cwd,
)
def _script(name: str) -> str:
return str(SCRIPTS_DIR / name)
def _script_env(**extra) -> dict:
env = {**os.environ, "E2E_COMFYUI_ROOT": str(ROOT), "PORT": str(PORT),
"TIMEOUT": str(TIMEOUT)}
env.update({k: str(v) for k, v in extra.items()})
return env
def _pack_dir(name: str = PACK_NAME) -> Path:
return CN_DIR / name
def _pack_exists(name: str = PACK_NAME) -> bool:
return _pack_dir(name).is_dir()
def _remove_pack(name: str = PACK_NAME) -> None:
"""Donor _remove_pack pattern: rmtree 3-retry + rename-to-.trash_ fallback."""
path = _pack_dir(name)
if path.is_symlink():
path.unlink()
return
if not path.is_dir():
return
for attempt in range(3):
try:
shutil.rmtree(path)
return
except OSError:
if attempt < 2:
time.sleep(1)
trash = CN_DIR / f".trash_{uuid.uuid4().hex[:8]}"
try:
os.rename(path, trash)
shutil.rmtree(trash, ignore_errors=True)
except OSError:
shutil.rmtree(path, ignore_errors=True)
def _wait_for(predicate, timeout=POLL_TIMEOUT, interval=POLL_INTERVAL) -> bool:
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
if predicate():
return True
time.sleep(interval)
return False
def _pip_import_rc() -> int:
return _run([str(VENV_PY), "-c", f"import {PIP_IMPORT}"]).returncode
def _pip_marker_rc() -> "subprocess.CompletedProcess":
return _run([
str(VENV_PY), "-c",
f"import {PIP_IMPORT} as m; assert m.MARKER == '{PIP_MARKER}'",
])
def _pip_uninstall() -> "subprocess.CompletedProcess":
return _run([str(VENV_PY), "-m", "pip", "uninstall", "-y", PIP_PKG])
def _scripts_clean() -> bool:
"""True when the reservation file is absent OR carries no pip-test1 line."""
if not SCRIPTS_FILE.exists():
return True
return "pip-test1" not in SCRIPTS_FILE.read_text(errors="ignore")
def _reservation_guard() -> None:
"""E2E-SC-23 — UNCONDITIONAL teardown guard for the unconsumed-reservation
leak class: a leaked line would pip-install on ANY next boot of this root."""
if SCRIPTS_FILE.exists() and "pip-test1" in SCRIPTS_FILE.read_text(errors="ignore"):
SCRIPTS_FILE.unlink()
assert _scripts_clean(), "reservation guard failed to clear pip-test1 line (SC-23)"
def _restore_config() -> None:
"""E2E-SC-07: restore from backup, then DELETE the backup and assert absence
(a surviving stale backup would silently restore an outdated config at a
FUTURE run's teardown via the create-only-if-absent rule)."""
if CFG_BACKUP.exists():
shutil.copyfile(CFG_BACKUP, CFG)
CFG_BACKUP.unlink()
assert not CFG_BACKUP.exists(), "config backup must be DELETED after restore (SC-07/24)"
def _port_free() -> bool:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
s.settimeout(1)
return s.connect_ex(("127.0.0.1", PORT)) != 0
finally:
s.close()
def _pre_guards() -> None:
"""E2E-SC-20 — before EACH arm's matrix rows; assert all three."""
_remove_pack(PACK_NAME)
assert not _pack_exists(PACK_NAME), (
f"pre-guard: failed to clean {PACK_NAME} (file locks?)"
)
_pip_uninstall() # ignore rc: not-installed is fine
assert _pip_import_rc() != 0, "pre-guard: pip fixture importable before test"
_reservation_guard()
assert _scripts_clean(), "pre-guard: stale pip-test1 reservation present"
# ---------------------------------------------------------------------------
# Server lifecycle (E2E-SC-04 + spec §3 binding holder contract)
# ---------------------------------------------------------------------------
class ServerHolder:
"""Mutable process-handle holder owned by the class server fixture.
The holder always points at the CURRENT launch identity; SC-14 replaces
its content when it restarts through it, so class teardown stops
whatever is live no orphan."""
def __init__(self):
self.launch_id: str | None = None
self.log_path: Path | None = None
self.live = False
self.smoke_ok = False
def _stage(mode: str) -> None:
r = _run(["bash", _script("stage_flags.sh"), mode], env=_script_env(), check=False)
assert r.returncode == 0, f"stage_flags.sh {mode} failed:\n{r.stdout}\n{r.stderr}"
assert CFG_BACKUP.exists(), "backup must exist after staging (SC-06)"
def _start_server(holder: ServerHolder, launch_id: str, listen: str = "127.0.0.1") -> None:
r = _run(
["bash", _script("start_comfyui.sh")],
env=_script_env(LISTEN=listen, LAUNCH_ID=launch_id),
timeout=TIMEOUT + 90,
)
assert r.returncode == 0, (
f"start_comfyui.sh failed for launch {launch_id}:\n{r.stdout}\n{r.stderr}"
)
holder.launch_id = launch_id
holder.log_path = LOGS_DIR / f"comfyui.{PORT}.{launch_id}.log"
holder.live = True
assert holder.log_path.is_file(), "per-launch log file missing (SC-04)"
def _stop_server(holder: ServerHolder) -> None:
if not holder.live:
return
r = _run(["bash", _script("stop_comfyui.sh")], env=_script_env(), timeout=120)
assert r.returncode == 0, f"stop_comfyui.sh failed:\n{r.stdout}\n{r.stderr}"
holder.live = False
assert _port_free(), "port still bound after stop (SC-07)"
def _launch_log(holder: ServerHolder) -> str:
assert holder.log_path is not None and holder.log_path.is_file()
return holder.log_path.read_text(errors="ignore")
def _named_log(launch_id: str) -> str:
p = LOGS_DIR / f"comfyui.{PORT}.{launch_id}.log"
assert p.is_file(), f"launch log for {launch_id} missing"
return p.read_text(errors="ignore")
def _require_smoke(holder: ServerHolder) -> None:
"""SC-05 abort semantics: matrix rows refuse to run after a smoke failure
so a mount/activation problem cannot produce misleading 404 results."""
if not holder.smoke_ok:
pytest.fail(
"aborting matrix row: smoke (GET /manager/version) has not passed "
"for this launch — mount/activation problem or Q-2 bundled-manager "
"collision (E2E-SC-05)"
)
def _smoke(holder: ServerHolder) -> None:
r = requests.get(f"{BASE_URL}/manager/version", timeout=10)
assert r.status_code == 200, (
f"smoke FAILED: GET /manager/version -> {r.status_code}; the "
f"worktree-mounted plugin did not register its routes (E2E-SC-05). "
f"Log tail:\n{_launch_log(holder)[-2000:]}"
)
assert r.text.strip(), "smoke: /manager/version body empty"
holder.smoke_ok = True
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture(scope="session")
def mount_worktree():
"""E2E-SC-02/03 — SOLE owner of mount create / reuse-verify / teardown."""
ref = os.environ.get("E2E_MANAGER_REF", "HEAD")
r = _run(["git", "-C", str(MANAGER_REPO), "rev-parse", f"{ref}^{{commit}}"], check=True)
sha = r.stdout.strip()
# Scoping invariant (SC-03): the mount path is {ROOT}-prefixed and is
# NEVER under the members' .claude/worktrees tree. Every mount/teardown
# command below references ONLY this path.
mount = MOUNT.resolve()
assert ".claude/worktrees" not in str(mount).replace(os.sep, "/"), (
"mount path must never live under member worktrees (SC-03 scoping)"
)
assert str(mount).startswith(str(ROOT.resolve())), (
"mount path must be {ROOT}-prefixed (SC-03 scoping)"
)
porcelain = _run(["git", "-C", str(MANAGER_REPO), "worktree", "list", "--porcelain"]).stdout
if f"worktree {mount}" in porcelain:
# Reuse path (SC-03)
head = _run(["git", "-C", str(mount), "rev-parse", "HEAD"]).stdout.strip()
if head != sha:
_run(["git", "-C", str(mount), "checkout", "--detach", sha], check=True)
else:
# Create path (SC-02)
_run(["git", "-C", str(MANAGER_REPO), "worktree", "add", "--detach",
str(mount), sha], check=True)
# From here a worktree exists at `mount`. Any failure between now and the
# yield (the verify asserts below) must STILL run teardown — otherwise a
# failed setup leaks an orphaned worktree into the next session (review
# follow-up). Hence the try/finally wraps verify + yield, not just yield.
try:
# Verify (every session)
head = _run(["git", "-C", str(mount), "rev-parse", "HEAD"]).stdout.strip()
assert head == sha, f"mount HEAD {head} != expected {sha} (SC-02)"
print(f"[mount_worktree] Manager mounted at {mount} @ SHA {sha}") # [D2] traceability
assert (mount / ".git").is_file(), (
".git in the mount must be a FILE (gitdir pointer) — worktree layout (SC-02)"
)
# [D2] other half: no pip-installed Manager in the venv; per MM §2.2 no
# assertion anywhere relies on the mounted Manager's OWN version/remote
# self-report (.git-file degradation accepted by design — spec R5).
for dist in ("comfyui-manager", "ComfyUI-Manager"):
rc = _run([str(VENV_PY), "-m", "pip", "show", dist]).returncode
assert rc != 0, f"pip-installed Manager '{dist}' found in venv — violates [D2]"
yield {"path": mount, "sha": sha}
finally:
if os.environ.get("E2E_KEEP_MOUNT"):
print(f"[mount_worktree] E2E_KEEP_MOUNT set — keeping {mount}")
else:
# Exception-safe: prune + absence asserts run even when remove fails
# (review iter-2 — crash residue must still be surfaced honestly).
try:
_run(["git", "-C", str(MANAGER_REPO), "worktree", "remove", "--force", str(mount)],
check=True)
finally:
_run(["git", "-C", str(MANAGER_REPO), "worktree", "prune"])
porcelain = _run(["git", "-C", str(MANAGER_REPO), "worktree", "list", "--porcelain"]).stdout
assert f"worktree {mount}" not in porcelain, "mount still listed after remove (SC-07)"
assert not mount.exists(), "mount dir still present after remove (SC-07)"
@pytest.fixture(scope="class")
def deny_server(mount_worktree):
"""L-D: both flags ABSENT (live 'missing key reads false'), loopback."""
_pre_guards() # SC-20
_stage("deny") # SC-06
holder = ServerHolder()
_start_server(holder, "L-D") # SC-04
yield holder
# Exception-safe teardown chain (review iter-2 must-fix): a failing
# stop is exactly the crashed-run shape SC-23 exists for — the guard
# and the config restore MUST run regardless.
try:
_stop_server(holder) # SC-07 (current handle, whatever is live)
finally:
try:
_reservation_guard() # SC-23 — UNCONDITIONAL
finally:
_restore_config() # SC-07: restore + DELETE backup
@pytest.fixture(scope="class")
def allow_server(mount_worktree):
"""L-A: both flags true, loopback. SC-14 mutates the holder to R-A."""
_pre_guards() # SC-20 (re-guards before the allow arm)
_stage("allow") # SC-06
holder = ServerHolder()
_start_server(holder, "L-A")
yield holder
# Exception-safe teardown chain (review iter-2 must-fix): every
# residual guard runs even when the stop (or an earlier sweep step)
# raises — SC-23 is contractually UNCONDITIONAL (R3 leak class).
try:
_stop_server(holder) # stops the CURRENT identity (L-A or R-A)
finally:
try:
_remove_pack(PACK_NAME) # defensive re-sweep (primary assert is SC-21)
_pip_uninstall() # defensive (primary assert is SC-22)
# Amendment A2: sweep the S-A fixture's transitive-dep residual
# class (python-slugify + text-unidecode ride the git
# transaction; verified NOT in ComfyUI's own requirements).
_run([str(VENV_PY), "-m", "pip", "uninstall", "-y", *TRANSITIVE_DEPS])
finally:
try:
_reservation_guard() # SC-23 — UNCONDITIONAL (failure path cover)
finally:
_restore_config()
@pytest.fixture(scope="class")
def public_server(mount_worktree):
"""L-P (opt-in, Q-7): both flags true, 0.0.0.0 listener."""
_pre_guards()
_stage("allow")
holder = ServerHolder()
_start_server(holder, "L-P", listen="0.0.0.0")
yield holder
# Exception-safe teardown chain (review iter-2 must-fix).
try:
_stop_server(holder)
finally:
try:
_reservation_guard()
finally:
_restore_config()
# ---------------------------------------------------------------------------
# Classes — definition order IS execution order (deny first on the fresh env)
# ---------------------------------------------------------------------------
class TestDenyArms:
"""L-D launch: SC-05 smoke, SC-10, SC-11 (deny rows are offline-safe —
denial happens before any network access)."""
def test_00_smoke_manager_version(self, deny_server):
_smoke(deny_server) # SC-05
def test_01_sa_deny(self, deny_server):
"""E2E-SC-10: S-A deny — 403 + exact flag token + no artifact + honest log."""
_require_smoke(deny_server)
r = requests.post(f"{BASE_URL}/customnode/install/git_url",
json={"url": NODEPACK_URL}, timeout=30)
assert r.status_code == 403
assert r.json() == {"error": "allow_git_url_install"}, (
f"deny body must carry the flag token, got {r.text!r}"
)
assert not _pack_exists(PACK_NAME), "clone artifact created on DENY (SC-10)"
log = _launch_log(deny_server)
assert DENY_COPY_GIT in log, "flag-naming denial copy missing from L-D log"
assert OLD_COPY_GENERAL not in log and OLD_COPY_NORMAL_MINUS not in log, (
"denial attributed to security_level — honest-copy violation (SC-10)"
)
def test_02_sb_deny(self, deny_server):
"""E2E-SC-11: S-B deny — 403 + flag token + no reservation + not importable."""
_require_smoke(deny_server)
r = requests.post(f"{BASE_URL}/customnode/install/pip",
json={"packages": PIP_URL}, timeout=30)
assert r.status_code == 403
assert r.json() == {"error": "allow_pip_install"}
assert _scripts_clean(), "reservation recorded on DENY (SC-11)"
assert _pip_import_rc() != 0, "pip fixture importable after DENY (SC-11)"
log = _launch_log(deny_server)
assert DENY_COPY_PIP in log, "flag-naming denial copy missing from L-D log"
class TestAllowArms:
"""L-A launch + R-A restart. ORDERED methods (donor sequential-class
precedent): SC-12 -> SC-13 -> SC-14 -> SC-21 -> SC-22 -> SC-42."""
def test_00_smoke_manager_version(self, allow_server):
_smoke(allow_server) # SC-05 (re-smoke on the new launch)
@needs_network
def test_01_git_url_allow(self, allow_server):
"""E2E-SC-12: S-A allow — 200 + real clone + clone-target proof."""
_require_smoke(allow_server)
r = requests.post(f"{BASE_URL}/customnode/install/git_url",
json={"url": NODEPACK_URL}, timeout=120)
assert r.status_code == 200, f"S-A allow expected 200, got {r.status_code}: {r.text!r}"
assert _wait_for(lambda: _pack_exists(PACK_NAME)), (
f"{PACK_NAME} not cloned within {POLL_TIMEOUT}s (SC-12)"
)
git_dir = _pack_dir() / ".git"
assert git_dir.is_dir(), "no .git DIRECTORY — not a real clone (SC-12)"
# Donor clone-target proof: .git/config [remote "origin"] url matches
# the requested URL modulo the .git suffix.
cp = configparser.ConfigParser()
cp.read(git_dir / "config")
section = 'remote "origin"'
assert section in cp, f'[{section}] missing from .git/config: {cp.sections()!r}'
remote_url = cp[section].get("url", "").rstrip("/")
expected = NODEPACK_URL.rstrip("/")
assert remote_url in (expected, expected + ".git"), (
f"clone targeted the WRONG repo: {remote_url!r} != {expected!r} (SC-12)"
)
@needs_network
def test_02_pip_allow_reserved(self, allow_server):
"""E2E-SC-13 (VERBATIM anti-false-PASS oracle): 200 = RESERVED, NOT
INSTALLED. Asserting import success here would be the exact false-PASS
the MM correction exists to prevent."""
_require_smoke(allow_server)
r = requests.post(f"{BASE_URL}/customnode/install/pip",
json={"packages": PIP_URL}, timeout=30)
assert r.status_code == 200, f"S-B allow expected 200, got {r.status_code}: {r.text!r}"
assert SCRIPTS_FILE.is_file(), "no reservation file after S-B allow (SC-13)"
content = SCRIPTS_FILE.read_text(errors="ignore")
reserved_lines = [
ln for ln in content.splitlines()
if "'#FORCE'" in ln and PIP_PKG in ln
]
assert reserved_lines, (
f"no reservation line with '#FORCE' + {PIP_PKG!r} in {SCRIPTS_FILE}:\n{content}"
)
# MANDATORY: the package is NOT installed at this point.
assert _pip_import_rc() != 0, (
"pip fixture importable right after the 200 — reservation semantics "
"violated, or a previous run leaked state (SC-13 anti-false-PASS)"
)
@needs_network
def test_03_restart_consumes_reservation(self, allow_server):
"""E2E-SC-14: R-A restart THROUGH the holder; the consuming boot
executes + removes the reservation; MARKER import proves field-level."""
_require_smoke(allow_server)
assert SCRIPTS_FILE.is_file(), "precondition: reservation must exist (SC-13 first)"
# Restart THROUGH the holder (spec §3 binding item 3): stop the live
# L-A process, relaunch as R-A with the SAME staged config, replace
# the handle — class teardown then stops R-A.
_stop_server(allow_server)
_start_server(allow_server, "R-A")
_smoke(allow_server)
# Field-level positive proof (not just exit code):
marker = _pip_marker_rc()
assert marker.returncode == 0, (
f"MARKER import failed after the consuming restart (SC-14):\n"
f"{marker.stderr}\nR-A log tail:\n{_named_log('R-A')[-3000:]}"
)
assert not SCRIPTS_FILE.exists(), (
"install-scripts.txt NOT removed by the consuming boot (SC-14 self-clean)"
)
ra_log = _named_log("R-A")
assert "## ComfyUI-Manager: EXECUTE =>" in ra_log and PIP_PKG in ra_log, (
"R-A log lacks the startup-script execution block (SC-14)"
)
assert "Startup script completed." in ra_log, (
"R-A log lacks the startup-script completion line (SC-14)"
)
@needs_network
def test_04_clone_residual_cleanup(self, allow_server):
"""E2E-SC-21: clone-dir hygiene; FS-absence primary + installed-index
cross-check while the server is still up (defensive, donor pattern)."""
_remove_pack(PACK_NAME)
assert not _pack_exists(PACK_NAME), "clone dir still present (SC-21 primary)"
try:
r = requests.get(f"{BASE_URL}/customnode/installed", timeout=15)
if r.status_code == 200:
installed = r.json()
assert PACK_NAME not in installed, (
f"{PACK_NAME} still in /customnode/installed after removal (SC-21)"
)
for key, pkg in installed.items():
if isinstance(pkg, dict):
assert pkg.get("cnr_id") != PACK_NAME and pkg.get("aux_id") != PACK_NAME, (
f"installed entry {key!r} still references {PACK_NAME!r} (SC-21)"
)
except (ValueError, requests.RequestException):
# Spec SC-21: if the response schema proves awkward, FS-absence
# alone satisfies this row.
pass
@needs_network
def test_05_pip_residual_uninstall(self, allow_server):
"""E2E-SC-22: S-B residual class 1 (venv package)."""
r = _pip_uninstall()
assert r.returncode == 0, f"pip uninstall failed (SC-22):\n{r.stdout}\n{r.stderr}"
assert _pip_import_rc() != 0, "pip fixture importable after uninstall (SC-22)"
@needs_network
def test_06_requirements_watchdog(self, allow_server):
"""E2E-SC-42 (Q-6 watchdog, amendment A2): every management-script
EXECUTE in the L-A launch log must be attributable to the owned
fixture's OWN pinned requirements (python-slugify==8.0.4 — the
nodepack fixture's deliberate invariant-4 ride-along requirement).
Any other EXECUTE (e.g. a Manager-requirements install the Q-6
risk this row guards) FAILS the watchdog.
The allowlisted line doubles as LIVE proof of the invariant-4
ride-along class: a dependency pip install executed inside the
git-URL transaction without consulting allow_pip_install."""
la_log = _named_log("L-A")
banner = "## ComfyUI-Manager: EXECUTE =>"
idx = 0
execs = []
while True:
idx = la_log.find(banner, idx)
if idx < 0:
break
execs.append(la_log[idx: idx + 600])
idx += len(banner)
# Non-vacuity (review iter-2 / A2 positive half): the ride-along
# MUST have happened — exactly ONE management-script execution,
# the fixture's single pinned requirement.
assert len(execs) == 1, (
f"expected exactly 1 management-script execution during L-A "
f"(the fixture's pinned requirement ride-along), found "
f"{len(execs)} (SC-42 / A2)"
)
window = execs[0]
assert NODEPACK_PINNED_REQ in window, (
"unexpected management-script execution during L-A — not "
"attributable to the fixture's pinned requirement "
f"({NODEPACK_PINNED_REQ}) (SC-42 watchdog):\n{window}"
)
# Line-level shape: it must be a pip-install command, not an
# arbitrary script that merely mentions the requirement string.
assert "'pip'" in window and "'install'" in window, (
f"EXECUTE block is not a pip-install command (SC-42):\n{window}"
)
@pytest.mark.skipif(
not os.environ.get("E2E_PUBLIC_LISTEN"),
reason="public-listener row is opt-in (E2E_PUBLIC_LISTEN=1) — Q-7 default-off",
)
class TestPublicListener:
"""E2E-SC-40 (opt-in): flags=true + 0.0.0.0 -> still 403 on both surfaces.
Live proof of invariant 2 (predicate = flag AND loopback at REQUEST time)."""
def test_00_smoke_manager_version(self, public_server):
_smoke(public_server)
def test_01_sa_public_deny(self, public_server):
_require_smoke(public_server)
r = requests.post(f"{BASE_URL}/customnode/install/git_url",
json={"url": NODEPACK_URL}, timeout=30)
assert r.status_code == 403
assert r.json() == {"error": "allow_git_url_install"}
assert not _pack_exists(PACK_NAME)
def test_02_sb_public_deny(self, public_server):
_require_smoke(public_server)
r = requests.post(f"{BASE_URL}/customnode/install/pip",
json={"packages": PIP_URL}, timeout=30)
assert r.status_code == 403
assert r.json() == {"error": "allow_pip_install"}
assert _scripts_clean()
class TestZeroResidual:
"""E2E-SC-24: the complete [D3] residual inventory in one assertion block.
Runs AFTER the server classes (their class-scoped fixtures have finalized:
server stopped, config restored, backup deleted). The unmount half of the
inventory is asserted by the mount_worktree finalizer itself it cannot
be asserted from inside the session while the mount is still live."""
def test_99_zero_residual_sweep(self, mount_worktree):
# custom_nodes clean (incl. .trash_ fallback leftovers)
assert not _pack_exists(PACK_NAME), "nodepack residue in custom_nodes (SC-24)"
leftovers = [p.name for p in CN_DIR.iterdir()
if p.name.startswith((".trash_", PACK_NAME))]
assert not leftovers, f"residual entries in custom_nodes: {leftovers} (SC-24)"
# venv clean
assert _pip_import_rc() != 0, "pip fixture still importable (SC-24)"
# Amendment A2: transitive-dep residual class swept
for dep in TRANSITIVE_DEPS:
rc = _run([str(VENV_PY), "-m", "pip", "show", dep]).returncode
assert rc != 0, f"transitive dep {dep!r} survived the sweep (SC-24 / A2)"
# reservation clean
assert _scripts_clean(), "pip-test1 reservation residue (SC-24)"
# config restored + backup DELETED
assert CFG.is_file(), "config.ini missing after restore (SC-24)"
cfg_text = CFG.read_text(errors="ignore")
assert "allow_git_url_install" not in cfg_text, "staged flag leaked into restored config (SC-24)"
assert "allow_pip_install" not in cfg_text, "staged flag leaked into restored config (SC-24)"
assert not CFG_BACKUP.exists(), "stale config backup survived (SC-24 / peer R2)"
# port free
assert _port_free(), f"port {PORT} still bound (SC-24)"
if __name__ == "__main__":
sys.exit(pytest.main([__file__, "-v"]))
+121
View File
@@ -0,0 +1,121 @@
"""AC verification for wi-001 (B2): Content-Type rejection helper.
Validates _reject_simple_form_content_type against the 5-item curl matrix
from the WI's acceptance criteria using aiohttp test client. The test
builds a minimal aiohttp app that mirrors the helper's wiring into a
no-body POST handler, so we exercise the real request.content_type
parsing path rather than a mock.
AC matrix:
form-url 400
multipart 400
text/plain 400
no-CT 200
application/json 200
"""
import asyncio
import unittest
from pathlib import Path
from aiohttp import web
from aiohttp.test_utils import TestClient, TestServer
# Parse the helper from manager_server.py without importing it, to avoid
# pulling in the full ComfyUI/PromptServer stack. Note: we intentionally do
# NOT add the `glob/` directory to sys.path — the dir name would shadow
# Python's stdlib `glob` module and break pytest collection.
REPO_ROOT = Path(__file__).resolve().parent.parent
def _load_helper():
"""Parse manager_server.py and execute only the helper definition."""
import ast
source = (REPO_ROOT / "glob" / "manager_server.py").read_text()
tree = ast.parse(source)
wanted = {"_SIMPLE_FORM_CONTENT_TYPES", "_reject_simple_form_content_type"}
nodes = []
for node in tree.body:
if isinstance(node, ast.Assign):
for target in node.targets:
if isinstance(target, ast.Name) and target.id in wanted:
nodes.append(node)
elif isinstance(node, ast.FunctionDef) and node.name in wanted:
nodes.append(node)
module = ast.Module(body=nodes, type_ignores=[])
ns = {"web": web, "frozenset": frozenset}
exec(compile(module, "manager_server_helpers", "exec"), ns)
return ns["_reject_simple_form_content_type"]
_reject_simple_form_content_type = _load_helper()
async def _handler(request):
resp = _reject_simple_form_content_type(request)
if resp is not None:
return resp
return web.Response(status=200)
class ContentTypeRejectionTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.loop = asyncio.new_event_loop()
asyncio.set_event_loop(cls.loop)
app = web.Application()
app.router.add_post("/noop", _handler)
cls.server = TestServer(app, loop=cls.loop)
cls.client = TestClient(cls.server, loop=cls.loop)
cls.loop.run_until_complete(cls.client.start_server())
@classmethod
def tearDownClass(cls):
cls.loop.run_until_complete(cls.client.close())
cls.loop.close()
def _post(self, headers):
async def go():
return await self.client.post("/noop", headers=headers, data=b"")
return self.loop.run_until_complete(go())
def test_form_urlencoded_rejected(self):
r = self._post({"Content-Type": "application/x-www-form-urlencoded"})
self.assertEqual(r.status, 400)
def test_multipart_form_data_rejected(self):
# aiohttp requires a boundary for multipart; helper should still reject
# based on the primary mimetype.
r = self._post({"Content-Type": "multipart/form-data; boundary=xyz"})
self.assertEqual(r.status, 400)
def test_text_plain_rejected(self):
r = self._post({"Content-Type": "text/plain"})
self.assertEqual(r.status, 400)
def test_no_content_type_allowed(self):
# Explicitly strip Content-Type: aiohttp client may add a default,
# so we use a raw request to ensure absence is tested.
async def go():
import aiohttp
async with aiohttp.ClientSession() as session:
async with session.post(
self.client.make_url("/noop"),
data=None,
skip_auto_headers=["Content-Type"],
) as resp:
return resp.status
status = self.loop.run_until_complete(go())
self.assertEqual(status, 200)
def test_application_json_allowed(self):
r = self._post({"Content-Type": "application/json"})
self.assertEqual(r.status, 200)
if __name__ == "__main__":
unittest.main(verbosity=2)
+153
View File
@@ -0,0 +1,153 @@
"""Unit tests for the dedicated-install-flag predicate.
Covers `is_dedicated_install_allowed(flag_value, listen_address)` in
glob/manager_server.py:
- Truth table: allowed iff flag is true AND the listener is loopback.
- REPLACE-by-construction: the 2-arg signature has no security_level /
network_mode parameter and the body references no config machinery,
so security_level cannot influence the outcome in either direction.
- Cross-flag isolation: a single flag_value input cannot consult the
other flag.
- Request-time evaluation: the body must not read the import-time
`is_local_mode` snapshot (callers pass args.listen per request).
Harness: glob/manager_server.py is not importable under the test runner
(`from comfy.cli_args import args`, PromptServer), so we AST-parse the
file and exec only the wanted pure defs `glob/` is never added to
sys.path (the dir name shadows the stdlib `glob`).
"""
import ast
import inspect
import unittest
from pathlib import Path
from typing import Any
REPO_ROOT = Path(__file__).resolve().parent.parent
MANAGER_SERVER_PATH = REPO_ROOT / "glob" / "manager_server.py"
_WANTED = {"is_loopback", "is_dedicated_install_allowed"}
def _load_predicates():
"""Parse manager_server.py; exec only the wanted pure function defs."""
source = MANAGER_SERVER_PATH.read_text()
tree = ast.parse(source)
nodes = []
node_by_name = {}
for node in tree.body:
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name in _WANTED:
nodes.append(node)
node_by_name[node.name] = node
missing = _WANTED - node_by_name.keys()
assert not missing, f"expected pure defs missing from manager_server.py: {missing}"
module = ast.Module(body=nodes, type_ignores=[])
ns: dict = {"bool": bool}
exec(compile(module, "manager_server_predicates", "exec"), ns)
return ns, node_by_name
_NS, _NODES = _load_predicates()
IS_LOOPBACK: Any = _NS["is_loopback"]
PREDICATE: Any = _NS["is_dedicated_install_allowed"]
PREDICATE_NODE = _NODES["is_dedicated_install_allowed"]
class IsLoopbackBehaviorTest(unittest.TestCase):
"""Pins the loopback term the predicate composes."""
def test_ipv4_loopback(self):
self.assertTrue(IS_LOOPBACK("127.0.0.1"))
def test_public_address(self):
self.assertFalse(IS_LOOPBACK("0.0.0.0"))
def test_ipv6_loopback(self):
self.assertTrue(IS_LOOPBACK("::1"))
def test_invalid_address_reads_false(self):
# Non-IP strings deny-by-default (ValueError path).
self.assertFalse(IS_LOOPBACK("localhost"))
self.assertFalse(IS_LOOPBACK(""))
class DedicatedInstallPredicateTest(unittest.TestCase):
"""P-direct truth table + REPLACE-by-construction."""
def test_truth_table(self):
"""allowed iff flag AND loopback."""
cases = [
# (flag_value, listen_address, expected)
(True, "127.0.0.1", True),
(False, "127.0.0.1", False),
(True, "0.0.0.0", False),
(False, "0.0.0.0", False),
(True, "::1", True),
(True, "not-an-ip", False), # invalid listen -> deny
]
for flag_value, listen, expected in cases:
with self.subTest(flag=flag_value, listen=listen):
result = PREDICATE(flag_value, listen)
self.assertIsInstance(result, bool)
self.assertEqual(result, expected)
def test_falsy_flag_values_deny(self):
"""Secure-by-default: any falsy flag never allows."""
for falsy in (False, None, 0, ""):
with self.subTest(flag=falsy):
self.assertFalse(PREDICATE(falsy, "127.0.0.1"))
def test_signature_has_no_security_level(self):
"""Exactly (flag_value, listen_address) — no security_level term."""
params = list(inspect.signature(PREDICATE).parameters)
self.assertEqual(params, ["flag_value", "listen_address"])
for name in params:
self.assertNotIn("security", name)
self.assertNotIn("network_mode", name)
def test_body_free_of_config_machinery(self):
"""Body references no security_level plumbing, config reader, or the
import-time `is_local_mode` snapshot (request-time evaluation)."""
forbidden = {
"is_allowed_security_level",
"security_level",
"get_config",
"core",
"is_local_mode",
"network_mode",
"args",
}
seen = set()
for node in ast.walk(PREDICATE_NODE):
if isinstance(node, ast.Name):
seen.add(node.id)
elif isinstance(node, ast.Attribute):
seen.add(node.attr)
elif isinstance(node, ast.Constant) and isinstance(node.value, str):
seen.add(node.value)
self.assertEqual(
seen & forbidden, set(),
"predicate body must stay config-import-free",
)
def test_cross_flag_isolation_by_construction(self):
"""A single flag_value input cannot consult the other flag."""
seen_strings = {
node.value
for node in ast.walk(PREDICATE_NODE)
if isinstance(node, ast.Constant) and isinstance(node.value, str)
}
self.assertNotIn("allow_git_url_install", seen_strings)
self.assertNotIn("allow_pip_install", seen_strings)
self.assertTrue(PREDICATE(True, "127.0.0.1"))
self.assertFalse(PREDICATE(False, "127.0.0.1"))
def test_purity_deterministic(self):
"""Pure predicate — repeat calls identical."""
for _ in range(3):
self.assertTrue(PREDICATE(True, "127.0.0.1"))
self.assertFalse(PREDICATE(True, "0.0.0.0"))
if __name__ == "__main__":
unittest.main(verbosity=2)
+242
View File
@@ -0,0 +1,242 @@
"""Config-contract tests for the dedicated install flags.
Drives the real glob/manager_core config reader/writer through a
subprocess-isolated harness and pins: missing keys read False
(secure-by-default), only case-insensitive "true" is truthy, write
round-trips losslessly, edits need a restart (cached_config), the
exception-fallback path supplies False, no auto-migration seeds the
flags from a legacy security_level, and the get_bool missing->False
quirk the flags rely on stays frozen.
Harness: the child process injects a stub `folder_paths` (routing
import-time side effects into a tmpdir, and making has_system_user_api()
True so force_security_level_if_needed does not force 'strong'), prepends
`glob/` to ITS OWN sys.path (shadowing of stdlib `glob` confined to the
child), points manager_core.manager_config_path at a tmp config.ini,
resets cached_config, runs the scenario, and prints one JSON line for the
parent to assert.
"""
import json
import subprocess
import sys
import textwrap
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
_CHILD_PREAMBLE = textwrap.dedent(
"""
import sys, types, tempfile, os, json
tmp = tempfile.mkdtemp(prefix="cm_flags_cfg_")
stub = types.ModuleType("folder_paths")
stub.get_user_directory = lambda: tmp
stub.get_system_user_directory = lambda *a, **k: os.path.join(tmp, "sysuser")
sys.modules["folder_paths"] = stub
sys.path.insert(0, {glob_path!r})
import manager_core
CONFIG_PATH = os.path.join(tmp, "config.ini")
manager_core.manager_config_path = CONFIG_PATH
manager_core.cached_config = None
def write_ini(text):
with open(CONFIG_PATH, "w") as f:
f.write(text)
def fresh_read():
manager_core.cached_config = None
return manager_core.get_config()
def flag_view(cfg):
return {{
"git": cfg.get("allow_git_url_install", "<ABSENT>"),
"pip": cfg.get("allow_pip_install", "<ABSENT>"),
}}
"""
)
def _run_child(body):
"""Run a scenario body in the isolated child; return its JSON payload."""
script = _CHILD_PREAMBLE.format(glob_path=str(REPO_ROOT / "glob")) + textwrap.dedent(body)
proc = subprocess.run(
[sys.executable, "-c", script],
capture_output=True,
text=True,
timeout=180,
cwd=str(REPO_ROOT),
)
if proc.returncode != 0:
raise AssertionError(
"config-harness child failed (rc=%d). stderr tail:\n%s"
% (proc.returncode, "\n".join(proc.stderr.strip().splitlines()[-8:]))
)
lines = proc.stdout.strip().splitlines()
if not lines:
raise AssertionError(
"config-harness child exited 0 but produced no stdout. stderr tail:\n%s"
% "\n".join(proc.stderr.strip().splitlines()[-8:])
)
last_line = lines[-1]
try:
return json.loads(last_line)
except json.JSONDecodeError as e:
raise AssertionError(
"config-harness child emitted a non-JSON last line: %r\nfull stdout:\n%s"
% (last_line, proc.stdout)
) from e
class InstallFlagsConfigContractTest(unittest.TestCase):
def test_sc17_missing_keys_read_false(self):
"""Both keys absent from config.ini -> both flags read False
(secure-by-default)."""
payload = _run_child(
"""
write_ini("[default]\\nsecurity_level = normal\\n")
print(json.dumps(flag_view(fresh_read())))
"""
)
self.assertIs(payload["git"], False)
self.assertIs(payload["pip"], False)
def test_sc18_malformed_and_case_matrix(self):
"""Only case-insensitive "true" is truthy; malformed -> False."""
payload = _run_child(
"""
out = {}
for raw in ["1", "yes", "TRUE", "true ", "true"]:
write_ini("[default]\\nallow_git_url_install = %s\\nallow_pip_install = %s\\n" % (raw, raw))
cfg = fresh_read()
out[raw] = flag_view(cfg)
print(json.dumps(out))
"""
)
expected = {
"1": False, # malformed: numeric truthiness NOT honored
"yes": False, # malformed: yes/no NOT honored
"TRUE": True, # case-insensitive read (:1724)
"true ": True, # configparser strips surrounding whitespace
"true": True,
}
for raw, want in expected.items():
with self.subTest(value=raw):
self.assertIs(payload[raw]["git"], want)
self.assertIs(payload[raw]["pip"], want)
def test_sc19_write_round_trip(self):
"""write_config persists str(bool); round-trip is lossless."""
payload = _run_child(
"""
write_ini("[default]\\nsecurity_level = normal\\n")
cfg = fresh_read()
cfg["allow_git_url_install"] = True
cfg["allow_pip_install"] = False
manager_core.write_config()
raw = open(CONFIG_PATH).read()
reread = flag_view(fresh_read())
print(json.dumps({
"raw_has_git_true": "allow_git_url_install = True" in raw,
"raw_has_pip_false": "allow_pip_install = False" in raw,
"reread": reread,
}))
"""
)
self.assertTrue(
payload["raw_has_git_true"],
"write_config must persist allow_git_url_install = True in [default]",
)
self.assertTrue(
payload["raw_has_pip_false"],
"write_config must persist allow_pip_install = False in [default]",
)
self.assertIs(payload["reread"]["git"], True)
self.assertIs(payload["reread"]["pip"], False)
def test_sc20_restart_only_activation(self):
"""Editing config.ini without restart has NO effect (cache wins);
a reset (== restart) picks up the change."""
payload = _run_child(
"""
write_ini("[default]\\nallow_git_url_install = false\\n")
first = manager_core.get_config() # populates cached_config
before_edit = flag_view(first)
write_ini("[default]\\nallow_git_url_install = true\\n")
cached = flag_view(manager_core.get_config()) # NO reset: cache must win
after_restart = flag_view(fresh_read()) # reset == restart
print(json.dumps({
"before_edit": before_edit,
"cached_after_edit": cached,
"after_restart": after_restart,
}))
"""
)
self.assertIs(payload["before_edit"]["git"], False)
self.assertIs(
payload["cached_after_edit"]["git"],
False,
"cached_config must NOT hot-reload the edited flag",
)
self.assertIs(payload["after_restart"]["git"], True)
def test_sc21_exception_fallback_supplies_false(self):
"""Corrupted config.ini -> exception-fallback dict supplies flags False."""
payload = _run_child(
"""
# No [default] section header -> read_config raises inside try,
# lands in the exception-fallback dict.
write_ini("allow_git_url_install = true\\ngarbage without section\\n")
cfg = fresh_read()
print(json.dumps({
"flags": flag_view(cfg),
"fallback_marker_file_logging": cfg.get("file_logging"),
}))
"""
)
# file_logging True proves the FALLBACK dict was used (the parse
# path would yield False for a missing file_logging key).
self.assertIs(
payload["fallback_marker_file_logging"],
True,
"corrupted ini must route through the exception-fallback dict",
)
self.assertIs(payload["flags"]["git"], False)
self.assertIs(payload["flags"]["pip"], False)
def test_sc28_no_auto_migration_from_weak(self):
"""Legacy `security_level=weak` does NOT seed the flags (no auto-migration)."""
payload = _run_child(
"""
write_ini("[default]\\nsecurity_level = weak\\n")
cfg = fresh_read()
print(json.dumps({
"flags": flag_view(cfg),
"security_level": cfg.get("security_level"),
}))
"""
)
self.assertEqual(payload["security_level"], "weak")
self.assertIs(payload["flags"]["git"], False, "no auto-seed from weak")
self.assertIs(payload["flags"]["pip"], False, "no auto-seed from weak")
def test_sc42_get_bool_quirk_guard(self):
"""get_bool ignores its default param: missing `file_logging` reads
False despite a True default. The flags rely on this missing->False
quirk; this guard pins it."""
payload = _run_child(
"""
write_ini("[default]\\nsecurity_level = normal\\n")
cfg = fresh_read()
print(json.dumps({"file_logging": cfg.get("file_logging", "<ABSENT>")}))
"""
)
self.assertIs(
payload["file_logging"],
False,
"get_bool quirk changed: missing key no longer reads False — "
"new flags rely on missing->False",
)
if __name__ == "__main__":
unittest.main(verbosity=2)
+461
View File
@@ -0,0 +1,461 @@
"""Handler-gate tests for the dedicated install flags.
Three layers, each covering what the others can't:
1. SCInstallGateMirrorTest a slim mirror of the batch-install handler
(`install_custom_node`, S-C) wired to the REAL extracted gate
primitives. Covers the handler *composition* that the pure predicate
test cannot: risky-level routing, the load-bearing public canary
(the entry gate has no network term, so the deny must come from the
predicate's loopback term), block-arm unconditionality, the
security_level entry gate, and the CNR/middle false-pass guards.
2. DenialConstantsTest content of the flag denial constants and the
`security_403_response` precedence, asserted directly (no server).
3. BindingProofTest AST proof that the REAL handlers (S-A/S-B/S-C) are
wired to the predicate and that the old `is_allowed_security_level('high')`
gate is gone from S-A/S-B (closes the mirror-vs-real gap).
The direct S-A/S-B allow/deny behavior is covered by the binding proof
(wiring) plus the real-server E2E suite (behavior); the mirror here
focuses on S-C, whose multi-arm branching is the genuine logic risk.
Harness: glob/manager_server.py is not importable under the runner
(`from comfy.cli_args import args`, PromptServer), so we AST-extract the
gate primitives and exec them into a stub namespace `glob/` is never
added to sys.path (the dir name shadows stdlib glob).
"""
import ast
import asyncio
import contextlib
import inspect
import json
import logging
import unittest
from pathlib import Path
from typing import Any
from aiohttp import web
from aiohttp.test_utils import TestClient, TestServer
REPO_ROOT = Path(__file__).resolve().parent.parent
MANAGER_SERVER_PATH = REPO_ROOT / "glob" / "manager_server.py"
_WANTED_FUNCS = {
"is_loopback",
"is_dedicated_install_allowed",
"is_allowed_security_level",
"security_403_response",
}
_WANTED_CONSTS = {
"SECURITY_MESSAGE_MIDDLE_OR_BELOW",
"SECURITY_MESSAGE_NORMAL_MINUS",
"SECURITY_MESSAGE_GENERAL",
"SECURITY_MESSAGE_FLAG_GIT_URL",
"SECURITY_MESSAGE_FLAG_PIP",
}
_HANDLER_NAMES = {
"install_custom_node_git_url", # S-A
"install_custom_node_pip", # S-B
"install_custom_node", # S-C
}
class _MigrationStub:
def __init__(self):
self.system_user_api = True
def has_system_user_api(self):
return self.system_user_api
class _CoreStub:
"""Stand-in for `core` consulted by is_allowed_security_level."""
def __init__(self):
self.security_level = "normal"
def get_config(self):
return {"security_level": self.security_level}
def _load_surfaces():
source = MANAGER_SERVER_PATH.read_text()
tree = ast.parse(source)
exec_nodes = []
handler_nodes = {}
for node in tree.body:
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
if node.name in _WANTED_FUNCS:
node.decorator_list = [] # exec needs no aiohttp routing context
exec_nodes.append(node)
if node.name in _HANDLER_NAMES:
handler_nodes[node.name] = node
elif isinstance(node, ast.Assign):
for target in node.targets:
if isinstance(target, ast.Name) and target.id in _WANTED_CONSTS:
exec_nodes.append(node)
module = ast.Module(body=exec_nodes, type_ignores=[])
ns: dict = {
"web": web,
"bool": bool,
"manager_migration": _MigrationStub(),
"core": _CoreStub(),
"is_local_mode": True,
}
exec(compile(module, "manager_server_gate_surfaces", "exec"), ns)
# Feature is implemented — these must resolve, else the extraction or
# the production code regressed.
for name in _WANTED_FUNCS | _WANTED_CONSTS:
assert ns.get(name) is not None, "missing gate primitive: %s" % name
for name in _HANDLER_NAMES:
assert name in handler_nodes, "missing handler: %s" % name
return ns, handler_nodes
NS, HANDLERS = _load_surfaces()
PREDICATE: Any = NS["is_dedicated_install_allowed"]
IS_LOOPBACK: Any = NS["is_loopback"]
IAS: Any = NS["is_allowed_security_level"]
SEC_403: Any = NS["security_403_response"]
CATALOG_URL = "https://github.com/catalog/listed-node"
UNKNOWN_URL = "https://github.com/x/not-in-catalog"
CATALOG_PIP = "torch"
UNKNOWN_PIP = "definitely-not-in-catalog-pkg"
def _body_unknown(files=None, pip=None):
"""version=='unknown' ingestion arm."""
return {
"version": "unknown",
"selected_version": "unknown",
"files": files or [UNKNOWN_URL],
"pip": pip or [],
"channel": "default",
"mode": "cache",
"ui_id": "test-row",
}
def _body_cnr_latest():
"""non-nightly CNR arm — risky='low' set statically."""
return {
"version": "1.0.0",
"selected_version": "latest",
"id": "catalog-cnr-pack",
"channel": "default",
"mode": "cache",
"ui_id": "test-row",
}
class _TrackingFlags(dict):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.reads = []
def __getitem__(self, key):
self.reads.append(key)
return super().__getitem__(key)
class GateEnv:
"""Injectable per-row environment for the mirror app."""
def __init__(self, git=False, pip=False, listen="127.0.0.1", security_level="normal"):
self.flags = _TrackingFlags(
{"allow_git_url_install": git, "allow_pip_install": pip}
)
self.listen = listen
self.security_level = security_level
self.task_queue = []
self.risky_calls = 0
self.catalog_urls = {CATALOG_URL}
self.catalog_pips = {CATALOG_PIP}
def get_risky_level(self, files, pip_packages):
"""Mirror of get_risky_level: URL check precedes pip check."""
self.risky_calls += 1
for x in files or []:
if x not in self.catalog_urls:
return "high"
for p in pip_packages or []:
if p not in self.catalog_pips:
return "block"
return "middle"
_SC_DENY_TEXT = "A security error has occurred. Please check the terminal logs"
def _apply_env(env):
"""Retained gates use the is_local_mode snapshot + config stub."""
NS["is_local_mode"] = IS_LOOPBACK(env.listen)
NS["core"].security_level = env.security_level
def _make_sc_install(env):
"""Slim mirror of install_custom_node (S-C) in its post-change gate
shape: security_level entry gate, then risky-level routing where the
'high' (unknown-URL) arm goes through the dedicated predicate and the
retained arms keep is_allowed_security_level."""
async def sc_install(request):
# ENTRY gate — UNCHANGED (security_level-governed)
if not IAS("middle"):
logging.error(NS["SECURITY_MESSAGE_MIDDLE_OR_BELOW"])
return web.Response(status=403, text=_SC_DENY_TEXT)
json_data = await request.json()
risky_level = None
git_url = None
selected_version = json_data.get("selected_version")
if json_data["version"] != "unknown" and selected_version != "unknown":
if selected_version != "nightly":
risky_level = "low" # static — get_risky_level NOT called
else:
git_url = [json_data.get("repository")]
else:
git_url = json_data.get("files")
if risky_level is None:
risky_level = env.get_risky_level(git_url, json_data.get("pip", []))
if risky_level == "high":
# unknown-URL arm -> dedicated predicate (flag AND loopback)
if not PREDICATE(env.flags["allow_git_url_install"], env.listen):
logging.error(NS["SECURITY_MESSAGE_FLAG_GIT_URL"])
return web.Response(status=404, text=_SC_DENY_TEXT)
elif not IAS(risky_level):
# 'block' is always False -> unconditional deny; 'middle'/'low'
# retained UNCHANGED.
logging.error(NS["SECURITY_MESSAGE_GENERAL"])
return web.Response(status=404, text=_SC_DENY_TEXT)
env.task_queue.append(("install", json_data.get("ui_id")))
return web.Response(status=200)
app = web.Application()
app.router.add_post("/manager/queue/install", sc_install)
return app
class _LogCapture(logging.Handler):
def __init__(self):
super().__init__()
self.messages = []
def emit(self, record):
self.messages.append(record.getMessage())
@contextlib.contextmanager
def _capture_logs():
handler = _LogCapture()
root = logging.getLogger()
old_level = root.level
root.addHandler(handler)
root.setLevel(logging.DEBUG)
try:
yield handler.messages
finally:
root.removeHandler(handler)
root.setLevel(old_level)
class SCInstallGateMirrorTest(unittest.TestCase):
"""Batch-install (S-C) gate composition via a slim handler mirror."""
def setUp(self):
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)
def tearDown(self):
self.loop.close()
def _post(self, env, body):
_apply_env(env)
async def go():
server = TestServer(_make_sc_install(env))
client = TestClient(server)
await client.start_server()
try:
resp = await client.post("/manager/queue/install", json=body)
return resp.status, await resp.text()
finally:
await client.close()
return self.loop.run_until_complete(go())
def _installs(self, env):
return [item for item in env.task_queue if item[0] == "install"]
def _has(self, logs, const_name):
return any(NS[const_name] in m for m in logs)
def test_high_arm_allow(self):
env = GateEnv(git=True, listen="127.0.0.1")
with _capture_logs() as logs:
status, _ = self._post(env, _body_unknown())
self.assertEqual(status, 200)
self.assertEqual(len(self._installs(env)), 1)
self.assertFalse(self._has(logs, "SECURITY_MESSAGE_FLAG_GIT_URL"))
def test_high_arm_flag_deny(self):
env = GateEnv(git=False, listen="127.0.0.1")
with _capture_logs() as logs:
status, text = self._post(env, _body_unknown())
self.assertEqual(status, 404) # risky-position deny shape kept
self.assertIn("A security error has occurred", text)
self.assertEqual(self._installs(env), [])
self.assertTrue(self._has(logs, "SECURITY_MESSAGE_FLAG_GIT_URL"))
self.assertFalse(self._has(logs, "SECURITY_MESSAGE_NORMAL_MINUS"))
def test_load_bearing_public_canary(self):
"""Entry gate passes on a public listener; the deny MUST come from
the predicate's loopback term (the 'middle' set has no network
term). 404 (risky deny), not 403 (entry deny)."""
env = GateEnv(git=True, listen="0.0.0.0", security_level="normal")
with _capture_logs() as logs:
status, _ = self._post(env, _body_unknown())
self.assertEqual(status, 404)
self.assertEqual(self._installs(env), [])
self.assertFalse(
self._has(logs, "SECURITY_MESSAGE_MIDDLE_OR_BELOW"),
"entry gate must PASS here — deny must come from the predicate",
)
self.assertTrue(self._has(logs, "SECURITY_MESSAGE_FLAG_GIT_URL"))
def test_unknown_pip_block_unconditional(self):
"""Unknown-pip 'block' stays unconditional regardless of both flags
(catalog URL + non-catalog pip; URL check precedes pip check)."""
env = GateEnv(git=True, pip=True, listen="127.0.0.1")
body = _body_unknown(files=[CATALOG_URL], pip=[UNKNOWN_PIP])
with _capture_logs() as logs:
status, _ = self._post(env, body)
self.assertEqual(status, 404)
self.assertEqual(self._installs(env), [])
self.assertEqual(env.risky_calls, 1)
self.assertTrue(self._has(logs, "SECURITY_MESSAGE_GENERAL"))
self.assertEqual(env.flags.reads, [], "block arm must not consult the flags")
def test_entry_gate_strong_denies_despite_flags(self):
"""The security_level entry gate stays in force; flags do NOT bypass it."""
env = GateEnv(git=True, pip=True, listen="127.0.0.1", security_level="strong")
with _capture_logs() as logs:
status, _ = self._post(env, _body_unknown())
self.assertEqual(status, 403)
self.assertTrue(self._has(logs, "SECURITY_MESSAGE_MIDDLE_OR_BELOW"))
self.assertEqual(self._installs(env), [])
self.assertEqual(env.flags.reads, [], "entry deny must not consult the flags")
def test_cnr_latest_arm_never_consults_flags(self):
"""non-nightly CNR sets risky='low' statically — get_risky_level and
the flags are never consulted (false-pass guard)."""
env = GateEnv(git=False, pip=False, listen="127.0.0.1")
status, _ = self._post(env, _body_cnr_latest())
self.assertEqual(status, 200)
self.assertEqual(len(self._installs(env)), 1)
self.assertEqual(env.risky_calls, 0)
self.assertEqual(env.flags.reads, [], "flags must NOT be consulted on the CNR arm")
def test_middle_arm_retained(self):
"""all-catalog body -> risky='middle'; consults security_level
(UNCHANGED), not the flags."""
env = GateEnv(git=False, pip=False, listen="127.0.0.1")
body = _body_unknown(files=[CATALOG_URL], pip=[CATALOG_PIP])
status, _ = self._post(env, body)
self.assertEqual(status, 200)
self.assertEqual(len(self._installs(env)), 1)
self.assertEqual(env.risky_calls, 1)
self.assertEqual(env.flags.reads, [], "flags must NOT be consulted on the middle arm")
class DenialConstantsTest(unittest.TestCase):
"""Denial-copy honesty + security_403_response precedence (no server)."""
def _assert_honest_copy(self, const, flag_name):
self.assertIn(flag_name, const, "constant must name the responsible flag")
self.assertIn("config.ini", const, "constant must name config.ini")
for cause_phrasing in (
"is not allowed in this security_level",
"set the security level",
"a security_level of",
"security level configuration",
):
self.assertNotIn(cause_phrasing, const)
self.assertNotEqual(const, NS["SECURITY_MESSAGE_NORMAL_MINUS"])
self.assertNotEqual(const, NS["SECURITY_MESSAGE_GENERAL"])
def test_flag_constants_content(self):
self._assert_honest_copy(NS["SECURITY_MESSAGE_FLAG_GIT_URL"], "allow_git_url_install")
self._assert_honest_copy(NS["SECURITY_MESSAGE_FLAG_PIP"], "allow_pip_install")
def test_security_403_precedence(self):
"""outdated branch FIRST; flag_token names the flag; no-arg callers
stay byte-identical."""
self.assertIn("flag_token", inspect.signature(SEC_403).parameters)
NS["manager_migration"].system_user_api = False
try:
resp = SEC_403(flag_token="allow_git_url_install")
self.assertEqual(
json.loads(resp.text), {"error": "comfyui_outdated"},
"comfyui_outdated must take PRECEDENCE over flag_token",
)
finally:
NS["manager_migration"].system_user_api = True
resp = SEC_403(flag_token="allow_git_url_install")
self.assertEqual(json.loads(resp.text), {"error": "allow_git_url_install"})
resp = SEC_403()
self.assertEqual(
json.loads(resp.text), {"error": "security_level"},
"no-arg callers must stay byte-identical",
)
class BindingProofTest(unittest.TestCase):
"""AST proof that the REAL handlers are wired to the predicate (closes
the mirror-vs-real gap)."""
@staticmethod
def _ias_literal_calls(node):
out = []
for sub in ast.walk(node):
if (
isinstance(sub, ast.Call)
and isinstance(sub.func, ast.Name)
and sub.func.id == "is_allowed_security_level"
and sub.args
):
arg = sub.args[0]
out.append(arg.value if isinstance(arg, ast.Constant) else None)
return out
def test_handlers_bind_predicate(self):
"""S-A, S-B, S-C all gate via is_dedicated_install_allowed with the
right flag + args.listen (request-time); S-C keeps the 'middle'
entry gate and a variable-arg retained is_allowed_security_level path."""
for name, flag in (
("install_custom_node_git_url", "allow_git_url_install"),
("install_custom_node_pip", "allow_pip_install"),
("install_custom_node", "allow_git_url_install"),
):
with self.subTest(handler=name):
src = ast.unparse(HANDLERS[name])
self.assertIn("is_dedicated_install_allowed(", src)
self.assertIn(flag, src)
self.assertIn("args.listen", src)
sc_literals = self._ias_literal_calls(HANDLERS["install_custom_node"])
self.assertIn("middle", sc_literals, "entry gate must stay UNCHANGED")
self.assertIn(None, sc_literals, "a variable-arg retained path must remain")
def test_replace_no_high_literal_at_sa_sb(self):
"""REPLACE proof: no is_allowed_security_level('high') remains at
S-A / S-B (the flag fully replaced the old security_level gate)."""
for name in ("install_custom_node_git_url", "install_custom_node_pip"):
with self.subTest(handler=name):
self.assertNotIn(
"high", self._ias_literal_calls(HANDLERS[name]),
"%s still gates via is_allowed_security_level('high')" % name,
)
if __name__ == "__main__":
unittest.main(verbosity=2)
+136
View File
@@ -0,0 +1,136 @@
"""Structural (grep/AST) guards for the dedicated install flags.
Cheap source-level guards that complement the behavioral tests:
- Frontend 403 copy: both install surfaces in js/common.js name their
responsible flag, and the generic fallback copy stays unchanged.
- No new HTTP install surface is added.
- cm-cli stays an ungated local operator tool.
- The migration module never references the flags (no auto-seed
explicit opt-in only).
Harness: read/grep + AST over glob/*.py, cm-cli.py and js/*.js. No
imports of `glob/` modules (the dir name shadows stdlib glob).
"""
import re
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
MANAGER_SERVER_PATH = REPO_ROOT / "glob" / "manager_server.py"
MANAGER_MIGRATION_PATH = REPO_ROOT / "glob" / "manager_migration.py"
CM_CLI_PATH = REPO_ROOT / "cm-cli.py"
JS_COMMON_PATH = REPO_ROOT / "js" / "common.js"
GENERIC_403_COPY = "This action is not allowed with this security level configuration."
FLAG_TOKENS = ("allow_git_url_install", "allow_pip_install")
def _js_function_block(source, func_name):
"""Slice an `export async function <name>` block (up to the next
export or EOF)."""
start = source.find("export async function %s" % func_name)
if start < 0:
raise AssertionError("function %s not found in js source" % func_name)
next_export = source.find("export ", start + 1)
return source[start: next_export if next_export > 0 else len(source)]
def _handle403_call_args(source):
"""All handle403Response(...) CALL argument strings (def/import lines
excluded)."""
calls = []
for match in re.finditer(r"handle403Response\s*\(([^()]*(?:\([^()]*\)[^()]*)*)\)", source):
line_start = source.rfind("\n", 0, match.start()) + 1
line = source[line_start: source.find("\n", match.start())]
if "function handle403Response" in line or line.lstrip().startswith("import"):
continue
calls.append(match.group(1).strip())
return calls
class JsCopyStructuralTest(unittest.TestCase):
"""Frontend honest-copy contract."""
@classmethod
def setUpClass(cls):
cls.common_src = JS_COMMON_PATH.read_text()
def test_surface_messages_name_their_flag(self):
"""Both install 403 branches pass a flag-naming defaultMessage."""
for func, flag in (
("install_via_git_url", "allow_git_url_install"),
("install_pip", "allow_pip_install"),
):
with self.subTest(func=func):
block = _js_function_block(self.common_src, func)
two_arg_calls = [a for a in _handle403_call_args(block) if "," in a]
self.assertTrue(
two_arg_calls,
"%s must call handle403Response with a defaultMessage" % func,
)
self.assertIn(flag, block)
self.assertIn("config.ini", block)
def test_generic_fallback_and_frozen_callers_unchanged(self):
"""The generic fallback copy stays (exactly its two occurrences in
handle403Response), and no other handle403Response caller across
js/ gains a defaultMessage."""
self.assertEqual(self.common_src.count(GENERIC_403_COPY), 2)
surface_blocks = "".join(
_js_function_block(self.common_src, name)
for name in ("install_pip", "install_via_git_url")
)
allowed_two_arg = {a for a in _handle403_call_args(surface_blocks) if "," in a}
for js_file in sorted((REPO_ROOT / "js").glob("*.js")):
source = js_file.read_text()
for args in _handle403_call_args(source):
if "," in args:
self.assertIn(
args, allowed_two_arg,
"frozen handle403Response caller in %s gained a "
"defaultMessage: handle403Response(%s)" % (js_file.name, args),
)
class StructuralSecurityGuardsTest(unittest.TestCase):
"""Source-level guards against scope bleed."""
def test_no_new_install_route_surface(self):
"""No new HTTP surface for git-URL/pip install."""
source = MANAGER_SERVER_PATH.read_text()
routes = set(re.findall(r"@routes\.post\(\"([^\"]+)\"\)", source))
expected_surfaces = {
"/customnode/install/git_url",
"/customnode/install/pip",
"/manager/queue/install",
"/manager/queue/reinstall",
}
self.assertTrue(expected_surfaces.issubset(routes))
install_like = {r for r in routes if "install" in r}
self.assertEqual(
install_like,
expected_surfaces
| {"/manager/queue/uninstall", "/manager/queue/install_model"},
"install-like route set drifted — no new install surface allowed",
)
def test_cm_cli_ungated(self):
"""cm-cli stays a local operator tool — no gate, no flag lookup."""
source = CM_CLI_PATH.read_text()
for token in FLAG_TOKENS + ("is_allowed_security_level", "is_dedicated_install_allowed"):
self.assertNotIn(token, source, "cm-cli.py must stay ungated")
def test_no_autoseed_in_migration(self):
"""The migration module never references the flags (explicit
opt-in only no auto-seed from security_level)."""
source = MANAGER_MIGRATION_PATH.read_text()
for token in FLAG_TOKENS:
self.assertNotIn(
token, source,
"manager_migration.py must not seed/translate the new flags",
)
if __name__ == "__main__":
unittest.main(verbosity=2)