Compare commits

..

139 Commits
3.38 ... main

Author SHA1 Message Date
Dr.Lt.Data
8662f6e527 update DB
Some checks are pending
Python Linting / Run Ruff (push) Waiting to run
2025-12-29 00:28:43 +09:00
Dr.Lt.Data
3103fc9864 update DB
Some checks are pending
Python Linting / Run Ruff (push) Waiting to run
2025-12-28 08:35:32 +09:00
Shobhit Gupta
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
mrm987
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
chrishill197724-gif
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.Data
0a202dd506 update DB 2025-12-28 07:47:42 +09:00
room3dev
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
Lucas
97b86b02ad
Add "Image MetaHub Save Node" to custom-node-list (#2432) 2025-12-28 07:44:29 +09:00
CornmeisterNL
f2da1635f2
Add CornmeisterNL PowerPack (#2425)
Co-authored-by: CornmeisterNL <cornmeister@gmail.com>
2025-12-28 07:25:47 +09:00
Dr.Lt.Data
f0ed5c3433 update DB
Some checks are pending
Python Linting / Run Ruff (push) Waiting to run
2025-12-27 05:37:29 +09:00
Eric Rollei
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
Wakapedia
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
ConstantlyGrowup
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
hkun
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
ArtsticH
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
Some checks failed
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
Some checks are pending
Python Linting / Run Ruff (push) Waiting to run
2025-12-24 01:47:04 +09:00
BlackVortexAI
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
AugustusLXIII
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
Asidert
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
HB2k
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 Rage
b4d5b228ae
Add ComfyRage text nodes. (#2408) 2025-12-24 01:24:31 +09:00
Dr.Lt.Data
29b4824ee2 update DB
Some checks are pending
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
Some checks are pending
Python Linting / Run Ruff (push) Waiting to run
2025-12-22 18:52:24 +09:00
Dr.Lt.Data
e0e4886e63 update DB
Some checks are pending
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
pixelpainter
7706b047ce
Update custom-node-list.json (#2414) 2025-12-22 12:08:57 +09:00
akawana
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
Some checks failed
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
David
ae6c7dd673
Changed Main Dialog to match aesthetics and close button location as Original ComfyUI Interface (#2349)
Some checks are pending
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
Some checks are pending
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
shootthesound
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
Some checks failed
Python Linting / Run Ruff (push) Has been cancelled
2025-12-17 13:03:57 +09:00
Dr.Lt.Data
9eb4c3ab23 update DB
Some checks are pending
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
revisiontony
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
tppp2806
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
akawana
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
Some checks are pending
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
Some checks are pending
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
Some checks are pending
Python Linting / Run Ruff (push) Waiting to run
2025-12-15 12:57:52 +09:00
Dr.Lt.Data
f32dd80c24 update DB
Some checks are pending
Python Linting / Run Ruff (push) Waiting to run
2025-12-15 03:16:01 +09:00
Gero Doll
a06ba343de
Add ComfyUI-PromptGenerator to custom node list (#2391)
Some checks are pending
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
Some checks failed
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
SKFRMSEHF
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
Benjamin Lu
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
Some checks are pending
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
Some checks are pending
Python Linting / Run Ruff (push) Waiting to run
2025-12-12 05:34:33 +09:00
Jean Kássio
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
hkun
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
Denys
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
Braeden
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 Kenji
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
Lord Lethris
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
Some checks failed
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
Some checks are pending
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
Some checks failed
Python Linting / Run Ruff (push) Has been cancelled
2025-12-08 23:28:26 +09:00
Dr.Lt.Data
29014699bb update DB
Some checks are pending
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
DayMan84
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
UmeAiRT
17e5ae6bc2
Add ComfyUI-UmeAiRT-Sync (#2360) 2025-12-08 21:31:52 +09:00
akawana
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
Some checks are pending
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
shootthesound
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 Kraken
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
ds
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
Some checks are pending
Python Linting / Run Ruff (push) Waiting to run
2025-12-07 07:36:12 +09:00
Matthew-X
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 Covelo
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
ameyukisora
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ássio
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
HALXP
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 Sorrentini
95ecd85a12
Add ComfyUI-LegionPower node with description (#2326) 2025-12-07 07:11:33 +09:00
Dr.Lt.Data
5c475e3c15 update DB
Some checks failed
Python Linting / Run Ruff (push) Has been cancelled
2025-12-05 19:03:46 +09:00
Dr.Lt.Data
f705ee6863 update DB
Some checks are pending
Python Linting / Run Ruff (push) Waiting to run
2025-12-05 13:00:22 +09:00
akawana
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
rjgoif
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
vramfcker
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
llikethat
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
Fatih Eke
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
fredlef
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
Some checks failed
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-kun
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
luxdelux7
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
Pondowner857
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
llikethat
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.co
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
HALXP
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
27 changed files with 48394 additions and 9488 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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

22958
github-stats-cache.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -44,7 +44,7 @@ import manager_migration
from node_package import InstalledNodePackage
version_code = [3, 38]
version_code = [3, 39]
version_str = f"V{version_code[0]}.{version_code[1]}" + (f'.{version_code[2]}' if len(version_code) > 2 else '')
@ -2253,9 +2253,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()
@ -2523,22 +2531,22 @@ def update_to_stable_comfyui(repo_path):
logging.error('\t'+branch.name)
return "fail", None
versions, current_tag, _ = get_comfyui_versions(repo)
versions, current_tag, latest_tag = get_comfyui_versions(repo)
if len(versions) == 0 or (len(versions) == 1 and versions[0] == 'nightly'):
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]
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 current_tag == latest_tag:
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:
@ -3362,36 +3370,80 @@ async def restore_snapshot(snapshot_path, git_helper_extras=None):
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

View File

@ -93,7 +93,7 @@ def check_legacy_backup(manager_files_path):
# Notice board output
add_startup_notice(
"Legacy ComfyUI-Manager data backup exists. Please verify and remove when no longer needed.",
"Legacy ComfyUI-Manager data backup exists. Please verify and remove when no longer needed. See terminal for details.",
level='info'
)

View File

@ -38,6 +38,25 @@ SECURITY_MESSAGE_NORMAL_MINUS_MODEL = "ERROR: Downloading models that are not in
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:
@ -182,10 +201,19 @@ def set_preview_method(method):
core.get_config()['preview_method'] = method
if args.preview_method == latent_preview.LatentPreviewMethod.NoPreviews:
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.")
logging.warning(
"[ComfyUI-Manager] Since --preview-method is set, "
"ComfyUI-Manager's preview method feature will be ignored."
)
def set_component_policy(mode):
@ -1482,14 +1510,26 @@ async def install_model(request):
@routes.get("/manager/preview_method")
async def preview_method(request):
# Setting change request
if "value" in request.rel_url.query:
# Reject setting change if per-queue preview feature is available
if COMFYUI_HAS_PER_QUEUE_PREVIEW:
return web.Response(text="DISABLED", status=403)
# Process normally if not available
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)
return web.Response(status=200)
# Status query request
else:
# Return DISABLED if per-queue preview feature is available
if COMFYUI_HAS_PER_QUEUE_PREVIEW:
return web.Response(text="DISABLED", status=200)
# Return current value if not available
return web.Response(text=core.manager_funcs.get_current_preview_method(), status=200)
@routes.get("/manager/db_mode")
async def db_mode(request):

View File

@ -55,7 +55,7 @@ def get_pip_cmd(force_uv=False):
subprocess.check_output(test_cmd, stderr=subprocess.DEVNULL, timeout=5)
return [sys.executable] + (['-s'] if embedded else []) + ['-m', 'pip']
except Exception:
logging.warning("[ComfyUI-Manager] python -m pip not available. Falling back to uv.")
logging.warning("[ComfyUI-Manager] `python -m pip` not available. Falling back to `uv`.")
# Try uv (either forced or pip failed)
import shutil
@ -64,19 +64,19 @@ def get_pip_cmd(force_uv=False):
try:
test_cmd = [sys.executable] + (['-s'] if embedded else []) + ['-m', 'uv', '--version']
subprocess.check_output(test_cmd, stderr=subprocess.DEVNULL, timeout=5)
logging.info("[ComfyUI-Manager] Using uv as Python module for pip operations.")
logging.info("[ComfyUI-Manager] Using `uv` as Python module for pip operations.")
return [sys.executable] + (['-s'] if embedded else []) + ['-m', 'uv', 'pip']
except Exception:
pass
# Try standalone uv
if shutil.which('uv'):
logging.info("[ComfyUI-Manager] Using standalone uv for pip operations.")
logging.info("[ComfyUI-Manager] Using standalone `uv` for pip operations.")
return ['uv', 'pip']
# Nothing worked
logging.error("[ComfyUI-Manager] Neither python -m pip nor uv are available. Cannot proceed with package operations.")
raise Exception("Neither pip nor uv are available for package management")
logging.error("[ComfyUI-Manager] Neither `python -m pip` nor `uv` are available. Cannot proceed with package operations.")
raise Exception("Neither `pip` nor `uv` are available for package management")
def make_pip_cmd(cmd):

227
js/comfyui-gui-builder.js Normal file
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;
}

View File

@ -20,6 +20,7 @@ import { ComponentBuilderDialog, getPureName, load_components, set_component_pol
import { CustomNodesManager } from "./custom-nodes-manager.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 +45,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 +85,7 @@ docStyle.innerHTML = `
}
.cm-menu-container {
padding : calc(var(--spacing)*2);
column-gap: 20px;
display: flex;
flex-wrap: wrap;
@ -140,8 +146,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 +244,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 +314,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 {
@ -831,7 +825,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: {
@ -842,7 +836,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: {
@ -853,7 +847,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()
@ -861,7 +855,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:
@ -870,7 +864,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:
@ -880,7 +874,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:
@ -892,7 +886,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:
@ -904,7 +898,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:
@ -916,8 +910,8 @@ class ManagerMenuDialog extends ComfyDialog {
}
}),
$el("br", {}, []),
$el("button.cm-button", {
$el("div", {}, []),
$el("button.p-button.p-component.cm-button", {
type: "button",
textContent: "Model Manager",
onclick:
@ -929,7 +923,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 () => {
@ -941,13 +935,13 @@ 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,
];
@ -960,12 +954,13 @@ 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())
@ -975,27 +970,110 @@ class ManagerMenuDialog extends ComfyDialog {
api.fetchApi(`/manager/db_mode?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?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 => {
@ -1004,7 +1082,7 @@ 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]}` }, []));
}
}
@ -1019,11 +1097,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'],
@ -1034,7 +1114,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')
@ -1056,12 +1136,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 => {
@ -1074,15 +1156,14 @@ class ManagerMenuDialog extends ComfyDialog {
set_component_policy(event.target.value);
});
const componentSetttingItem = createSettingsCombo("Component", component_policy_combo);
update_policy_combo = document.createElement("select");
if(isElectron)
update_policy_combo.style.display = 'none';
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 => {
@ -1093,20 +1174,22 @@ class ManagerMenuDialog extends ComfyDialog {
api.fetchApi(`/manager/policy/update?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);
$el("br", {}, []),
$el("filedset.cm-experimental", {}, [
if(isElectron)
updateSetttingItem.style.display = 'none';
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:
@ -1116,7 +1199,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:
@ -1134,7 +1217,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",
@ -1185,11 +1268,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")
@ -1212,7 +1295,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)',
@ -1228,13 +1311,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");
@ -1249,31 +1331,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.comfy-modal-content",
const content = $el("div.cm-menu-container",
[
$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()])
]),
$el("br", {}, []),
close_button,
$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()])
]
);
content.style.width = '100%';
content.style.height = '100%';
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
this.element = $el("div.comfy-modal", { id:'cm-manager-dialog', parent: document.body }, [ content ]);
this.element = frame;
}
get isVisible() {
@ -1281,7 +1355,7 @@ class ManagerMenuDialog extends ComfyDialog {
}
show() {
this.element.style.display = "block";
this.element.style.display = "flex";
}
toggleVisibility() {

View File

@ -1,8 +1,9 @@
.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: 80%;
height: 80%;
width: 80vw;
height: 75vh;
min-height: 30em;
display: flex;
flex-direction: column;
gap: 10px;
@ -10,6 +11,7 @@
font-family: arial, sans-serif;
text-underline-offset: 3px;
outline: none;
margin: calc(var(--spacing)*2);
}
.cn-manager .cn-flex-auto {
@ -17,17 +19,21 @@
}
.cn-manager button {
width: auto;
position: relative;
overflow: hidden;
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;
}
.cn-manager button:hover {
filter: brightness(125%);
}
.cn-manager button:disabled,
.cn-manager input:disabled,
.cn-manager select:disabled {
@ -40,8 +46,13 @@
.cn-manager .cn-manager-restart {
display: none;
background-color: #500000;
color: white;
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 {
@ -79,7 +90,6 @@
flex-wrap: wrap;
gap: 5px;
align-items: center;
padding: 0 5px;
}
.cn-manager-header label {
@ -91,16 +101,32 @@
.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 {
@ -588,6 +614,10 @@
height: 100%;
}
.cn-install-buttons button {
padding: 4px 8px;
}
.cn-selected-buttons {
display: flex;
gap: 5px;

View File

@ -1,6 +1,7 @@
import { app } from "../../scripts/app.js";
import { ComfyDialog, $el } from "../../scripts/ui.js";
import { api } from "../../scripts/api.js";
import { buildGuiFrameCustomHeader, createSettingsCombo } from "./comfyui-gui-builder.js";
import {
manager_instance, rebootAPI, install_via_git_url,
@ -18,32 +19,19 @@ loadCss("./custom-nodes-manager.css");
const gridId = "node";
const pageHtml = `
<div class="cn-manager-header">
<label>Filter
<select class="cn-manager-filter"></select>
</label>
<input class="cn-manager-keywords" type="search" placeholder="Search" />
<div class="cn-manager-status"></div>
<div class="cn-flex-auto"></div>
<div class="cn-manager-channel"></div>
</div>
<div class="cn-manager cn-manager-dark">
<div class="cn-manager-grid"></div>
<div class="cn-manager-selection"></div>
<div class="cn-manager-message"></div>
<div class="cn-manager-footer">
<button class="cn-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="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Back
</button>
<button class="cn-manager-restart">Restart</button>
<button class="cn-manager-stop">Stop</button>
<button class="cn-manager-restart p-button p-component">Restart</button>
<button class="cn-manager-stop p-button p-component">Stop</button>
<div class="cn-flex-auto"></div>
<button class="cn-manager-used-in-workflow">Used In Workflow</button>
<button class="cn-manager-check-update">Check Update</button>
<button class="cn-manager-check-missing">Check Missing</button>
<button class="cn-manager-install-url">Install via Git URL</button>
<button class="cn-manager-used-in-workflow p-button p-component">Used In Workflow</button>
<button class="cn-manager-check-update p-button p-component">Check Update</button>
<button class="cn-manager-check-missing p-button p-component">Check Missing</button>
<button class="cn-manager-install-url p-button p-component">Install via Git URL</button>
</div>
</div>
`;
@ -89,11 +77,26 @@ export class CustomNodesManager {
}
init() {
this.element = $el("div", {
parent: document.body,
className: "comfy-modal cn-manager"
});
this.element.innerHTML = pageHtml;
const header = $el("div.cn-manager-header.px-2", {}, [
// $el("label", {}, [
// $el("span", { textContent: "Filter" }),
// $el("select.cn-manager-filter")
// ]),
createSettingsCombo("Filter", $el("select.cn-manager-filter")),
$el("input.cn-manager-keywords.p-inputtext.p-component", { type: "search", placeholder: "Search" }),
$el("div.cn-manager-status"),
$el("div.cn-flex-auto"),
$el("div.cn-manager-channel")
]);
const frame = buildGuiFrameCustomHeader(
'cn-manager-dialog', // dialog id
header, // custom header element
pageHtml, // dialog content element
this
); // send this so we can attach close functions
this.element = frame;
this.element.setAttribute("tabindex", 0);
this.element.focus();
@ -372,7 +375,7 @@ export class CustomNodesManager {
return list.map(id => {
const bt = buttons[id];
return `<button class="cn-btn-${id}" group="${action}" mode="${bt.mode}">${bt.label}</button>`;
return `<button class="cn-btn-${id} p-button p-component" group="${action}" mode="${bt.mode}">${bt.label}</button>`;
}).join("");
}
@ -655,7 +658,6 @@ export class CustomNodesManager {
}
renderGrid() {
// update theme
const globalStyle = window.getComputedStyle(document.body);
this.colorVars = {

View File

@ -1,13 +1,15 @@
.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%;
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 {
@ -18,14 +20,15 @@
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:hover {
filter: brightness(125%);
}
.cmm-manager button:disabled,
.cmm-manager input:disabled,
.cmm-manager select:disabled {
@ -38,7 +41,7 @@
.cmm-manager .cmm-manager-refresh {
display: none;
background-color: #000080;
background-color: #000080 !important;
color: white;
}
@ -53,7 +56,6 @@
flex-wrap: wrap;
gap: 5px;
align-items: center;
padding: 0 5px;
}
.cmm-manager-header label {
@ -67,16 +69,34 @@
.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 {
@ -148,6 +168,10 @@
color: white;
}
.cmm-btn-install {
padding: 4px 8px;
}
.cmm-btn-download {
width: 18px;
height: 18px;

View File

@ -9,40 +9,23 @@ 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 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 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-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>
<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>
`;
export class ModelManager {
@ -64,11 +47,23 @@ export class ModelManager {
}
init() {
this.element = $el("div", {
parent: document.body,
className: "comfy-modal cmm-manager"
});
this.element.innerHTML = pageHtml;
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")
]);
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 = frame;
this.initFilter();
this.bindEvents();
this.initGrid();
@ -347,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',
@ -420,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) {

65
js/snapshot.css Normal file
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;
}

View File

@ -1,8 +1,10 @@
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, handle403Response } 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) {
@ -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');
}
}
}
@ -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();
@ -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) {

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()

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

View File

@ -1,5 +1,15 @@
{
"custom_nodes": [
{
"author": "Fossiel",
"title": "ComfyUI-MultiGPU-Patched",
"reference": "https://github.com/Fossiel/ComfyUI-MultiGPU-Patched",
"files": [
"https://github.com/Fossiel/ComfyUI-MultiGPU-Patched"
],
"install_type": "git-clone",
"description": "Patched fork of ComfyUI-MultiGPU providing universal .safetensors and GGUF multi-GPU distribution with DisTorch 2.0 engine, model-driven allocation options (bytes/ratio modes), WanVideoWrapper integration, and up to 10% faster GGUF inference. (Description by CC)"
},
{
"author": "synchronicity-labs",
"title": "ComfyUI Sync Lipsync Node",

View File

@ -1,5 +1,378 @@
{
"custom_nodes": [
{
"author": "yutrodimitri-ship-it",
"title": "ComfyUI-YUTRO-CastingStudio-v2 [REMOVED]",
"reference": "https://github.com/yutrodimitri-ship-it/ComfyUI-YUTRO-CastingStudio-v2",
"files": [
"https://github.com/yutrodimitri-ship-it/ComfyUI-YUTRO-CastingStudio-v2"
],
"install_type": "git-clone",
"description": "A professional modular suite of nodes for ComfyUI designed for virtual casting agencies, professional photographers, and content creators to generate high-quality model portfolios efficiently. (Description by CC)\nNOTE: The files in the repo are not organized."
},
{
"author": "amamisonlyuser",
"title": "MixvtonComfyui [REMOVED]",
"reference": "https://github.com/amamisonlyuser/MixvtonComfyui",
"files": [
"https://github.com/amamisonlyuser/MixvtonComfyui"
],
"install_type": "git-clone",
"description": "NODES: CXH_Leffa_Viton_Load, CXH_Leffa_Viton_Run\nNOTE: The files in the repo are not organized."
},
{
"author": "AhBumm",
"title": "ComfyUI_MangaLineExtraction [REMOVED]",
"reference": "https://github.com/AhBumm/ComfyUI_MangaLineExtraction-hf",
"files": [
"https://github.com/AhBumm/ComfyUI_MangaLineExtraction-hf"
],
"description": "p1atdev/MangaLineExtraction-hf as a node in comfyui",
"install_type": "git-clone"
},
{
"author": "danieljanata",
"title": "ComfyUI-QwenVL-Override [REMOVED]",
"reference": "https://github.com/danieljanata/ComfyUI-QwenVL-Override",
"files": [
"https://github.com/danieljanata/ComfyUI-QwenVL-Override"
],
"install_type": "git-clone",
"description": "Adds two nodes that reuse upstream ComfyUI-QwenVL presets but add a runtime override that can be wired/unwired without getting stuck."
},
{
"author": "Futureversecom",
"title": "ComfyUI-JEN [REMOVED]",
"reference": "https://github.com/futureversecom/ComfyUI-JEN",
"files": [
"https://github.com/futureversecom/ComfyUI-JEN"
],
"install_type": "git-clone",
"description": "Comfy UI custom nodes for JEN music generation powered by Futureverse"
},
{
"author": "TheBill2001",
"title": "comfyui-upscale-by-model [REMOVED]",
"reference": "https://github.com/TheBill2001/comfyui-upscale-by-model",
"files": [
"https://github.com/TheBill2001/comfyui-upscale-by-model"
],
"install_type": "git-clone",
"description": "This custom node allow upscaling an image by a factor using a model."
},
{
"author": "XYMikky12138",
"title": "ComfyUI-NanoBanana-inpaint [REMOVED]",
"reference": "https://github.com/XYMikky12138/ComfyUI-NanoBanana-inpaint",
"files": [
"https://github.com/XYMikky12138/ComfyUI-NanoBanana-inpaint"
],
"install_type": "git-clone",
"description": "ComfyUI nodes for API-based inpainting (Gemini, Imagen) with aspect ratio constraints, smart cropping, resize fitting, intelligent paste-back with transparency support. (Description by CC)"
},
{
"author": "Blonicx",
"title": "ComfyUI-Rework-X [REMOVED]",
"id": "rework-x",
"reference": "https://github.com/Blonicx/ComfyUI-X-Rework",
"files": [
"https://github.com/Blonicx/ComfyUI-X-Rework"
],
"install_type": "git-clone",
"description": "This is a plugin for ComfyUI that adds new Util Nodes and Nodes for easier image creation and sharing."
},
{
"author": "scott-createplay",
"title": "ComfyUI_video_essentials [REMOVED]",
"reference": "https://github.com/scott-createplay/ComfyUI_video_essentials",
"files": [
"https://github.com/scott-createplay/ComfyUI_video_essentials"
],
"install_type": "git-clone",
"description": "Essential video processing nodes for ComfyUI"
},
{
"author": "thnikk",
"title": "comfyui-thnikk-utils [REMOVED]",
"reference": "https://github.com/thnikk/comfyui-thnikk-utils",
"files": [
"https://github.com/thnikk/comfyui-thnikk-utils"
],
"install_type": "git-clone",
"description": "Nodes to clean up your workflow."
},
{
"author": "Tr1dae",
"title": "LoRA Matcher Nodes for ComfyUI [REMOVED]",
"reference": "https://github.com/Tr1dae/ComfyUI-LoraPromptMatcher",
"files": [
"https://github.com/Tr1dae/ComfyUI-LoraPromptMatcher"
],
"install_type": "git-clone",
"description": "This custom node provides two different approaches to automatically match text prompts with LoRA models using their descriptions."
},
{
"author": "jkhayiying",
"title": "ImageLoadFromLocalOrUrl Node for ComfyUI [REMOVED]",
"id": "JkhaImageLoaderPathOrUrl",
"reference": "https://gitee.com/yyh915/jkha-load-img",
"files": [
"https://gitee.com/yyh915/jkha-load-img"
],
"install_type": "git-clone",
"description": "This is a node to load an image from local path or url."
},
{
"author": "pizurny",
"title": "ComfyUI-Just-DWPose [REMOVED]",
"reference": "https://github.com/pizurny/ComfyUI-Just-DWPose",
"files": [
"https://github.com/pizurny/ComfyUI-Just-DWPose"
],
"install_type": "git-clone",
"description": "An advanced DWPose annotator for ComfyUI with TorchScript and ONNX backends, featuring comprehensive pose detection, bone validation, temporal smoothing, and custom visualization tools."
},
{
"author": "lfelipegg",
"title": "lfgg_custom_nodes [REMOVED]",
"reference": "https://github.com/lfelipegg/lfgg_custom_nodes",
"files": [
"https://github.com/lfelipegg/lfgg_custom_nodes"
],
"install_type": "git-clone",
"description": "NODES: ModelMergeCombos\nNOTE: The files in the repo are not organized."
},
{
"author": "AndSni",
"title": "Comfy-FL-Nodes [REMOVED]",
"reference": "https://github.com/AndSni/Comfy-FL-Nodes",
"files": [
"https://github.com/AndSni/Comfy-FL-Nodes"
],
"install_type": "git-clone",
"description": "Generates human characters for commerce applications in ComfyUI. (Description by CC)"
},
{
"author": "neeltheninja",
"title": "ComfyUI-ControlNeXt [REMOVED]",
"reference": "https://github.com/neverbiasu/ComfyUI-ControlNeXt",
"files": [
"https://github.com/neverbiasu/ComfyUI-ControlNeXt"
],
"install_type": "git-clone",
"description": "In progress🚧"
},
{
"author": "TheArtOfficial",
"title": "ComfyUI-Nitra [REMOVED]",
"reference": "https://github.com/TheArtOfficial/ComfyUI-Nitra",
"files": [
"https://github.com/TheArtOfficial/ComfyUI-Nitra"
],
"install_type": "git-clone",
"description": "Nitra custom node for ComfyUI"
},
{
"author": "AngelCookies",
"title": "ComfyUI-Seed-Tracker [REMOVED]",
"reference": "https://github.com/AngelCookies/ComfyUI-Seed-Tracker",
"files": [
"https://github.com/AngelCookies/ComfyUI-Seed-Tracker"
],
"install_type": "git-clone",
"description": "A ComfyUI extension that tracks random seeds throughout your image generation workflows"
},
{
"author": "shinich39",
"title": "comfyui-nothing-happened [REMOVED]",
"reference": "httphttps://github.com/shinich39/comfyui-nothing-happened",
"files": [
"https://github.com/shinich39/comfyui-nothing-happened"
],
"description": "Save image and keep metadata.",
"install_type": "git-clone"
},
{
"author": "ashtar1984",
"title": "comfyui-switch-bypass-mute-by-group [REMOVED]",
"reference": "https://github.com/ashtar1984/comfyui-switch-bypass-mute-by-group",
"files": [
"https://github.com/ashtar1984/comfyui-switch-bypass-mute-by-group"
],
"install_type": "git-clone",
"description": "ComfyUI custom node for group-based node switching, bypassing, and muting control. (Description by CC)"
},
{
"author": "wallen0322",
"title": "ComfyUI-TTM-WAN22 [REMOVED]",
"reference": "https://github.com/wallen0322/ComfyUI-TTM-WAN22",
"files": [
"https://github.com/wallen0322/ComfyUI-TTM-WAN22"
],
"install_type": "git-clone",
"description": "TTM (Time-to-Move) node for ComfyUI enabling motion-controlled video generation with Wan2.2 models using dual-clock denoising for independent background and object animation control."
},
{
"author": "cdanielp",
"title": "COMFYUI_PROMPTMODELS [REMOVED]",
"reference": "https://github.com/cdanielp/COMFYUI_PROMPTMODELS",
"files": [
"https://github.com/cdanielp/COMFYUI_PROMPTMODELS"
],
"install_type": "git-clone",
"description": "Custom nodes for ComfyUI by PROMPTMODELS."
},
{
"author": "mcrataobrabo",
"title": "comfyui-smart-lora-downloader - Automatically Fetch Missing LoRAs [REMOVED]",
"reference": "https://github.com/mcrataobrabo/comfyui-smart-lora-downloader",
"files": [
"https://github.com/mcrataobrabo/comfyui-smart-lora-downloader"
],
"install_type": "git-clone",
"description": "Automatically detect and download missing LoRAs for ComfyUI workflows"
},
{
"author": "KANAsho34636",
"title": "ComfyUI-NaturalSort-ImageLoader [REMOVED]",
"reference": "https://github.com/KANAsho34636/ComfyUI-NaturalSort-ImageLoader",
"files": [
"https://github.com/KANAsho34636/ComfyUI-NaturalSort-ImageLoader"
],
"install_type": "git-clone",
"description": "Custom image loader node supporting natural number sorting with multiple sort modes (natural, lexicographic, modification time, creation time, reverse natural). (Description by CC)"
},
{
"author": "johninthewinter",
"title": "comfyui-fal-flux-2-John [REMOVED]",
"reference": "https://github.com/johninthewinter/comfyui-fal-flux-2-John",
"files": [
"https://github.com/johninthewinter/comfyui-fal-flux-2-John"
],
"install_type": "git-clone",
"description": "Custom nodes for ComfyUI that integrate with fal.ai's FLUX 2 and FLUX 1 LoRA APIs for text-to-image generation."
},
{
"author": "LargeModGames",
"title": "ComfyUI LoRA Auto Downloader [REMOVED]",
"reference": "https://github.com/LargeModGames/comfyui-smart-lora-downloader",
"files": [
"https://github.com/LargeModGames/comfyui-smart-lora-downloader"
],
"install_type": "git-clone",
"description": "Automatically download missing LoRAs from CivitAI and detect missing LoRAs in workflows. Features smart directory detection and easy installation."
},
{
"author": "DiffusionWave",
"title": "PickResolution_DiffusionWave [DEPRECATED]",
"reference": "https://github.com/DiffusionWave/PickResolution_DiffusionWave",
"files": [
"https://github.com/DiffusionWave/PickResolution_DiffusionWave"
],
"install_type": "git-clone",
"description": "A custom node for ComfyUI that allows selecting a base resolution, applying a custom scaling value based on FLOAT (up to 10 decimal places), and adding an extra integer value. Outputs include both INT and FLOAT resolutions, making it perfect for you to play around with."
},
{
"author": "geltz",
"title": "ComfyUI-geltz [REMOVED]",
"reference": "https://github.com/geltz/ComfyUI-geltz",
"files": [
"https://github.com/geltz/ComfyUI-geltz"
],
"install_type": "git-clone",
"description": "Various custom nodes; guidance, latents, sampling, tokenization, etc."
},
{
"author": "anilsathyan7",
"title": "ComfyUI-Crystal-Upscaler [REMOVED]",
"reference": "https://github.com/anilsathyan7/ComfyUI-Crystal-Upscaler",
"files": [
"https://github.com/anilsathyan7/ComfyUI-Crystal-Upscaler"
],
"install_type": "git-clone",
"description": "ComfyUI custom node for image upscaling using crystal upscaling technology. (Description by CC)"
},
{
"author": "nohikomiso",
"title": "ComfyUI-ImageFolderPicker [REMOVED/UNSAFE]",
"reference": "https://github.com/nohikomiso/ComfyUI-ImageFolderPicker",
"files": [
"https://github.com/nohikomiso/ComfyUI-ImageFolderPicker"
],
"install_type": "git-clone",
"description": "Custom ComfyUI node for browsing local server folders and selecting images via thumbnail display in a grid interface. (Description by CC)[w/This nodepack has a vulnerability that allows it to retrieve a list of files from arbitrary paths.]"
},
{
"author": "rzasharp79",
"title": "ComfyUI--SolarFlare [REMOVED]",
"reference": "https://github.com/rzasharp79/ComfyUI--SolarFlare",
"files": [
"https://github.com/rzasharp79/ComfyUI--SolarFlare"
],
"install_type": "git-clone",
"description": "NODES: Qwen Image, ..."
},
{
"author": "shinich39",
"title": "comfyui-no-one-above-me [REMOVED]",
"reference": "https://github.com/shinich39/comfyui-no-one-above-me",
"files": [
"https://github.com/shinich39/comfyui-no-one-above-me"
],
"install_type": "git-clone",
"description": "Fix node to top."
},
{
"author": "octapus8085",
"title": "OpenAI-comfyui-O [REMOVED]",
"reference": "https://github.com/Spicely/Comfyui-File-Utils",
"files": [
"https://github.com/Spicely/Comfyui-File-Utils"
],
"install_type": "git-clone",
"description": "This plugin provides multiple file-handling and utility nodes for ComfyUI, including: image saving, audio saving, video saving, video composition, audio-to-subtitle conversion, and random number generation nodes. These nodes not only process files but also return their absolute file paths.\nNOTE: The files in the repo are not organized.[w/This nodepack contains a node that has a vulnerability allowing write to arbitrary file paths.]"
},
{
"author": "yemanou",
"title": "NABA Image (Gemini REST) Node [REMOVED]",
"reference": "https://github.com/yemanou/ComfyUI-NABA",
"files": [
"https://github.com/yemanou/ComfyUI-NABA"
],
"install_type": "git-clone",
"description": "Simplified Gemini 2.5 Flash Image Preview node for ComfyUI. REST-only for stability, two optional reference images, padded aspect ratio resizing (no stretching), and basic sampling controls. All extra debug layers, SDK path, multi-seed, and legacy compatibility code removed to avoid crashes."
},
{
"author": "comrender",
"title": "ComfyUI-Nano-Banana-Resizer [REMOVED]",
"reference": "https://github.com/comrender/ComfyUI-Nano-Banana-Resizer",
"files": [
"https://github.com/comrender/ComfyUI-Nano-Banana-Resizer"
],
"install_type": "git-clone",
"description": "A ComfyUI custom node that automatically calculates optimal output dimensions for Google's Nano Banana image editing model, supporting 22 aspect ratio buckets and ensuring pixel-perfect outputs without shifting or cropping."
},
{
"author": "comrender",
"title": "ComfyUI-edge-match-checker [REMOVED]",
"reference": "https://github.com/comrender/ComfyUI-edge-match-checker",
"files": [
"https://github.com/comrender/ComfyUI-edge-match-checker"
],
"install_type": "git-clone",
"description": "Node comparing two image masks or images with adjustable overlap threshold (default 95%) for detecting minor shifts and mismatches in proportions, suitable for automated post-processing validation. (Description by CC)"
},
{
"author": "comrender",
"title": "ComfyUI-gpt5_image_text [REMOVED]",
"reference": "https://github.com/comrender/ComfyUI-gpt5_image_text",
"files": [
"https://github.com/comrender/ComfyUI-gpt5_image_text"
],
"install_type": "git-clone",
"description": "A ComfyUI custom node for vision + text analysis using GPT-5 and GPT-4o with direct API key input, system prompt, temperature, max tokens, and multi-image support."
},
{
"author": "PozzettiAndrea",
"title": "ComfyUI-CameraAnalysis [REMOVED]",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
[project]
name = "comfyui-manager"
description = "ComfyUI-Manager provides features to install and manage custom nodes for ComfyUI, as well as various functionalities to assist with ComfyUI."
version = "3.38"
version = "3.39"
license = { file = "LICENSE.txt" }
dependencies = ["GitPython", "PyGithub", "matrix-nio", "transformers", "huggingface-hub>0.20", "typer", "rich", "typing-extensions", "toml", "uv", "chardet"]

View File

@ -16,6 +16,108 @@ import sys
from urllib.parse import urlparse
from github import Github, Auth
from pathlib import Path
from typing import Set, Dict, Optional
# Scanner version for cache invalidation
SCANNER_VERSION = "2.0.12" # Add dict comprehension + export list detection
# Cache for extract_nodes and extract_nodes_enhanced results
_extract_nodes_cache: Dict[str, Set[str]] = {}
_extract_nodes_enhanced_cache: Dict[str, Set[str]] = {}
_file_mtime_cache: Dict[Path, float] = {}
def _get_repo_root(file_path: Path) -> Optional[Path]:
"""Find the repository root directory containing .git"""
current = file_path if file_path.is_dir() else file_path.parent
while current != current.parent:
if (current / ".git").exists():
return current
current = current.parent
return None
def _get_repo_hash(repo_path: Path) -> str:
"""Get git commit hash or fallback identifier"""
git_dir = repo_path / ".git"
if not git_dir.exists():
return ""
try:
# Read HEAD to get current commit
head_file = git_dir / "HEAD"
if head_file.exists():
head_content = head_file.read_text().strip()
if head_content.startswith("ref:"):
# HEAD points to a ref
ref_path = git_dir / head_content[5:].strip()
if ref_path.exists():
commit_hash = ref_path.read_text().strip()
return commit_hash[:16] # First 16 chars
else:
# Detached HEAD
return head_content[:16]
except:
pass
return ""
def _load_per_repo_cache(repo_path: Path) -> Optional[tuple]:
"""Load nodes and metadata from per-repo cache
Returns:
tuple: (nodes_set, metadata_dict) or None if cache invalid
"""
cache_file = repo_path / ".git" / "nodecache.json"
if not cache_file.exists():
return None
try:
with open(cache_file, 'r') as f:
cache_data = json.load(f)
# Verify scanner version
if cache_data.get('scanner_version') != SCANNER_VERSION:
return None
# Verify git hash
current_hash = _get_repo_hash(repo_path)
if cache_data.get('git_hash') != current_hash:
return None
# Return nodes and metadata
nodes = cache_data.get('nodes', [])
metadata = cache_data.get('metadata', {})
return (set(nodes) if nodes else set(), metadata)
except:
return None
def _save_per_repo_cache(repo_path: Path, all_nodes: Set[str], metadata: dict = None):
"""Save nodes and metadata to per-repo cache"""
cache_file = repo_path / ".git" / "nodecache.json"
if not cache_file.parent.exists():
return
git_hash = _get_repo_hash(repo_path)
cache_data = {
"scanner_version": SCANNER_VERSION,
"git_hash": git_hash,
"scanned_at": datetime.datetime.now().isoformat(),
"nodes": sorted(list(all_nodes)),
"metadata": metadata if metadata else {}
}
try:
with open(cache_file, 'w') as f:
json.dump(cache_data, f, indent=2)
except:
pass # Silently fail - cache is optional
def download_url(url, dest_folder, filename=None):
@ -51,11 +153,12 @@ Examples:
# Standard mode
python3 scanner.py
python3 scanner.py --skip-update
python3 scanner.py --skip-all --force-rescan
# Scan-only mode
python3 scanner.py --scan-only temp-urls-clean.list
python3 scanner.py --scan-only urls.list --temp-dir /custom/temp
python3 scanner.py --scan-only urls.list --skip-update
python3 scanner.py --scan-only urls.list --skip-update --force-rescan
'''
)
@ -69,6 +172,8 @@ Examples:
help='Skip GitHub stats collection')
parser.add_argument('--skip-all', action='store_true',
help='Skip all update operations')
parser.add_argument('--force-rescan', action='store_true',
help='Force rescan all nodes (ignore cache)')
# Backward compatibility: positional argument for temp_dir
parser.add_argument('temp_dir_positional', nargs='?', metavar='TEMP_DIR',
@ -94,6 +199,11 @@ parse_cnt = 0
def extract_nodes(code_text):
global parse_cnt
# Check cache first
cache_key = hash(code_text)
if cache_key in _extract_nodes_cache:
return _extract_nodes_cache[cache_key].copy()
try:
if parse_cnt % 100 == 0:
print(".", end="", flush=True)
@ -128,12 +238,614 @@ def extract_nodes(code_text):
if key is not None and isinstance(key.value, str):
s.add(key.value.strip())
# Cache the result
_extract_nodes_cache[cache_key] = s
return s
else:
# Cache empty result
_extract_nodes_cache[cache_key] = set()
return set()
except:
# Cache empty result on error
_extract_nodes_cache[cache_key] = set()
return set()
def extract_nodes_from_repo(repo_path: Path, verbose: bool = False, force_rescan: bool = False) -> tuple:
"""
Extract all nodes and metadata from a repository with per-repo caching.
Automatically caches results in .git/nodecache.json.
Cache is invalidated when:
- Git commit hash changes
- Scanner version changes
- force_rescan flag is True
Args:
repo_path: Path to repository root
verbose: If True, print UI-only extension detection messages
force_rescan: If True, ignore cache and force fresh scan
Returns:
tuple: (nodes_set, metadata_dict)
"""
# Ensure path is absolute
repo_path = repo_path.resolve()
# Check per-repo cache first (unless force_rescan is True)
if not force_rescan:
cached_result = _load_per_repo_cache(repo_path)
if cached_result is not None:
return cached_result
# Cache miss - scan all .py files
all_nodes = set()
all_metadata = {}
py_files = list(repo_path.rglob("*.py"))
# Filter out __pycache__, .git, and other hidden directories
filtered_files = []
for f in py_files:
try:
rel_path = f.relative_to(repo_path)
# Skip __pycache__, .git, and any directory starting with .
if '__pycache__' not in str(rel_path) and not any(part.startswith('.') for part in rel_path.parts):
filtered_files.append(f)
except:
continue
py_files = filtered_files
for py_file in py_files:
try:
# Read file with proper encoding
with open(py_file, 'r', encoding='utf-8', errors='ignore') as f:
code = f.read()
if code:
# Extract nodes using SAME logic as scan_in_file
# V1 nodes (enhanced with fallback patterns)
nodes = extract_nodes_enhanced(code, py_file, visited=set(), verbose=verbose)
all_nodes.update(nodes)
# V3 nodes detection
v3_nodes = extract_v3_nodes(code)
all_nodes.update(v3_nodes)
# Dict parsing - exclude commented NODE_CLASS_MAPPINGS lines
pattern = r"_CLASS_MAPPINGS\s*(?::\s*\w+\s*)?=\s*(?:\\\s*)?{([^}]*)}"
regex = re.compile(pattern, re.MULTILINE | re.DOTALL)
for match_obj in regex.finditer(code):
# Get the line where NODE_CLASS_MAPPINGS is defined
match_start = match_obj.start()
line_start = code.rfind('\n', 0, match_start) + 1
line_end = code.find('\n', match_start)
if line_end == -1:
line_end = len(code)
line = code[line_start:line_end]
# Skip if line starts with # (commented)
if re.match(r'^\s*#', line):
continue
match = match_obj.group(1)
# Filter out commented lines from dict content
match_lines = match.split('\n')
match_filtered = '\n'.join(
line for line in match_lines
if not re.match(r'^\s*#', line)
)
# Extract key-value pairs with double quotes
key_value_pairs = re.findall(r"\"([^\"]*)\"\s*:\s*([^,\n]*)", match_filtered)
for key, value in key_value_pairs:
all_nodes.add(key.strip())
# Extract key-value pairs with single quotes
key_value_pairs = re.findall(r"'([^']*)'\s*:\s*([^,\n]*)", match_filtered)
for key, value in key_value_pairs:
all_nodes.add(key.strip())
# Handle .update() pattern (AFTER comment removal)
code_cleaned = re.sub(r'^#.*?$', '', code, flags=re.MULTILINE)
update_pattern = r"_CLASS_MAPPINGS\.update\s*\(\s*{([^}]*)}\s*\)"
update_match = re.search(update_pattern, code_cleaned, re.DOTALL)
if update_match:
update_dict_text = update_match.group(1)
# Extract key-value pairs (double quotes)
update_pairs = re.findall(r'"([^"]*)"\s*:\s*([^,\n]*)', update_dict_text)
for key, value in update_pairs:
all_nodes.add(key.strip())
# Extract key-value pairs (single quotes)
update_pairs_single = re.findall(r"'([^']*)'\s*:\s*([^,\n]*)", update_dict_text)
for key, value in update_pairs_single:
all_nodes.add(key.strip())
# Additional regex patterns (AFTER comment removal)
patterns = [
r'^[^=]*_CLASS_MAPPINGS\["(.*?)"\]',
r'^[^=]*_CLASS_MAPPINGS\[\'(.*?)\'\]',
r'@register_node\("(.+)",\s*\".+"\)',
r'"(\w+)"\s*:\s*{"class":\s*\w+\s*'
]
for pattern in patterns:
keys = re.findall(pattern, code_cleaned)
all_nodes.update(key.strip() for key in keys)
# Extract metadata from this file
metadata = extract_metadata_only(str(py_file))
all_metadata.update(metadata)
except Exception:
# Silently skip files that can't be read
continue
# Save to per-repo cache
_save_per_repo_cache(repo_path, all_nodes, all_metadata)
return (all_nodes, all_metadata)
def _verify_class_exists(node_name: str, code_text: str, file_path: Optional[Path] = None) -> tuple[bool, Optional[str], Optional[int]]:
"""
Verify that a node class exists and has ComfyUI node structure.
Returns: (exists: bool, file_path: str, line_number: int)
A valid ComfyUI node must have:
- Class definition (not commented)
- At least one of: INPUT_TYPES, RETURN_TYPES, FUNCTION method/attribute
"""
try:
with warnings.catch_warnings():
warnings.filterwarnings('ignore', category=SyntaxWarning)
tree = ast.parse(code_text)
except:
return (False, None, None)
for node in ast.walk(tree):
if isinstance(node, ast.ClassDef):
if node.name == node_name or node.name.replace('_', '') == node_name.replace('_', ''):
# Found class definition - check if it has ComfyUI interface
has_input_types = False
has_return_types = False
has_function = False
for item in node.body:
# Check for INPUT_TYPES method
if isinstance(item, ast.FunctionDef) and item.name == 'INPUT_TYPES':
has_input_types = True
# Check for RETURN_TYPES attribute
elif isinstance(item, ast.Assign):
for target in item.targets:
if isinstance(target, ast.Name):
if target.id == 'RETURN_TYPES':
has_return_types = True
elif target.id == 'FUNCTION':
has_function = True
# Check for FUNCTION method
elif isinstance(item, ast.FunctionDef):
has_function = True
# Valid if has any ComfyUI signature
if has_input_types or has_return_types or has_function:
file_str = str(file_path) if file_path else None
return (True, file_str, node.lineno)
return (False, None, None)
def _extract_display_name_mappings(code_text: str) -> Set[str]:
"""
Extract node names from NODE_DISPLAY_NAME_MAPPINGS.
Pattern:
NODE_DISPLAY_NAME_MAPPINGS = {
"node_key": "Display Name",
...
}
Returns:
Set of node keys from NODE_DISPLAY_NAME_MAPPINGS
"""
try:
with warnings.catch_warnings():
warnings.filterwarnings('ignore', category=SyntaxWarning)
tree = ast.parse(code_text)
except:
return set()
nodes = set()
for node in tree.body:
if isinstance(node, ast.Assign):
for target in node.targets:
if isinstance(target, ast.Name) and target.id == 'NODE_DISPLAY_NAME_MAPPINGS':
if isinstance(node.value, ast.Dict):
for key in node.value.keys:
if isinstance(key, ast.Constant) and isinstance(key.value, str):
nodes.add(key.value.strip())
return nodes
def extract_nodes_enhanced(
code_text: str,
file_path: Optional[Path] = None,
visited: Optional[Set[Path]] = None,
verbose: bool = False
) -> Set[str]:
"""
Enhanced node extraction with multi-layer detection system.
Scanner 2.0.11 - Comprehensive detection strategy:
- Phase 1: NODE_CLASS_MAPPINGS dict literal
- Phase 2: Class.NAME attribute access (e.g., FreeChat.NAME)
- Phase 3: Item assignment (NODE_CLASS_MAPPINGS["key"] = value)
- Phase 4: Class existence verification (detects active classes even if registration commented)
- Phase 5: NODE_DISPLAY_NAME_MAPPINGS cross-reference
- Phase 6: Empty dict detection (UI-only extensions, logging only)
Fixed Bugs:
- Scanner 2.0.9: Fallback cascade prevented Phase 3 execution
- Scanner 2.0.10: Missed active classes with commented registrations (15 false negatives)
Args:
code_text: Python source code
file_path: Path to file (for logging and caching)
visited: Visited paths (for circular import prevention)
verbose: If True, print UI-only extension detection messages
Returns:
Set of node names (union of all detected patterns)
"""
# Check file-based cache if file_path provided
if file_path is not None:
try:
file_path_obj = Path(file_path) if not isinstance(file_path, Path) else file_path
if file_path_obj.exists():
current_mtime = file_path_obj.stat().st_mtime
# Check if we have cached result with matching mtime and scanner version
if file_path_obj in _file_mtime_cache:
cached_mtime = _file_mtime_cache[file_path_obj]
cache_key = (str(file_path_obj), cached_mtime, SCANNER_VERSION)
if current_mtime == cached_mtime and cache_key in _extract_nodes_enhanced_cache:
return _extract_nodes_enhanced_cache[cache_key].copy()
except:
pass # Ignore cache errors, proceed with normal execution
# Suppress warnings from AST parsing
with warnings.catch_warnings():
warnings.filterwarnings('ignore', category=SyntaxWarning)
warnings.filterwarnings('ignore', category=DeprecationWarning)
# Phase 1: Original extract_nodes() - dict literal
phase1_nodes = extract_nodes(code_text)
# Phase 2: Class.NAME pattern
if visited is None:
visited = set()
phase2_nodes = _fallback_classname_resolver(code_text, file_path)
# Phase 3: Item assignment pattern
phase3_nodes = _fallback_item_assignment(code_text)
# Phase 4: NODE_DISPLAY_NAME_MAPPINGS cross-reference (NEW in 2.0.11)
# This catches nodes that are in display names but not in NODE_CLASS_MAPPINGS
phase4_nodes = _extract_display_name_mappings(code_text)
# Phase 5: Class existence verification ONLY for display name candidates (NEW in 2.0.11)
# This phase is CONSERVATIVE - only verify classes that appear in display names
# This catches the specific Scanner 2.0.10 bug pattern:
# - NODE_CLASS_MAPPINGS registration is commented
# - NODE_DISPLAY_NAME_MAPPINGS still has the entry
# - Class implementation exists
# Example: Bjornulf_ollamaLoader in Bjornulf_custom_nodes
phase5_nodes = set()
for node_name in phase4_nodes:
# Only check classes that appear in display names but not in registrations
if node_name not in (phase1_nodes | phase2_nodes | phase3_nodes):
exists, _, _ = _verify_class_exists(node_name, code_text, file_path)
if exists:
phase5_nodes.add(node_name)
# Phase 6: Dict comprehension pattern (NEW in 2.0.12)
# Detects: NODE_CLASS_MAPPINGS = {cls.__name__: cls for cls in to_export}
# Example: TobiasGlaubach/ComfyUI-TG_PyCode
phase6_nodes = _fallback_dict_comprehension(code_text, file_path)
# Phase 7: Import-based class names for dict comprehension (NEW in 2.0.12)
# Detects imported classes that are added to export lists
phase7_nodes = _fallback_import_class_names(code_text, file_path)
# Union all results (FIX: Scanner 2.0.9 bug + Scanner 2.0.10 bug + Scanner 2.0.12 dict comp)
# 2.0.9: Used early return which missed Phase 3 nodes
# 2.0.10: Only checked registrations, missed classes referenced in display names
# 2.0.12: Added dict comprehension and import-based class detection
all_nodes = phase1_nodes | phase2_nodes | phase3_nodes | phase4_nodes | phase5_nodes | phase6_nodes | phase7_nodes
# Phase 8: Empty dict detector (logging only, doesn't add nodes)
if not all_nodes:
_fallback_empty_dict_detector(code_text, file_path, verbose)
# Cache the result
if file_path is not None:
try:
file_path_obj = Path(file_path) if not isinstance(file_path, Path) else file_path
if file_path_obj.exists():
current_mtime = file_path_obj.stat().st_mtime
cache_key = (str(file_path_obj), current_mtime, SCANNER_VERSION)
_extract_nodes_enhanced_cache[cache_key] = all_nodes
_file_mtime_cache[file_path_obj] = current_mtime
except:
pass
return all_nodes
def _fallback_classname_resolver(code_text: str, file_path: Optional[Path]) -> Set[str]:
"""
Detect Class.NAME pattern in NODE_CLASS_MAPPINGS.
Pattern:
NODE_CLASS_MAPPINGS = {
FreeChat.NAME: FreeChat,
PaidChat.NAME: PaidChat
}
"""
try:
with warnings.catch_warnings():
warnings.filterwarnings('ignore', category=SyntaxWarning)
parsed = ast.parse(code_text)
except:
return set()
nodes = set()
for node in parsed.body:
if isinstance(node, ast.Assign):
for target in node.targets:
if isinstance(target, ast.Name) and target.id == 'NODE_CLASS_MAPPINGS':
if isinstance(node.value, ast.Dict):
for key in node.value.keys:
# Detect Class.NAME pattern
if isinstance(key, ast.Attribute):
if isinstance(key.value, ast.Name):
# Use class name as node name
nodes.add(key.value.id)
# Also handle literal strings
elif isinstance(key, ast.Constant) and isinstance(key.value, str):
nodes.add(key.value.strip())
return nodes
def _fallback_item_assignment(code_text: str) -> Set[str]:
"""
Detect item assignment pattern.
Pattern:
NODE_CLASS_MAPPINGS = {}
NODE_CLASS_MAPPINGS["MyNode"] = MyNode
"""
try:
with warnings.catch_warnings():
warnings.filterwarnings('ignore', category=SyntaxWarning)
parsed = ast.parse(code_text)
except:
return set()
nodes = set()
for node in ast.walk(parsed):
if isinstance(node, ast.Assign):
for target in node.targets:
if isinstance(target, ast.Subscript):
if (isinstance(target.value, ast.Name) and
target.value.id in ['NODE_CLASS_MAPPINGS', 'NODE_CONFIG']):
# Extract key
if isinstance(target.slice, ast.Constant):
if isinstance(target.slice.value, str):
nodes.add(target.slice.value)
return nodes
def _fallback_dict_comprehension(code_text: str, file_path: Optional[Path] = None) -> Set[str]:
"""
Detect dict comprehension pattern with __name__ attribute access.
Pattern:
NODE_CLASS_MAPPINGS = {cls.__name__: cls for cls in to_export}
NODE_CLASS_MAPPINGS = {c.__name__: c for c in [ClassA, ClassB]}
This function detects dict comprehension assignments to NODE_CLASS_MAPPINGS
and extracts class names from the iterable (list literal or variable reference).
Returns:
Set of class names extracted from the dict comprehension
"""
try:
with warnings.catch_warnings():
warnings.filterwarnings('ignore', category=SyntaxWarning)
parsed = ast.parse(code_text)
except:
return set()
nodes = set()
export_lists = {} # Track list variables and their contents
# First pass: collect list assignments (to_export = [...], exports = [...])
for node in ast.walk(parsed):
if isinstance(node, ast.Assign):
for target in node.targets:
if isinstance(target, ast.Name):
var_name = target.id
# Check for list literal
if isinstance(node.value, ast.List):
class_names = set()
for elt in node.value.elts:
if isinstance(elt, ast.Name):
class_names.add(elt.id)
export_lists[var_name] = class_names
# Handle augmented assignment: to_export += [...]
elif isinstance(node, ast.AugAssign):
if isinstance(node.target, ast.Name) and isinstance(node.op, ast.Add):
var_name = node.target.id
if isinstance(node.value, ast.List):
class_names = set()
for elt in node.value.elts:
if isinstance(elt, ast.Name):
class_names.add(elt.id)
if var_name in export_lists:
export_lists[var_name].update(class_names)
else:
export_lists[var_name] = class_names
# Second pass: find NODE_CLASS_MAPPINGS dict comprehension
for node in ast.walk(parsed):
if isinstance(node, ast.Assign):
for target in node.targets:
if isinstance(target, ast.Name) and target.id in ['NODE_CLASS_MAPPINGS', 'NODE_CONFIG']:
# Check for dict comprehension
if isinstance(node.value, ast.DictComp):
dictcomp = node.value
# Check if key is cls.__name__ pattern
key = dictcomp.key
if isinstance(key, ast.Attribute) and key.attr == '__name__':
# Get the iterable from the first generator
for generator in dictcomp.generators:
iter_node = generator.iter
# Case 1: Inline list [ClassA, ClassB, ...]
if isinstance(iter_node, ast.List):
for elt in iter_node.elts:
if isinstance(elt, ast.Name):
nodes.add(elt.id)
# Case 2: Variable reference (to_export, exports, etc.)
elif isinstance(iter_node, ast.Name):
var_name = iter_node.id
if var_name in export_lists:
nodes.update(export_lists[var_name])
return nodes
def _fallback_import_class_names(code_text: str, file_path: Optional[Path] = None) -> Set[str]:
"""
Extract class names from imports that are added to export lists.
Pattern:
from .module import ClassA, ClassB
to_export = [ClassA, ClassB]
NODE_CLASS_MAPPINGS = {cls.__name__: cls for cls in to_export}
This is a complementary fallback that works with _fallback_dict_comprehension
to resolve import-based node registrations.
Returns:
Set of imported class names that appear in export-like contexts
"""
try:
with warnings.catch_warnings():
warnings.filterwarnings('ignore', category=SyntaxWarning)
parsed = ast.parse(code_text)
except:
return set()
# Collect imported names
imported_names = set()
for node in ast.walk(parsed):
if isinstance(node, ast.ImportFrom):
for alias in node.names:
name = alias.asname if alias.asname else alias.name
imported_names.add(name)
# Check if these names appear in list assignments that feed into NODE_CLASS_MAPPINGS
export_candidates = set()
has_dict_comp_mapping = False
for node in ast.walk(parsed):
# Check for dict comprehension NODE_CLASS_MAPPINGS
if isinstance(node, ast.Assign):
for target in node.targets:
if isinstance(target, ast.Name) and target.id == 'NODE_CLASS_MAPPINGS':
if isinstance(node.value, ast.DictComp):
has_dict_comp_mapping = True
# Collect list contents
if isinstance(node, ast.Assign):
if isinstance(node.value, ast.List):
for elt in node.value.elts:
if isinstance(elt, ast.Name) and elt.id in imported_names:
export_candidates.add(elt.id)
# Handle augmented assignment
elif isinstance(node, ast.AugAssign):
if isinstance(node.value, ast.List):
for elt in node.value.elts:
if isinstance(elt, ast.Name) and elt.id in imported_names:
export_candidates.add(elt.id)
# Only return if there's a dict comprehension mapping
if has_dict_comp_mapping:
return export_candidates
return set()
def _extract_repo_name(file_path: Path) -> str:
"""
Extract repository name from file path.
Path structure: /home/rho/.tmp/analysis/temp/{author}_{reponame}/{path/to/file.py}
Returns: {author}_{reponame} or filename if extraction fails
"""
try:
parts = file_path.parts
# Find 'temp' directory in path
if 'temp' in parts:
temp_idx = parts.index('temp')
if temp_idx + 1 < len(parts):
# Next part after 'temp' is the repo directory
return parts[temp_idx + 1]
except (ValueError, IndexError):
pass
# Fallback to filename if extraction fails
return file_path.name if hasattr(file_path, 'name') else str(file_path)
def _fallback_empty_dict_detector(code_text: str, file_path: Optional[Path], verbose: bool = False) -> None:
"""
Detect empty NODE_CLASS_MAPPINGS (UI-only extensions).
Logs for documentation purposes only (when verbose=True).
Args:
code_text: Python source code to analyze
file_path: Path to the file being analyzed
verbose: If True, print detection messages
"""
empty_patterns = [
'NODE_CLASS_MAPPINGS = {}',
'NODE_CLASS_MAPPINGS={}',
]
code_normalized = code_text.replace(' ', '').replace('\n', '')
for pattern in empty_patterns:
pattern_normalized = pattern.replace(' ', '')
if pattern_normalized in code_normalized:
if file_path and verbose:
repo_name = _extract_repo_name(file_path)
print(f"Info: UI-only extension (empty NODE_CLASS_MAPPINGS): {repo_name}")
return
def has_comfy_node_base(class_node):
"""Check if class inherits from io.ComfyNode or ComfyNode"""
@ -229,6 +941,25 @@ def extract_v3_nodes(code_text):
# scan
def extract_metadata_only(filename):
"""Extract only metadata (@author, @title, etc) without node scanning"""
try:
with open(filename, encoding='utf-8', errors='ignore') as file:
code = file.read()
metadata = {}
lines = code.strip().split('\n')
for line in lines:
if line.startswith('@'):
if line.startswith("@author:") or line.startswith("@title:") or line.startswith("@nickname:") or line.startswith("@description:"):
key, value = line[1:].strip().split(':', 1)
metadata[key.strip()] = value.strip()
return metadata
except:
return {}
def scan_in_file(filename, is_builtin=False):
global builtin_nodes
@ -242,8 +973,8 @@ def scan_in_file(filename, is_builtin=False):
nodes = set()
class_dict = {}
# V1 nodes detection
nodes |= extract_nodes(code)
# V1 nodes detection (enhanced with fallback patterns)
nodes |= extract_nodes_enhanced(code, file_path=Path(filename), visited=set())
# V3 nodes detection
nodes |= extract_v3_nodes(code)
@ -620,13 +1351,14 @@ def update_custom_nodes(scan_only_mode=False, url_list_file=None):
return node_info
def gen_json(node_info, scan_only_mode=False):
def gen_json(node_info, scan_only_mode=False, force_rescan=False):
"""
Generate extension-node-map.json from scanned node information
Args:
node_info (dict): Repository metadata mapping
scan_only_mode (bool): If True, exclude metadata from output
force_rescan (bool): If True, ignore cache and force rescan all nodes
"""
# scan from .py file
node_files, node_dirs = get_nodes(temp_dir)
@ -642,11 +1374,15 @@ def gen_json(node_info, scan_only_mode=False):
py_files = get_py_file_paths(dirname)
metadata = {}
# Use per-repo cache for node AND metadata extraction
try:
nodes, metadata = extract_nodes_from_repo(Path(dirname), verbose=False, force_rescan=force_rescan)
except:
# Fallback to file-by-file scanning if extract_nodes_from_repo fails
nodes = set()
for py in py_files:
nodes_in_file, metadata_in_file = scan_in_file(py, dirname == "ComfyUI")
nodes.update(nodes_in_file)
# Include metadata from .py files in both modes
metadata.update(metadata_in_file)
dirname = os.path.basename(dirname)
@ -810,7 +1546,10 @@ if __name__ == "__main__":
print("\n# Generating 'extension-node-map.json'...\n")
# Generate extension-node-map.json
gen_json(updated_node_info, scan_only_mode)
force_rescan = args.force_rescan if hasattr(args, 'force_rescan') else False
if force_rescan:
print("⚠️ Force rescan enabled - ignoring all cached results\n")
gen_json(updated_node_info, scan_only_mode, force_rescan)
print("\n✅ DONE.\n")