Compare commits

..

110 Commits

Author SHA1 Message Date
Dr.Lt.Data
1246538bbb fixed: Issue where installation status was not properly recognized when the nodepack ID registered in the registry was not normalized.
- ex) `ComfyUI-Crystools`

https://github.com/Comfy-Org/ComfyUI-Manager/issues/1834#issuecomment-2937370214
2025-06-17 00:31:51 +09:00
Dr.Lt.Data
80518abf9d update DB 2025-06-16 22:42:41 +09:00
Leon Wong
fc1ae2a18e added comfyui-leon-nodes to ustom-node-list.json (#1937) 2025-06-16 22:17:45 +09:00
Yuan-Man
3fd8d2049c Add ComfyUI-Hunyuan3D-2.1 (#1936) 2025-06-16 22:16:50 +09:00
Dr.Lt.Data
35a6bcf20c update DB 2025-06-16 12:52:05 +09:00
Dr.Lt.Data
0d75fc331e update DB 2025-06-16 07:28:55 +09:00
Dr.Lt.Data
0a23e793e3 update DB 2025-06-15 15:43:09 +09:00
Dr.Lt.Data
2c1c03e063 update DB 2025-06-15 14:27:27 +09:00
Çağlayan Karagözler
64059d2949 Added ComfyUI-YouTubeUploader to custom nodes json (#1933)
* Update custom-node-list.json

Added ComfyUI-YouTubeUploader

* Update custom-node-list.json

* Update custom-node-list.json

Added proper link

* Update custom-node-list.json

---------

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

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

* Update custom-node-list.json

* Update custom-node-list.json

---------

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

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

* Update custom-node-list.json

---------

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

* Update custom-node-list.json

---------

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

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

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

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

* Update custom-node-list.json

---------

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

Add Comfy-Topaz-Photo

* Update custom-node-list.json

* Update custom-node-list.json

* Update custom-node-list.json

Add Comfy-Topaz-Photo

---------

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

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

Changes author from aimingfail → Pigidiy

Adds missing version field: v1.0.0

Updates id from img2halftone → likeSpiderMP3

The previous metadata was mistakenly duplicated from another node.

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

* Update custom-node-list.json

---------

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

* Update custom-node-list.json

---------

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

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

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-06-03 10:39:15 +09:00
Dr.Lt.Data
bbb6005634 fixed: scanner
update DB
2025-06-03 10:36:48 +09:00
vivi-gomez
8dbd996558 Add ComfyUI Fix Node Translate custom node (#1892)
* Update custom-node-list.json

* Update custom-node-list.json

* Update custom-node-list.json

---------

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

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

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

---------

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

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

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

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

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

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

* [docs] Update js/README.md with corrected descriptions
2025-05-21 21:44:48 +09:00
Christian Byrne
53244b794f [docs] Add README for glob directory (#1852) 2025-05-21 21:44:24 +09:00
44 changed files with 17720 additions and 9972 deletions

View File

@@ -8,7 +8,7 @@
* V3.16: Support for `uv` has been added. Set `use_uv` in `config.ini`. * V3.16: Support for `uv` has been added. Set `use_uv` in `config.ini`.
* V3.10: `double-click feature` is removed * V3.10: `double-click feature` is removed
* This feature has been moved to https://github.com/ltdrdata/comfyui-connection-helper * This feature has been moved to https://github.com/ltdrdata/comfyui-connection-helper
* V3.3.2: Overhauled. Officially supports [https://comfyregistry.org/](https://comfyregistry.org/). * V3.3.2: Overhauled. Officially supports [https://registry.comfy.org/](https://registry.comfy.org/).
* You can see whole nodes info on [ComfyUI Nodes Info](https://ltdrdata.github.io/) page. * You can see whole nodes info on [ComfyUI Nodes Info](https://ltdrdata.github.io/) page.
## Installation ## Installation

View File

File diff suppressed because it is too large Load Diff

41
docs/README.md Normal file
View File

@@ -0,0 +1,41 @@
# ComfyUI-Manager: Documentation
This directory contains documentation for the ComfyUI-Manager, providing guides and tutorials for users in multiple languages.
## Directory Structure
The documentation is organized into language-specific directories:
- **en/**: English documentation
- **ko/**: Korean documentation
## Core Documentation Files
### Command-Line Interface
- **cm-cli.md**: Documentation for the ComfyUI-Manager Command Line Interface (CLI), which allows using manager functionality without the UI.
### Advanced Features
- **use_aria2.md**: Guide for using the aria2 download accelerator with ComfyUI-Manager for faster model downloads.
## Documentation Standards
The documentation follows these standards:
1. **Markdown Format**: All documentation is written in Markdown for easy rendering on GitHub and other platforms
2. **Language-specific Directories**: Content is separated by language to facilitate localization
3. **Feature-focused Documentation**: Each major feature has its own documentation file
4. **Updated with Releases**: Documentation is kept in sync with software releases
## Contributing to Documentation
When contributing new documentation:
1. Place files in the appropriate language directory
2. Use clear, concise language appropriate for the target audience
3. Include examples where helpful
4. Consider adding screenshots or diagrams for complex features
5. Maintain consistent formatting with existing documentation
This documentation directory will continue to grow to support the expanding feature set of ComfyUI-Manager.

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

53
glob/README.md Normal file
View File

@@ -0,0 +1,53 @@
# ComfyUI-Manager: Core Backend (glob)
This directory contains the Python backend modules that power ComfyUI-Manager, handling the core functionality of node management, downloading, security, and server operations.
## Core Modules
- **manager_core.py**: The central implementation of management functions, handling configuration, installation, updates, and node management.
- **manager_server.py**: Implements server functionality and API endpoints for the web interface to interact with the backend.
- **manager_downloader.py**: Handles downloading operations for models, extensions, and other resources.
- **manager_util.py**: Provides utility functions used throughout the system.
## Specialized Modules
- **cm_global.py**: Maintains global variables and state management across the system.
- **cnr_utils.py**: Helper utilities for interacting with the custom node registry (CNR).
- **git_utils.py**: Git-specific utilities for repository operations.
- **node_package.py**: Handles the packaging and installation of node extensions.
- **security_check.py**: Implements the multi-level security system for installation safety.
- **share_3rdparty.py**: Manages integration with third-party sharing platforms.
## Architecture
The backend follows a modular design pattern with clear separation of concerns:
1. **Core Layer**: Manager modules provide the primary API and business logic
2. **Utility Layer**: Helper modules provide specialized functionality
3. **Integration Layer**: Modules that connect to external systems
## Security Model
The system implements a comprehensive security framework with multiple levels:
- **Block**: Highest security - blocks most remote operations
- **High**: Allows only specific trusted operations
- **Middle**: Standard security for most users
- **Normal-**: More permissive for advanced users
- **Weak**: Lowest security for development environments
## Implementation Details
- The backend is designed to work seamlessly with ComfyUI
- Asynchronous task queuing is implemented for background operations
- The system supports multiple installation modes
- Error handling and risk assessment are integrated throughout the codebase
## API Integration
The backend exposes a REST API via `manager_server.py` that enables:
- Custom node management (install, update, disable, remove)
- Model downloading and organization
- System configuration
- Snapshot management
- Workflow component handling

View File

@@ -46,6 +46,8 @@ def git_url(fullpath):
for k, v in config.items(): for k, v in config.items():
if k.startswith('remote ') and 'url' in v: if k.startswith('remote ') and 'url' in v:
if 'Comfy-Org/ComfyUI-Manager' in v['url']:
return "https://github.com/ltdrdata/ComfyUI-Manager"
return v['url'] return v['url']
return None return None

View File

@@ -43,7 +43,7 @@ import manager_downloader
from node_package import InstalledNodePackage from node_package import InstalledNodePackage
version_code = [3, 32, 3] version_code = [3, 33]
version_str = f"V{version_code[0]}.{version_code[1]}" + (f'.{version_code[2]}' if len(version_code) > 2 else '') version_str = f"V{version_code[0]}.{version_code[1]}" + (f'.{version_code[2]}' if len(version_code) > 2 else '')
@@ -400,18 +400,46 @@ class ManagedResult:
return self return self
class NormalizedKeyDict(dict):
def _normalize_key(self, key):
if isinstance(key, str):
return key.strip().lower()
return key
def __setitem__(self, key, value):
super().__setitem__(self._normalize_key(key), value)
def __getitem__(self, key):
return super().__getitem__(self._normalize_key(key))
def __delitem__(self, key):
return super().__delitem__(self._normalize_key(key))
def __contains__(self, key):
return super().__contains__(self._normalize_key(key))
def get(self, key, default=None):
return super().get(self._normalize_key(key), default)
def setdefault(self, key, default=None):
return super().setdefault(self._normalize_key(key), default)
def pop(self, key, default=None):
return super().pop(self._normalize_key(key), default)
class UnifiedManager: class UnifiedManager:
def __init__(self): def __init__(self):
self.installed_node_packages: dict[str, InstalledNodePackage] = {} self.installed_node_packages: dict[str, InstalledNodePackage] = {}
self.cnr_inactive_nodes = {} # node_id -> node_version -> fullpath self.cnr_inactive_nodes = NormalizedKeyDict() # node_id -> node_version -> fullpath
self.nightly_inactive_nodes = {} # node_id -> fullpath self.nightly_inactive_nodes = NormalizedKeyDict() # node_id -> fullpath
self.unknown_inactive_nodes = {} # node_id -> repo url * fullpath self.unknown_inactive_nodes = {} # node_id -> repo url * fullpath
self.active_nodes = {} # node_id -> node_version * fullpath self.active_nodes = NormalizedKeyDict() # node_id -> node_version * fullpath
self.unknown_active_nodes = {} # node_id -> repo url * fullpath self.unknown_active_nodes = {} # node_id -> repo url * fullpath
self.cnr_map = {} # node_id -> cnr info self.cnr_map = NormalizedKeyDict() # node_id -> cnr info
self.repo_cnr_map = {} # repo_url -> cnr info self.repo_cnr_map = {} # repo_url -> cnr info
self.custom_node_map_cache = {} # (channel, mode) -> augmented custom node list json self.custom_node_map_cache = {} # (channel, mode) -> augmented custom node list json
self.processed_install = set() self.processed_install = set()
def get_module_name(self, x): def get_module_name(self, x):
@@ -2876,7 +2904,7 @@ async def get_unified_total_nodes(channel, mode, regsitry_cache_mode='cache'):
if cnr_id is not None: if cnr_id is not None:
# cnr or nightly version # cnr or nightly version
cnr_ids.remove(cnr_id) cnr_ids.discard(cnr_id)
updatable = False updatable = False
cnr = unified_manager.cnr_map[cnr_id] cnr = unified_manager.cnr_map[cnr_id]

View File

@@ -181,7 +181,10 @@ def set_preview_method(method):
core.get_config()['preview_method'] = method core.get_config()['preview_method'] = method
set_preview_method(core.get_config()['preview_method']) if args.preview_method == latent_preview.LatentPreviewMethod.NoPreviews:
set_preview_method(core.get_config()['preview_method'])
else:
logging.warning("[ComfyUI-Manager] Since --preview-method is set, ComfyUI-Manager's preview method feature will be ignored.")
def set_component_policy(mode): def set_component_policy(mode):
@@ -437,7 +440,10 @@ async def task_worker():
if res.ver == 'unknown': if res.ver == 'unknown':
url = core.unified_manager.unknown_active_nodes[node_name][0] url = core.unified_manager.unknown_active_nodes[node_name][0]
title = os.path.basename(url) try:
title = os.path.basename(url)
except Exception:
title = node_name
else: else:
url = core.unified_manager.cnr_map[node_name].get('repository') url = core.unified_manager.cnr_map[node_name].get('repository')
title = core.unified_manager.cnr_map[node_name]['name'] title = core.unified_manager.cnr_map[node_name]['name']

View File

@@ -476,7 +476,7 @@ class PIPFixer:
normalized_name = parsed['package'].lower().replace('-', '_') normalized_name = parsed['package'].lower().replace('-', '_')
if normalized_name in new_pip_versions: if normalized_name in new_pip_versions:
if 'version' in parsed and 'operator' in parsed: if 'version' in parsed and 'operator' in parsed:
cur = StrictVersion(new_pip_versions[parsed['package']]) cur = StrictVersion(new_pip_versions[normalized_name])
dest = parsed['version'] dest = parsed['version']
op = parsed['operator'] op = parsed['operator']
if cur == dest: if cur == dest:

50
js/README.md Normal file
View File

@@ -0,0 +1,50 @@
# ComfyUI-Manager: Frontend (js)
This directory contains the JavaScript frontend implementation for ComfyUI-Manager, providing the user interface components that interact with the backend API.
## Core Components
- **comfyui-manager.js**: Main entry point that initializes the manager UI and integrates with ComfyUI.
- **custom-nodes-manager.js**: Implements the UI for browsing, installing, and managing custom nodes.
- **model-manager.js**: Handles the model management interface for downloading and organizing AI models.
- **components-manager.js**: Manages reusable workflow components system.
- **snapshot.js**: Implements the snapshot system for backing up and restoring installations.
## Sharing Components
- **comfyui-share-common.js**: Base functionality for workflow sharing features.
- **comfyui-share-copus.js**: Integration with the ComfyUI Opus sharing platform.
- **comfyui-share-openart.js**: Integration with the OpenArt sharing platform.
- **comfyui-share-youml.js**: Integration with the YouML sharing platform.
## Utility Components
- **cm-api.js**: Client-side API wrapper for communication with the backend.
- **common.js**: Shared utilities and helper functions used across the frontend.
- **node_fixer.js**: Utilities for fixing disconnected links and repairing malformed nodes by recreating them while preserving connections.
- **popover-helper.js**: UI component for popup tooltips and contextual information.
- **turbogrid.esm.js**: Grid component library - https://github.com/cenfun/turbogrid
- **workflow-metadata.js**: Handles workflow metadata parsing, validation and cross-repository compatibility including versioning, dependencies tracking, and resource management.
## Architecture
The frontend follows a modular component-based architecture:
1. **Integration Layer**: Connects with ComfyUI's existing UI system
2. **Manager Components**: Individual functional UI components (node manager, model manager, etc.)
3. **Sharing Components**: Platform-specific sharing implementations
4. **Utility Layer**: Reusable UI components and helpers
## Implementation Details
- The frontend integrates directly with ComfyUI's UI system through `app.js`
- Dialog-based UI for most manager functions to avoid cluttering the main interface
- Asynchronous API calls to handle backend operations without blocking the UI
## Styling
CSS files are included for specific components:
- **custom-nodes-manager.css**: Styling for the node management UI
- **model-manager.css**: Styling for the model management UI
This frontend implementation provides a comprehensive yet user-friendly interface for managing the ComfyUI ecosystem.

View File

@@ -153,6 +153,7 @@ app.registerExtension({
app.canvas.graph.add(new_node, false); app.canvas.graph.add(new_node, false);
node_info_copy(this, new_node, true); node_info_copy(this, new_node, true);
app.canvas.graph.remove(this); app.canvas.graph.remove(this);
requestAnimationFrame(() => app.canvas.setDirty(true, true))
}, },
}); });
}); });

95
node_db/README.md Normal file
View File

@@ -0,0 +1,95 @@
# ComfyUI-Manager: Node Database (node_db)
This directory contains the JSON database files that power ComfyUI-Manager's legacy node registry system. While the manager is gradually transitioning to the online Custom Node Registry (CNR), these local JSON files continue to provide important metadata about custom nodes, models, and their integrations.
## Directory Structure
The node_db directory is organized into several subdirectories, each serving a specific purpose:
- **dev/**: Development channel files with latest additions and experimental nodes
- **legacy/**: Historical/legacy nodes that may require special handling
- **new/**: New nodes that have passed initial verification but are still being evaluated
- **forked/**: Forks of existing nodes with modifications
- **tutorial/**: Example and tutorial nodes designed for learning purposes
## Core Database Files
Each subdirectory contains a standard set of JSON files:
- **custom-node-list.json**: Primary database of custom nodes with metadata
- **extension-node-map.json**: Maps between extensions and individual nodes they provide
- **model-list.json**: Catalog of models that can be downloaded through the manager
- **alter-list.json**: Alternative implementations of nodes for compatibility or functionality
- **github-stats.json**: GitHub repository statistics for node popularity metrics
## Database Schema
### custom-node-list.json
```json
{
"custom_nodes": [
{
"title": "Node display name",
"name": "Repository name",
"reference": "Original repository if forked",
"files": ["GitHub URL or other source location"],
"install_type": "git",
"description": "Description of the node's functionality",
"pip": ["optional pip dependencies"],
"js": ["optional JavaScript files"],
"tags": ["categorization tags"]
}
]
}
```
### extension-node-map.json
```json
{
"extension-id": [
["list", "of", "node", "classes"],
{
"author": "Author name",
"description": "Extension description",
"nodename_pattern": "Optional regex pattern for node name matching"
}
]
}
```
## Transition to Custom Node Registry (CNR)
This local database system is being progressively replaced by the online Custom Node Registry (CNR), which provides:
- Real-time updates without manual JSON maintenance
- Improved versioning support
- Better security validation
- Enhanced metadata
The Manager supports both systems simultaneously during the transition period.
## Implementation Details
- The database follows a channel-based architecture for different sources
- Multiple database modes are supported: Channel, Local, and Remote
- The system supports differential updates to minimize bandwidth usage
- Security levels are enforced for different node installations based on source
## Usage in the Application
The Manager's backend uses these database files to:
1. Provide browsable lists of available nodes and models
2. Resolve dependencies for installation
3. Track updates and new versions
4. Map node classes to their source repositories
5. Assess risk levels for installation security
## Maintenance Scripts
Each subdirectory contains a `scan.sh` script that assists with:
- Scanning repositories for new nodes
- Updating metadata
- Validating database integrity
- Generating proper JSON structures
This database system enables a flexible, secure, and comprehensive management system for the ComfyUI ecosystem while the transition to CNR continues.

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,158 @@
{ {
"custom_nodes": [ "custom_nodes": [
{ {
"author": "#NOTICE_1.13", "author": "theUpsider",
"title": "NOTICE: This channel is not the default channel.", "title": "ComfyUI-Logic [DEPRECATED]",
"reference": "https://github.com/ltdrdata/ComfyUI-Manager", "id": "comfy-logic",
"files": [], "reference": "https://github.com/theUpsider/ComfyUI-Logic",
"files": [
"https://github.com/theUpsider/ComfyUI-Logic"
],
"install_type": "git-clone", "install_type": "git-clone",
"description": "If you see this message, your ComfyUI-Manager is outdated.\nLegacy channel provides only the list of the deprecated nodes. If you want to find the complete node list, please go to the Default channel." "description": "An extension to ComfyUI that introduces logic nodes and conditional rendering capabilities."
},
{
"author": "Malloc-pix",
"title": "comfyui_qwen2.4_vl_node [REMOVED]",
"reference": "https://github.com/Malloc-pix/comfyui_qwen2.4_vl_node",
"files": [
"https://github.com/Malloc-pix/comfyui_qwen2.4_vl_node"
],
"install_type": "git-clone",
"description": "NODES: CogVLM2 Captioner, CLIP Dynamic Text Encode(cy)"
},
{
"author": "inyourdreams-studio",
"title": "ComfyUI-RBLM [REMOVED]",
"reference": "https://github.com/inyourdreams-studio/comfyui-rblm",
"files": [
"https://github.com/inyourdreams-studio/comfyui-rblm"
],
"install_type": "git-clone",
"description": "A custom node pack for ComfyUI that provides text manipulation nodes."
},
{
"author": "dream-computing",
"title": "SyntaxNodes - Image Processing Effects for ComfyUI [REMOVED]",
"reference": "https://github.com/dream-computing/syntax-nodes",
"files": [
"https://github.com/dream-computing/syntax-nodes"
],
"install_type": "git-clone",
"description": "A collection of custom nodes for ComfyUI designed to apply various image processing effects, stylizations, and analyses."
},
{
"author": "UD1sto",
"title": "plugin-utils-nodes [DEPRECATED]",
"reference": "https://github.com/its-DeFine/plugin-utils-nodes",
"files": [
"https://github.com/its-DeFine/plugin-utils-nodes"
],
"install_type": "git-clone",
"description": "NODES: Compare Images (SimHash), Image Selector, Temporal Consistency, Update Image Reference, Frame Blend."
},
{
"author": "hanyingcho",
"title": "ComfyUI LLM Promp [REMOVED]",
"reference": "https://github.com/hanyingcho/comfyui-llmprompt",
"files": [
"https://github.com/hanyingcho/comfyui-llmprompt"
],
"install_type": "git-clone",
"description": "NODES: Load llm, Generate Text with LLM, Inference Qwen2VL, Inference Qwen2"
},
{
"author": "WASasquatch",
"title": "WAS Node Suite [DEPRECATED]",
"id": "was",
"reference": "https://github.com/WASasquatch/was-node-suite-comfyui",
"pip": ["numba"],
"files": [
"https://github.com/WASasquatch/was-node-suite-comfyui"
],
"install_type": "git-clone",
"description": "A node suite for ComfyUI with many new nodes, such as image processing, text processing, and more."
},
{
"author": "TOM1063",
"title": "ComfyUI-SamuraiTools [REMOVED]",
"reference": "https://github.com/TOM1063/ComfyUI-SamuraiTools",
"files": [
"https://github.com/TOM1063/ComfyUI-SamuraiTools"
],
"install_type": "git-clone",
"description": "ComfyUI custom node for switching integer values based on boolean conditions"
},
{
"author": "whitemoney293",
"title": "ComfyUI-MediaUtilities [REMOVED]",
"reference": "https://github.com/ThanaritKanjanametawatAU/ComfyUI-MediaUtilities",
"files": [
"https://github.com/ThanaritKanjanametawatAU/ComfyUI-MediaUtilities"
],
"install_type": "git-clone",
"description": "Custom nodes for loading and previewing media from URLs in ComfyUI."
},
{
"author": "pureexe",
"title": "DiffusionLight-ComfyUI [REMOVED]",
"reference": "https://github.com/pureexe/DiffusionLight-ComfyUI",
"files": [
"https://github.com/pureexe/DiffusionLight-ComfyUI"
],
"install_type": "git-clone",
"description": "DiffusionLight (Turbo) implemented in ComfyUI"
},
{
"author": "gondar-software",
"title": "comfyui-custom-padding [REMOVED]",
"reference": "https://github.com/gondar-software/comfyui-custom-padding",
"files": [
"https://github.com/gondar-software/comfyui-custom-padding"
],
"install_type": "git-clone",
"description": "NODES: Adaptive image padding, Adaptive image unpadding"
},
{
"author": "Charonartist",
"title": "ComfyUI-EagleExporter [REMOVED]",
"reference": "https://github.com/Charonartist/ComfyUI-EagleExporter",
"files": [
"https://github.com/Charonartist/ComfyUI-EagleExporter"
],
"install_type": "git-clone",
"description": "This is an extension that automatically saves video files generated with ComfyUI's 'video combine' extension to the Eagle library."
},
{
"author": "pomePLaszlo-collablyu",
"title": "comfyui_ejam [REMOVED]",
"reference": "https://github.com/PLaszlo-collab/comfyui_ejam",
"files": [
"https://github.com/PLaszlo-collab/comfyui_ejam"
],
"install_type": "git-clone",
"description": "Ejam nodes for comfyui"
},
{
"author": "jonnydolake",
"title": "ComfyUI-AIR-Nodes [REMOVED]",
"reference": "https://github.com/jonnydolake/ComfyUI-AIR-Nodes",
"files": [
"https://github.com/jonnydolake/ComfyUI-AIR-Nodes"
],
"install_type": "git-clone",
"description": "NODES: String List To Prompt Schedule, Force Minimum Batch Size, Target Location (Crop), Target Location (Paste), Image Composite Chained, Match Image Count To Mask Count, Random Character Prompts, Parallax Test, Easy Parallax, Parallax GPU Test"
},
{
"author": "solution9th",
"title": "Comfyui_mobilesam [REMOVED]",
"reference": "https://github.com/solution9th/Comfyui_mobilesam",
"files": [
"https://github.com/solution9th/Comfyui_mobilesam"
],
"install_type": "git-clone",
"description": "NODES: Mobile SAM Model Loader, Mobile SAM Detector, Mobile SAM Predictor"
}, },
{ {
"author": "syaofox", "author": "syaofox",
"title": "ComfyUI_fnodes [REMOVED]", "title": "ComfyUI_fnodes [REMOVED]",

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

846
openapi.yaml Normal file
View File

@@ -0,0 +1,846 @@
openapi: 3.1.0
info:
title: ComfyUI-Manager API
description: |
API for ComfyUI-Manager, a comprehensive management tool for ComfyUI custom nodes, models, and components.
This API enables programmatic access to node management, model downloading, snapshot operations,
and overall system configuration.
version: "3.32.3"
contact:
name: ComfyUI-Manager Maintainers
servers:
- url: '/'
description: Default ComfyUI server
# Common API components
components:
schemas:
Error:
type: object
properties:
error:
type: string
description: Error message
NodePackageMetadata:
type: object
properties:
title:
type: string
description: Display name of the node package
name:
type: string
description: Repository/package name
files:
type: array
items:
type: string
description: Source URLs for the package
description:
type: string
description: Description of the node package functionality
install_type:
type: string
enum: [git, copy, pip]
description: Installation method
version:
type: string
description: Version identifier
id:
type: string
description: Unique identifier for the node package
ui_id:
type: string
description: ID for UI reference
channel:
type: string
description: Source channel
mode:
type: string
description: Source mode
ModelMetadata:
type: object
properties:
name:
type: string
description: Name of the model
type:
type: string
description: Type of model
base:
type: string
description: Base model type
save_path:
type: string
description: Path for saving the model
url:
type: string
description: Download URL
filename:
type: string
description: Target filename
ui_id:
type: string
description: ID for UI reference
SnapshotItem:
type: string
description: Name of the snapshot
QueueStatus:
type: object
properties:
total_count:
type: integer
description: Total number of tasks
done_count:
type: integer
description: Number of completed tasks
in_progress_count:
type: integer
description: Number of tasks in progress
is_processing:
type: boolean
description: Whether the queue is currently processing
securitySchemes:
securityLevel:
type: apiKey
in: header
name: Security-Level
description: Security level for sensitive operations
parameters:
modeParam:
name: mode
in: query
description: Source mode (e.g., "local", "remote")
schema:
type: string
enum: [local, remote, default]
targetParam:
name: target
in: query
description: Target identifier
required: true
schema:
type: string
valueParam:
name: value
in: query
description: New value to set
required: true
schema:
type: string
# API Paths
paths:
# Custom Nodes Endpoints
/customnode/getmappings:
get:
summary: Get node-to-package mappings
description: Provides unified mapping between nodes and node packages
parameters:
- $ref: '#/components/parameters/modeParam'
responses:
'200':
description: Successful operation
content:
application/json:
schema:
type: object
additionalProperties:
type: array
items:
type: array
description: Mapping of node packages to node classes
/customnode/fetch_updates:
get:
summary: Check for updates
description: Fetches updates for custom nodes
parameters:
- $ref: '#/components/parameters/modeParam'
responses:
'200':
description: No updates available
'201':
description: Updates found
'400':
description: Error occurred
/customnode/installed:
get:
summary: Get installed custom nodes
description: Returns a list of installed node packages
parameters:
- name: mode
in: query
description: Lists mode, default or imported
schema:
type: string
enum: [default, imported]
responses:
'200':
description: Successful operation
content:
application/json:
schema:
type: object
additionalProperties:
$ref: '#/components/schemas/NodePackageMetadata'
/customnode/getlist:
get:
summary: Get custom node list
description: Provides a list of available custom nodes
parameters:
- $ref: '#/components/parameters/modeParam'
- name: skip_update
in: query
description: Skip update check
schema:
type: boolean
responses:
'200':
description: Successful operation
content:
application/json:
schema:
type: object
properties:
channel:
type: string
node_packs:
type: object
additionalProperties:
$ref: '#/components/schemas/NodePackageMetadata'
/customnode/alternatives:
get:
summary: Get alternative node options
description: Provides alternatives for nodes
parameters:
- $ref: '#/components/parameters/modeParam'
responses:
'200':
description: Successful operation
content:
application/json:
schema:
type: object
additionalProperties:
type: object
/customnode/versions/{node_name}:
get:
summary: Get available versions for a node
description: Lists all available versions for a specific node
parameters:
- name: node_name
in: path
required: true
schema:
type: string
responses:
'200':
description: Successful operation
content:
application/json:
schema:
type: array
items:
type: object
properties:
version:
type: string
'400':
description: Node not found
/customnode/disabled_versions/{node_name}:
get:
summary: Get disabled versions for a node
description: Lists all disabled versions for a specific node
parameters:
- name: node_name
in: path
required: true
schema:
type: string
responses:
'200':
description: Successful operation
content:
application/json:
schema:
type: array
items:
type: object
properties:
version:
type: string
'400':
description: Node not found
/customnode/import_fail_info:
post:
summary: Get import failure information
description: Returns information about why a node failed to import
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
cnr_id:
type: string
url:
type: string
responses:
'200':
description: Successful operation
'400':
description: No information available
/customnode/install/git_url:
post:
summary: Install custom node via Git URL
description: Installs a custom node from a Git repository URL
security:
- securityLevel: []
requestBody:
required: true
content:
text/plain:
schema:
type: string
responses:
'200':
description: Installation successful or already installed
'400':
description: Installation failed
'403':
description: Security policy violation
/customnode/install/pip:
post:
summary: Install custom node dependencies via pip
description: Installs Python package dependencies for custom nodes
security:
- securityLevel: []
requestBody:
required: true
content:
text/plain:
schema:
type: string
responses:
'200':
description: Installation successful
'403':
description: Security policy violation
# Model Management Endpoints
/externalmodel/getlist:
get:
summary: Get external model list
description: Provides a list of available external models
parameters:
- $ref: '#/components/parameters/modeParam'
responses:
'200':
description: Successful operation
content:
application/json:
schema:
type: object
properties:
models:
type: array
items:
$ref: '#/components/schemas/ModelMetadata'
# Queue Management Endpoints
/manager/queue/update_all:
get:
summary: Update all custom nodes
description: Queues update operations for all installed custom nodes
security:
- securityLevel: []
parameters:
- $ref: '#/components/parameters/modeParam'
responses:
'200':
description: Update queued successfully
'401':
description: Processing already in progress
'403':
description: Security policy violation
/manager/queue/reset:
get:
summary: Reset queue
description: Resets the operation queue
responses:
'200':
description: Queue reset successfully
/manager/queue/status:
get:
summary: Get queue status
description: Returns the current status of the operation queue
responses:
'200':
description: Successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/QueueStatus'
/manager/queue/install:
post:
summary: Install custom node
description: Queues installation of a custom node
security:
- securityLevel: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/NodePackageMetadata'
responses:
'200':
description: Installation queued successfully
'403':
description: Security policy violation
'404':
description: Target node not found or security issue
/manager/queue/start:
get:
summary: Start queue processing
description: Starts processing the operation queue
responses:
'200':
description: Processing started
'201':
description: Processing already in progress
/manager/queue/fix:
post:
summary: Fix custom node
description: Attempts to fix a broken custom node installation
security:
- securityLevel: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/NodePackageMetadata'
responses:
'200':
description: Fix operation queued successfully
'403':
description: Security policy violation
/manager/queue/reinstall:
post:
summary: Reinstall custom node
description: Uninstalls and then reinstalls a custom node
security:
- securityLevel: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/NodePackageMetadata'
responses:
'200':
description: Reinstall operation queued successfully
'403':
description: Security policy violation
/manager/queue/uninstall:
post:
summary: Uninstall custom node
description: Queues uninstallation of a custom node
security:
- securityLevel: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/NodePackageMetadata'
responses:
'200':
description: Uninstallation queued successfully
'403':
description: Security policy violation
/manager/queue/update:
post:
summary: Update custom node
description: Queues update of a custom node
security:
- securityLevel: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/NodePackageMetadata'
responses:
'200':
description: Update queued successfully
'403':
description: Security policy violation
/manager/queue/disable:
post:
summary: Disable custom node
description: Disables a custom node without uninstalling it
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/NodePackageMetadata'
responses:
'200':
description: Disable operation queued successfully
/manager/queue/update_comfyui:
get:
summary: Update ComfyUI
description: Queues an update operation for ComfyUI itself
responses:
'200':
description: Update queued successfully
/manager/queue/install_model:
post:
summary: Install model
description: Queues installation of a model
security:
- securityLevel: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ModelMetadata'
responses:
'200':
description: Installation queued successfully
'400':
description: Invalid model request
'403':
description: Security policy violation
# Snapshot Management Endpoints
/snapshot/getlist:
get:
summary: Get snapshot list
description: Returns a list of available snapshots
responses:
'200':
description: Successful operation
content:
application/json:
schema:
type: object
properties:
items:
type: array
items:
$ref: '#/components/schemas/SnapshotItem'
/snapshot/remove:
get:
summary: Remove snapshot
description: Removes a specified snapshot
security:
- securityLevel: []
parameters:
- $ref: '#/components/parameters/targetParam'
responses:
'200':
description: Snapshot removed successfully
'400':
description: Error removing snapshot
'403':
description: Security policy violation
/snapshot/restore:
get:
summary: Restore snapshot
description: Restores a specified snapshot
security:
- securityLevel: []
parameters:
- $ref: '#/components/parameters/targetParam'
responses:
'200':
description: Snapshot restoration scheduled
'400':
description: Error restoring snapshot
'403':
description: Security policy violation
/snapshot/get_current:
get:
summary: Get current snapshot
description: Returns the current system state as a snapshot
responses:
'200':
description: Successful operation
content:
application/json:
schema:
type: object
'400':
description: Error creating snapshot
/snapshot/save:
get:
summary: Save snapshot
description: Saves the current system state as a new snapshot
responses:
'200':
description: Snapshot saved successfully
'400':
description: Error saving snapshot
# ComfyUI Management Endpoints
/comfyui_manager/comfyui_versions:
get:
summary: Get ComfyUI versions
description: Returns available and current ComfyUI versions
responses:
'200':
description: Successful operation
content:
application/json:
schema:
type: object
properties:
versions:
type: array
items:
type: string
current:
type: string
'400':
description: Error retrieving versions
/comfyui_manager/comfyui_switch_version:
get:
summary: Switch ComfyUI version
description: Switches to a specified ComfyUI version
parameters:
- name: ver
in: query
description: Target version
schema:
type: string
responses:
'200':
description: Version switch successful
'400':
description: Error switching version
/manager/reboot:
get:
summary: Reboot ComfyUI
description: Restarts the ComfyUI server
security:
- securityLevel: []
responses:
'200':
description: Reboot initiated
'403':
description: Security policy violation
# Configuration Endpoints
/manager/preview_method:
get:
summary: Get or set preview method
description: Gets or sets the latent preview method
parameters:
- name: value
in: query
required: false
description: New preview method
schema:
type: string
enum: [auto, latent2rgb, taesd, none]
responses:
'200':
description: Setting updated or current value returned
content:
text/plain:
schema:
type: string
/manager/db_mode:
get:
summary: Get or set database mode
description: Gets or sets the database mode
parameters:
- name: value
in: query
required: false
description: New database mode
schema:
type: string
enum: [channel, local, remote]
responses:
'200':
description: Setting updated or current value returned
content:
text/plain:
schema:
type: string
/manager/policy/component:
get:
summary: Get or set component policy
description: Gets or sets the component policy
parameters:
- name: value
in: query
required: false
description: New component policy
schema:
type: string
responses:
'200':
description: Setting updated or current value returned
content:
text/plain:
schema:
type: string
/manager/policy/update:
get:
summary: Get or set update policy
description: Gets or sets the update policy
parameters:
- name: value
in: query
required: false
description: New update policy
schema:
type: string
enum: [stable, nightly, nightly-comfyui]
responses:
'200':
description: Setting updated or current value returned
content:
text/plain:
schema:
type: string
/manager/channel_url_list:
get:
summary: Get or set channel URL
description: Gets or sets the channel URL for custom node sources
parameters:
- name: value
in: query
required: false
description: New channel name
schema:
type: string
responses:
'200':
description: Setting updated or channel list returned
content:
application/json:
schema:
type: object
properties:
selected:
type: string
list:
type: array
items:
type: object
properties:
name:
type: string
url:
type: string
# Component Management Endpoints
/manager/component/save:
post:
summary: Save component
description: Saves a reusable workflow component
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
name:
type: string
workflow:
type: object
responses:
'200':
description: Component saved successfully
content:
text/plain:
schema:
type: string
'400':
description: Error saving component
/manager/component/loads:
post:
summary: Load components
description: Loads all available workflow components
responses:
'200':
description: Components loaded successfully
content:
application/json:
schema:
type: object
'400':
description: Error loading components
# Miscellaneous Endpoints
/manager/version:
get:
summary: Get manager version
description: Returns the current version of ComfyUI-Manager
responses:
'200':
description: Successful operation
content:
text/plain:
schema:
type: string
/manager/notice:
get:
summary: Get manager notice
description: Returns HTML content with notices and version information
responses:
'200':
description: Successful operation
content:
text/html:
schema:
type: string

View File

@@ -344,7 +344,12 @@ try:
log_file.write(message) log_file.write(message)
else: else:
log_file.write(f"[{timestamp}] {message}") log_file.write(f"[{timestamp}] {message}")
log_file.flush()
try:
log_file.flush()
except Exception:
pass
self.last_char = message if message == '' else message[-1] self.last_char = message if message == '' else message[-1]
if not file_only: if not file_only:
@@ -357,7 +362,10 @@ try:
original_stderr.flush() original_stderr.flush()
def flush(self): def flush(self):
log_file.flush() try:
log_file.flush()
except Exception:
pass
with std_log_lock: with std_log_lock:
if self.is_stdout: if self.is_stdout:

View File

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

View File

@@ -102,12 +102,8 @@ def extract_nodes(code_text):
def scan_in_file(filename, is_builtin=False): def scan_in_file(filename, is_builtin=False):
global builtin_nodes global builtin_nodes
try: with open(filename, encoding='utf-8', errors='ignore') as file:
with open(filename, encoding='utf-8') as file: code = file.read()
code = file.read()
except UnicodeDecodeError:
with open(filename, encoding='cp949') as file:
code = file.read()
pattern = r"_CLASS_MAPPINGS\s*=\s*{([^}]*)}" pattern = r"_CLASS_MAPPINGS\s*=\s*{([^}]*)}"
regex = re.compile(pattern, re.MULTILINE | re.DOTALL) regex = re.compile(pattern, re.MULTILINE | re.DOTALL)
@@ -297,7 +293,7 @@ def update_custom_nodes():
pass pass
def is_rate_limit_exceeded(): def is_rate_limit_exceeded():
return g.rate_limiting[0] == 0 return g.rate_limiting[0] <= 20
if is_rate_limit_exceeded(): if is_rate_limit_exceeded():
print(f"GitHub API Rate Limit Exceeded: remained - {(g.rate_limiting_resettime - datetime.datetime.now().timestamp())/60:.2f} min") print(f"GitHub API Rate Limit Exceeded: remained - {(g.rate_limiting_resettime - datetime.datetime.now().timestamp())/60:.2f} min")

19
tests-api/.gitignore vendored
View File

@@ -1,19 +0,0 @@
# Python cache files
__pycache__/
*.py[cod]
*$py.class
# Pytest cache
.pytest_cache/
# Coverage reports
.coverage
htmlcov/
# Virtual environments
venv/
env/
ENV/
# Test-specific resources
resources/tmp/

View File

@@ -1,91 +0,0 @@
# ComfyUI-Manager API Tests
This directory contains tests for the ComfyUI-Manager API endpoints, validating the OpenAPI specification and ensuring API functionality.
## Setup
1. Install test dependencies:
```bash
pip install -r requirements-test.txt
```
2. Ensure ComfyUI is running with ComfyUI-Manager installed:
```bash
# Start ComfyUI with the default server
python main.py
```
## Running Tests
### Run all tests
```bash
pytest -xvs
```
### Run specific test files
```bash
# Run only the spec validation tests
pytest -xvs test_spec_validation.py
# Run only the custom node API tests
pytest -xvs test_customnode_api.py
```
### Run specific test functions
```bash
# Run a specific test
pytest -xvs test_customnode_api.py::test_get_custom_node_list
```
## Test Configuration
The tests use the following default configuration:
- Server URL: `http://localhost:8188`
- Server timeout: 2 seconds
- Wait between requests: 0.5 seconds
- Maximum retries: 3
You can override these settings with environment variables:
```bash
# Use a different server URL
COMFYUI_SERVER_URL=http://localhost:8189 pytest -xvs
```
## Test Categories
The tests are organized into the following categories:
1. **Spec Validation** (`test_spec_validation.py`): Validates that the OpenAPI specification is correct and complete.
2. **Custom Node API** (`test_customnode_api.py`): Tests for custom node management endpoints.
3. **Snapshot API** (`test_snapshot_api.py`): Tests for snapshot management endpoints.
4. **Queue API** (`test_queue_api.py`): Tests for queue management endpoints.
5. **Config API** (`test_config_api.py`): Tests for configuration endpoints.
6. **Model API** (`test_model_api.py`): Tests for model management endpoints (minimal as these are being deprecated).
## Test Implementation Details
### Fixtures
- `test_config`: Provides the test configuration
- `server_url`: Returns the server URL from the configuration
- `openapi_spec`: Loads the OpenAPI specification
- `api_client`: Creates a requests Session for API calls
- `api_request`: Helper function for making consistent API requests
### Utilities
- `validation.py`: Functions for validating responses against the OpenAPI schema
- `schema_utils.py`: Utilities for extracting and manipulating schemas
## Notes
- Some tests are skipped with `@pytest.mark.skip` to avoid modifying state in automated testing
- Security-level restricted endpoints have minimal tests to avoid security issues
- Tests focus on read operations rather than write operations where possible

View File

@@ -1 +0,0 @@
# Make tests-api directory a proper package

View File

@@ -1,237 +0,0 @@
"""
PyTest configuration and fixtures for API tests.
"""
import os
import sys
import json
import pytest
import requests
import tempfile
import time
import yaml
from pathlib import Path
from typing import Dict, Generator, Optional, Tuple
# Import test utilities
import sys
import os
from pathlib import Path
# Get the absolute path to the current file (conftest.py)
current_file = Path(os.path.abspath(__file__))
# Get the directory containing the current file (the tests-api directory)
tests_api_dir = current_file.parent
# Add the tests-api directory to the Python path
if str(tests_api_dir) not in sys.path:
sys.path.insert(0, str(tests_api_dir))
# Apply mocks for ComfyUI imports
from mocks.patch import apply_mocks
apply_mocks()
# Now we can import from utils.validation
from utils.validation import load_openapi_spec
# Default test configuration
DEFAULT_TEST_CONFIG = {
"server_url": "http://localhost:8188",
"server_timeout": 2, # seconds
"wait_between_requests": 0.5, # seconds
"max_retries": 3,
}
@pytest.fixture(scope="session")
def test_config() -> Dict:
"""
Load test configuration from environment variables or use defaults.
"""
config = DEFAULT_TEST_CONFIG.copy()
# Override from environment variables if present
if "COMFYUI_SERVER_URL" in os.environ:
config["server_url"] = os.environ["COMFYUI_SERVER_URL"]
return config
@pytest.fixture(scope="session")
def server_url(test_config: Dict) -> str:
"""
Get the server URL from the test configuration.
"""
return test_config["server_url"]
@pytest.fixture(scope="session")
def openapi_spec() -> Dict:
"""
Load the OpenAPI specification.
"""
return load_openapi_spec()
@pytest.fixture(scope="session")
def api_client(server_url: str, test_config: Dict) -> requests.Session:
"""
Create a requests Session for API calls.
"""
session = requests.Session()
# Check if the server is running
try:
response = session.get(f"{server_url}/", timeout=test_config["server_timeout"])
response.raise_for_status()
except (requests.ConnectionError, requests.Timeout, requests.HTTPError):
pytest.skip("ComfyUI server is not running or not accessible")
return session
@pytest.fixture(scope="function")
def temp_dir() -> Generator[Path, None, None]:
"""
Create a temporary directory for test files.
"""
with tempfile.TemporaryDirectory() as temp_dir:
yield Path(temp_dir)
class SecurityLevelContext:
"""
Context manager for setting and restoring security levels.
"""
def __init__(self, api_client: requests.Session, server_url: str, security_level: str):
self.api_client = api_client
self.server_url = server_url
self.security_level = security_level
self.original_level = None
async def __aenter__(self):
# Get the current security level (not directly exposed in API, would require more setup)
# For now, we'll just set the new level
# Set the new security level
# Note: In a real implementation, we would need a way to set this
# This is a placeholder - the actual implementation would depend on how
# security levels are managed in ComfyUI-Manager
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
# Restore the original security level if needed
pass
@pytest.fixture
def security_level_context(api_client: requests.Session, server_url: str):
"""
Create a context manager for setting security levels.
"""
return lambda level: SecurityLevelContext(api_client, server_url, level)
def make_api_url(server_url: str, path: str) -> str:
"""
Construct a full API URL from the server URL and path.
"""
# Ensure the path starts with a slash
if not path.startswith("/"):
path = f"/{path}"
# Remove trailing slash from server_url if present
if server_url.endswith("/"):
server_url = server_url[:-1]
return f"{server_url}{path}"
@pytest.fixture
def api_request(api_client: requests.Session, server_url: str, test_config: Dict):
"""
Helper function for making API requests with consistent behavior.
"""
def _request(
method: str,
path: str,
params: Optional[Dict] = None,
json_data: Optional[Dict] = None,
headers: Optional[Dict] = None,
expected_status: int = 200,
retry_on_error: bool = True,
) -> Tuple[requests.Response, Optional[Dict]]:
"""
Make an API request with automatic validation.
Args:
method: HTTP method
path: API path
params: Query parameters
json_data: JSON request body
headers: HTTP headers
expected_status: Expected HTTP status code
retry_on_error: Whether to retry on connection errors
Returns:
Tuple of (Response object, JSON response data or None)
"""
method = method.lower()
url = make_api_url(server_url, path)
if headers is None:
headers = {}
# Add common headers
headers.setdefault("Accept", "application/json")
# Sleep between requests to avoid overwhelming the server
time.sleep(test_config["wait_between_requests"])
retries = test_config["max_retries"] if retry_on_error else 0
last_exception = None
for attempt in range(retries + 1):
try:
if method == "get":
response = api_client.get(url, params=params, headers=headers)
elif method == "post":
response = api_client.post(url, params=params, json=json_data, headers=headers)
elif method == "put":
response = api_client.put(url, params=params, json=json_data, headers=headers)
elif method == "delete":
response = api_client.delete(url, params=params, headers=headers)
else:
raise ValueError(f"Unsupported HTTP method: {method}")
# Check status code
assert response.status_code == expected_status, (
f"Expected status code {expected_status}, got {response.status_code}"
)
# Parse JSON response if possible
json_response = None
if response.headers.get("Content-Type", "").startswith("application/json"):
try:
json_response = response.json()
except json.JSONDecodeError:
if expected_status == 200:
raise ValueError("Response was not valid JSON")
return response, json_response
except (requests.ConnectionError, requests.Timeout) as e:
last_exception = e
if attempt < retries:
# Wait before retrying
time.sleep(1)
continue
break
if last_exception:
raise last_exception
raise RuntimeError("Failed to make API request")
return _request

View File

@@ -1 +0,0 @@
# Make tests-api/mocks directory a proper package

View File

@@ -1,26 +0,0 @@
"""
Mock CustomNodeManager for testing purposes
"""
class CustomNodeManager:
"""
Mock implementation of the CustomNodeManager class
"""
instance = None
def __init__(self):
self.custom_nodes = {}
self.node_paths = []
self.refresh_timeout = None
def get_node_path(self, node_class):
"""
Mock implementation to get the path for a node class
"""
return self.custom_nodes.get(node_class, None)
def update_node_paths(self):
"""
Mock implementation to update node paths
"""
pass

View File

@@ -1,116 +0,0 @@
"""
Patch module to mock imports for testing
"""
import sys
import importlib.util
import os
from pathlib import Path
# Import mock modules
from mocks.prompt_server import PromptServer
from mocks.custom_node_manager import CustomNodeManager
# Current directory
current_dir = Path(__file__).parent.parent # tests-api directory
# Define mocks
class MockModule:
"""Base class for mock modules"""
pass
# Create server mock module with PromptServer
server_mock = MockModule()
server_mock.PromptServer = PromptServer
prompt_server_instance = PromptServer()
server_mock.PromptServer.instance = prompt_server_instance
server_mock.PromptServer.inst = prompt_server_instance
# Create app mock module with custom_node_manager submodule
app_mock = MockModule()
app_custom_node_manager = MockModule()
app_custom_node_manager.CustomNodeManager = CustomNodeManager
app_custom_node_manager.CustomNodeManager.instance = CustomNodeManager()
# Create utils mock module with json_util submodule
utils_mock = MockModule()
utils_json_util = MockModule()
# Create utils.validation and utils.schema_utils submodules
utils_validation = MockModule()
utils_schema_utils = MockModule()
# Import actual modules (make sure path is set up correctly)
sys.path.insert(0, str(current_dir))
try:
# Import the validation module
from utils.validation import load_openapi_spec
utils_validation.load_openapi_spec = load_openapi_spec
# Import all schema_utils functions
from utils.schema_utils import (
get_all_paths,
get_grouped_paths,
get_methods_for_path,
find_paths_with_security,
get_content_types_for_response,
get_required_parameters
)
utils_schema_utils.get_all_paths = get_all_paths
utils_schema_utils.get_grouped_paths = get_grouped_paths
utils_schema_utils.get_methods_for_path = get_methods_for_path
utils_schema_utils.find_paths_with_security = find_paths_with_security
utils_schema_utils.get_content_types_for_response = get_content_types_for_response
utils_schema_utils.get_required_parameters = get_required_parameters
except ImportError as e:
print(f"Error importing test utilities: {e}")
# Define dummy functions if imports fail
def dummy_load_openapi_spec():
"""Dummy function for testing"""
return {"paths": {}}
utils_validation.load_openapi_spec = dummy_load_openapi_spec
def dummy_get_all_paths(spec):
return list(spec.get("paths", {}).keys())
utils_schema_utils.get_all_paths = dummy_get_all_paths
def dummy_get_grouped_paths(spec):
return {}
utils_schema_utils.get_grouped_paths = dummy_get_grouped_paths
def dummy_get_methods_for_path(spec, path):
return []
utils_schema_utils.get_methods_for_path = dummy_get_methods_for_path
def dummy_find_paths_with_security(spec, security_scheme=None):
return []
utils_schema_utils.find_paths_with_security = dummy_find_paths_with_security
def dummy_get_content_types_for_response(spec, path, method, status_code="200"):
return []
utils_schema_utils.get_content_types_for_response = dummy_get_content_types_for_response
def dummy_get_required_parameters(spec, path, method):
return []
utils_schema_utils.get_required_parameters = dummy_get_required_parameters
# Add merge_json_recursive from our mock utils
from mocks.utils import merge_json_recursive
utils_json_util.merge_json_recursive = merge_json_recursive
# Apply the mocks to sys.modules
def apply_mocks():
"""Apply all mocks to sys.modules"""
sys.modules['server'] = server_mock
sys.modules['app'] = app_mock
sys.modules['app.custom_node_manager'] = app_custom_node_manager
sys.modules['utils'] = utils_mock
sys.modules['utils.json_util'] = utils_json_util
sys.modules['utils.validation'] = utils_validation
sys.modules['utils.schema_utils'] = utils_schema_utils
# Make sure our actual utils module is importable
if current_dir not in sys.path:
sys.path.insert(0, str(current_dir))

View File

@@ -1,71 +0,0 @@
"""
Mock PromptServer for testing purposes
"""
class MockRoutes:
"""
Mock routing class with method decorators
"""
def __init__(self):
self.routes = {}
def get(self, path):
"""Decorator for GET routes"""
def decorator(f):
self.routes[('GET', path)] = f
return f
return decorator
def post(self, path):
"""Decorator for POST routes"""
def decorator(f):
self.routes[('POST', path)] = f
return f
return decorator
def put(self, path):
"""Decorator for PUT routes"""
def decorator(f):
self.routes[('PUT', path)] = f
return f
return decorator
def delete(self, path):
"""Decorator for DELETE routes"""
def decorator(f):
self.routes[('DELETE', path)] = f
return f
return decorator
class PromptServer:
"""
Mock implementation of the PromptServer class
"""
instance = None
inst = None
def __init__(self):
self.routes = MockRoutes()
self.registered_paths = set()
self.base_url = "http://127.0.0.1:8188" # Assuming server is running on default port
self.queue_lock = None
def add_route(self, method, path, handler, *args, **kwargs):
"""
Add a mock route to the server
"""
self.routes.routes[(method.upper(), path)] = handler
self.registered_paths.add(path)
async def send_msg(self, message, data=None):
"""
Mock send_msg method (does nothing in the mock)
"""
pass
def send_sync(self, message, data=None):
"""
Mock send_sync method (does nothing in the mock)
"""
pass

View File

@@ -1,20 +0,0 @@
"""
Mock utils module for testing purposes
"""
def merge_json_recursive(a, b):
"""
Mock implementation of merge_json_recursive
"""
if isinstance(a, dict) and isinstance(b, dict):
result = a.copy()
for key, value in b.items():
if key in result and isinstance(result[key], (dict, list)) and isinstance(value, (dict, list)):
result[key] = merge_json_recursive(result[key], value)
else:
result[key] = value
return result
elif isinstance(a, list) and isinstance(b, list):
return a + b
else:
return b

View File

@@ -1,382 +0,0 @@
openapi: 3.0.3
info:
title: ComfyUI-Manager API
description: API for managing ComfyUI extensions, custom nodes, and models
version: 1.0.0
contact:
name: ComfyUI Community
url: https://github.com/comfyanonymous/ComfyUI
servers:
- url: http://localhost:8188
description: Local ComfyUI server
paths:
/customnode/getlist:
get:
summary: Get the list of custom nodes
description: Returns the list of custom nodes from all configured channels
parameters:
- name: mode
in: query
description: "The mode to retrieve (local=installed nodes, remote=available nodes)"
schema:
type: string
enum: [local, remote]
default: remote
responses:
'200':
description: List of custom nodes
content:
application/json:
schema:
type: object
properties:
nodes:
type: array
items:
$ref: '#/components/schemas/CustomNode'
'500':
description: Server error
/customnode/get_node_mappings:
get:
summary: Get mappings between node class names and their custom nodes
description: Returns mappings that help identify which custom node package provides specific node classes
parameters:
- name: mode
in: query
description: "The mode for mappings (local=installed nodes, nickname=node nicknames)"
schema:
type: string
enum: [local, nickname]
default: local
required: true
responses:
'200':
description: Node mappings
content:
application/json:
schema:
type: object
additionalProperties:
type: string
'500':
description: Server error
/customnode/get_node_alternatives:
get:
summary: Get alternative nodes for specific node classes
description: Returns alternative implementations of node classes from different custom node packages
parameters:
- name: mode
in: query
description: "The mode to retrieve alternatives (local=installed nodes, remote=all available nodes)"
schema:
type: string
enum: [local, remote]
default: remote
responses:
'200':
description: Node alternatives
content:
application/json:
schema:
type: object
additionalProperties:
type: array
items:
type: string
'500':
description: Server error
/externalmodel/getlist:
get:
summary: Get the list of external models
description: Returns the list of models from all configured channels
parameters:
- name: mode
in: query
description: "The mode to retrieve (local=installed models, remote=available models)"
schema:
type: string
enum: [local, remote]
default: remote
responses:
'200':
description: List of external models
content:
application/json:
schema:
type: object
properties:
models:
type: array
items:
$ref: '#/components/schemas/ExternalModel'
'500':
description: Server error
/manager/get_config:
get:
summary: Get manager configuration
description: Returns the current configuration of ComfyUI-Manager
parameters:
- name: key
in: query
description: "The configuration key to retrieve"
schema:
type: string
required: true
responses:
'200':
description: Configuration value
content:
application/json:
schema:
type: object
properties:
value:
type: string
'400':
description: Invalid key or missing parameter
'500':
description: Server error
/manager/set_config:
post:
summary: Set manager configuration
description: Updates the configuration of ComfyUI-Manager
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- key
- value
properties:
key:
type: string
description: "The configuration key to update"
value:
type: string
description: "The new value for the configuration key"
responses:
'200':
description: Configuration updated successfully
content:
application/json:
schema:
type: object
properties:
success:
type: boolean
'400':
description: Invalid key or value
'500':
description: Server error
/snapshot/getlist:
get:
summary: Get the list of snapshots
description: Returns the list of saved snapshots
responses:
'200':
description: List of snapshots
content:
application/json:
schema:
type: object
properties:
snapshots:
type: array
items:
$ref: '#/components/schemas/Snapshot'
'500':
description: Server error
/comfyui_manager/queue/status:
get:
summary: Get queue status
description: Returns the current status of the operation queue
responses:
'200':
description: Queue status
content:
application/json:
schema:
$ref: '#/components/schemas/QueueStatus'
'500':
description: Server error
components:
schemas:
CustomNode:
type: object
required:
- name
- title
- reference
properties:
name:
type: string
description: "Internal name/ID of the custom node"
title:
type: string
description: "Display title of the custom node"
reference:
type: string
description: "Reference URL (usually GitHub repository URL)"
description:
type: string
description: "Description of what the custom node does"
install_type:
type: string
enum: [git, pip, copy]
description: "Installation method for the custom node"
files:
type: array
items:
type: string
description: "List of files provided by this custom node"
node_class_names:
type: array
items:
type: string
description: "List of node class names provided by this custom node"
installed:
type: boolean
description: "Whether the custom node is installed"
version:
type: string
description: "Version of the custom node"
tags:
type: array
items:
type: string
description: "Tags associated with the custom node"
ExternalModel:
type: object
required:
- name
- type
- url
properties:
name:
type: string
description: "Name of the model"
type:
type: string
description: "Type of the model (checkpoint, lora, embedding, etc.)"
url:
type: string
description: "Download URL for the model"
description:
type: string
description: "Description of the model"
size:
type: integer
description: "Size of the model in bytes"
installed:
type: boolean
description: "Whether the model is installed"
version:
type: string
description: "Version of the model"
tags:
type: array
items:
type: string
description: "Tags associated with the model"
Snapshot:
type: object
required:
- name
- date
properties:
name:
type: string
description: "Name of the snapshot"
date:
type: string
format: date-time
description: "Date when the snapshot was created"
description:
type: string
description: "Description of the snapshot"
nodes:
type: array
items:
type: string
description: "List of custom nodes in the snapshot"
models:
type: array
items:
type: string
description: "List of models in the snapshot"
QueueStatus:
type: object
properties:
pending:
type: array
items:
$ref: '#/components/schemas/QueueItem'
description: "List of pending operations in the queue"
completed:
type: array
items:
$ref: '#/components/schemas/QueueItem'
description: "List of completed operations in the queue"
failed:
type: array
items:
$ref: '#/components/schemas/QueueItem'
description: "List of failed operations in the queue"
running:
type: boolean
description: "Whether the queue is currently running"
QueueItem:
type: object
required:
- id
- type
- target
properties:
id:
type: string
description: "Unique ID of the queue item"
type:
type: string
enum: [install, update, uninstall]
description: "Type of operation"
target:
type: string
description: "Target of the operation (e.g., custom node name, model name)"
status:
type: string
enum: [pending, processing, completed, failed]
description: "Current status of the operation"
error:
type: string
description: "Error message if the operation failed"
created_at:
type: string
format: date-time
description: "Time when the operation was added to the queue"
completed_at:
type: string
format: date-time
description: "Time when the operation was completed"
securitySchemes:
ApiKeyAuth:
type: apiKey
in: header
name: X-API-Key
description: "API key for authentication"

View File

@@ -1,6 +0,0 @@
pytest>=7.3.1
requests>=2.31.0
openapi-spec-validator>=0.6.0
jsonschema>=4.17.3
pytest-asyncio>=0.21.0
pyyaml>=6.0

View File

@@ -1,270 +0,0 @@
"""
Tests for configuration endpoints.
"""
import pytest
from typing import Callable, Dict, List, Tuple
from utils.validation import validate_response
def test_get_preview_method(
api_request: Callable
):
"""
Test getting the current preview method.
"""
# Make the API request
path = "/manager/preview_method"
response, _ = api_request(
method="get",
path=path,
expected_status=200,
)
# Verify the response is one of the valid preview methods
assert response.text in ["auto", "latent2rgb", "taesd", "none"]
def test_get_db_mode(
api_request: Callable
):
"""
Test getting the current database mode.
"""
# Make the API request
path = "/manager/db_mode"
response, _ = api_request(
method="get",
path=path,
expected_status=200,
)
# Verify the response is one of the valid database modes
assert response.text in ["channel", "local", "remote"]
def test_get_component_policy(
api_request: Callable
):
"""
Test getting the current component policy.
"""
# Make the API request
path = "/manager/policy/component"
response, _ = api_request(
method="get",
path=path,
expected_status=200,
)
# Component policy could be any string
assert response.text is not None
def test_get_update_policy(
api_request: Callable
):
"""
Test getting the current update policy.
"""
# Make the API request
path = "/manager/policy/update"
response, _ = api_request(
method="get",
path=path,
expected_status=200,
)
# Verify the response is one of the valid update policies
assert response.text in ["stable", "nightly", "nightly-comfyui"]
def test_get_channel_url_list(
api_request: Callable,
openapi_spec: Dict
):
"""
Test getting the channel URL list.
"""
# Make the API request
path = "/manager/channel_url_list"
response, json_data = api_request(
method="get",
path=path,
expected_status=200,
)
# Validate response structure against the schema
assert json_data is not None
validate_response(
response_data=json_data,
path=path,
method="get",
spec=openapi_spec,
)
# Verify the response contains the expected fields
assert "selected" in json_data
assert "list" in json_data
assert isinstance(json_data["list"], list)
# Each channel should have a name and URL
if json_data["list"]:
first_channel = json_data["list"][0]
assert "name" in first_channel
assert "url" in first_channel
def test_get_manager_version(
api_request: Callable
):
"""
Test getting the manager version.
"""
# Make the API request
path = "/manager/version"
response, _ = api_request(
method="get",
path=path,
expected_status=200,
)
# Verify the response is a version string
assert response.text.startswith("V") # Version strings start with V
def test_get_manager_notice(
api_request: Callable
):
"""
Test getting the manager notice.
"""
# Make the API request
path = "/manager/notice"
response, _ = api_request(
method="get",
path=path,
expected_status=200,
)
# Verify the response is HTML content
assert response.headers.get("Content-Type", "").startswith("text/html") or "ComfyUI" in response.text
@pytest.mark.skip(reason="State-modifying operations")
class TestConfigChanges:
"""
Tests for changing configuration settings.
These are skipped to avoid modifying state in automated tests.
"""
@pytest.fixture(scope="class", autouse=True)
def save_original_config(self, api_request: Callable):
"""
Save the original configuration to restore after tests.
"""
# Save original values
response, _ = api_request(
method="get",
path="/manager/preview_method",
expected_status=200,
)
self.original_preview_method = response.text
response, _ = api_request(
method="get",
path="/manager/db_mode",
expected_status=200,
)
self.original_db_mode = response.text
response, _ = api_request(
method="get",
path="/manager/policy/update",
expected_status=200,
)
self.original_update_policy = response.text
yield
# Restore original values
api_request(
method="get",
path="/manager/preview_method",
params={"value": self.original_preview_method},
expected_status=200,
)
api_request(
method="get",
path="/manager/db_mode",
params={"value": self.original_db_mode},
expected_status=200,
)
api_request(
method="get",
path="/manager/policy/update",
params={"value": self.original_update_policy},
expected_status=200,
)
def test_set_preview_method(self, api_request: Callable):
"""
Test setting the preview method.
"""
# Set to a different value (taesd)
api_request(
method="get",
path="/manager/preview_method",
params={"value": "taesd"},
expected_status=200,
)
# Verify it was changed
response, _ = api_request(
method="get",
path="/manager/preview_method",
expected_status=200,
)
assert response.text == "taesd"
def test_set_db_mode(self, api_request: Callable):
"""
Test setting the database mode.
"""
# Set to local mode
api_request(
method="get",
path="/manager/db_mode",
params={"value": "local"},
expected_status=200,
)
# Verify it was changed
response, _ = api_request(
method="get",
path="/manager/db_mode",
expected_status=200,
)
assert response.text == "local"
def test_set_update_policy(self, api_request: Callable):
"""
Test setting the update policy.
"""
# Set to stable
api_request(
method="get",
path="/manager/policy/update",
params={"value": "stable"},
expected_status=200,
)
# Verify it was changed
response, _ = api_request(
method="get",
path="/manager/policy/update",
expected_status=200,
)
assert response.text == "stable"

View File

@@ -1,200 +0,0 @@
"""
Tests for custom node management endpoints.
"""
import pytest
from pathlib import Path
from typing import Callable, Dict, Tuple
from utils.validation import validate_response
@pytest.mark.parametrize(
"mode",
["local", "remote"]
)
def test_get_custom_node_list(
api_request: Callable,
openapi_spec: Dict,
mode: str
):
"""
Test the endpoint for listing custom nodes.
"""
# Make the API request
path = "/customnode/getlist"
response, json_data = api_request(
method="get",
path=path,
params={"mode": mode, "skip_update": "true"},
expected_status=200,
)
# Validate response structure against the schema
assert json_data is not None
validate_response(
response_data=json_data,
path=path,
method="get",
spec=openapi_spec,
)
# Verify the response contains the expected fields
assert "channel" in json_data
assert "node_packs" in json_data
assert isinstance(json_data["node_packs"], dict)
# If there are any node packs, verify they have the expected structure
if json_data["node_packs"]:
# Take the first node pack to validate
first_node_pack = next(iter(json_data["node_packs"].values()))
assert "title" in first_node_pack
assert "name" in first_node_pack
def test_get_installed_nodes(
api_request: Callable,
openapi_spec: Dict
):
"""
Test the endpoint for listing installed nodes.
"""
# Make the API request
path = "/customnode/installed"
response, json_data = api_request(
method="get",
path=path,
expected_status=200,
)
# Validate response structure against the schema
assert json_data is not None
validate_response(
response_data=json_data,
path=path,
method="get",
spec=openapi_spec,
)
# Verify the response is a dictionary of node packs
assert isinstance(json_data, dict)
@pytest.mark.parametrize(
"mode",
["local", "nickname"]
)
def test_get_node_mappings(
api_request: Callable,
openapi_spec: Dict,
mode: str
):
"""
Test the endpoint for getting node-to-package mappings.
"""
# Make the API request
path = "/customnode/getmappings"
response, json_data = api_request(
method="get",
path=path,
params={"mode": mode},
expected_status=200,
)
# Validate response structure against the schema
assert json_data is not None
validate_response(
response_data=json_data,
path=path,
method="get",
spec=openapi_spec,
)
# Verify the response is a dictionary mapping extension IDs to node info
assert isinstance(json_data, dict)
# If there are any mappings, verify they have the expected structure
if json_data:
# Take the first mapping to validate
first_mapping = next(iter(json_data.values()))
assert isinstance(first_mapping, list)
assert len(first_mapping) == 2
assert isinstance(first_mapping[0], list) # List of node classes
assert isinstance(first_mapping[1], dict) # Metadata
@pytest.mark.parametrize(
"mode",
["local", "remote"]
)
def test_get_node_alternatives(
api_request: Callable,
openapi_spec: Dict,
mode: str
):
"""
Test the endpoint for getting alternative node options.
"""
# Make the API request
path = "/customnode/alternatives"
response, json_data = api_request(
method="get",
path=path,
params={"mode": mode},
expected_status=200,
)
# Validate response structure against the schema
assert json_data is not None
validate_response(
response_data=json_data,
path=path,
method="get",
spec=openapi_spec,
)
# Verify the response is a dictionary
assert isinstance(json_data, dict)
def test_fetch_updates(
api_request: Callable
):
"""
Test the endpoint for fetching updates.
This might modify state, so we just check for a valid response.
"""
# Make the API request with skip_update=true to avoid actual updates
path = "/customnode/fetch_updates"
response, _ = api_request(
method="get",
path=path,
params={"mode": "local"},
# Don't validate JSON since this endpoint doesn't return JSON
expected_status=200,
retry_on_error=False, # Don't retry as this might have side effects
)
# Just check the status code is as expected (covered by api_request)
assert response.status_code in [200, 201]
@pytest.mark.skip(reason="Queue endpoints are better tested with queue operations")
def test_queue_update_all(
api_request: Callable
):
"""
Test the endpoint for queuing updates for all nodes.
Skipping as this would actually modify the installation.
"""
pass
@pytest.mark.skip(reason="Security-restricted endpoint")
def test_install_node_via_git_url(
api_request: Callable
):
"""
Test the endpoint for installing a node via Git URL.
Skipping as this requires high security level and would modify the installation.
"""
pass

View File

@@ -1,23 +0,0 @@
import os
import sys
# Print current working directory
print(f"Current directory: {os.getcwd()}")
# Print module search path
print(f"System path: {sys.path}")
# Try to import
try:
from utils.validation import load_openapi_spec
print("Import successful!")
except ImportError as e:
print(f"Import error: {e}")
# Try direct import
try:
sys.path.insert(0, os.path.join(os.getcwd(), "custom_nodes/ComfyUI-Manager/tests-api"))
from utils.validation import load_openapi_spec
print("Direct import successful!")
except ImportError as e:
print(f"Direct import error: {e}")

View File

@@ -1,62 +0,0 @@
"""
Tests for model management endpoints.
These features are scheduled for deprecation, so tests are minimal.
"""
import pytest
from typing import Callable, Dict
from utils.validation import validate_response
@pytest.mark.parametrize(
"mode",
["local", "remote"]
)
def test_get_external_model_list(
api_request: Callable,
openapi_spec: Dict,
mode: str
):
"""
Test the endpoint for listing external models.
"""
# Make the API request
path = "/externalmodel/getlist"
response, json_data = api_request(
method="get",
path=path,
params={"mode": mode},
expected_status=200,
)
# Validate response structure against the schema
assert json_data is not None
validate_response(
response_data=json_data,
path=path,
method="get",
spec=openapi_spec,
)
# Verify the response contains the expected fields
assert "models" in json_data
assert isinstance(json_data["models"], list)
# If there are any models, verify they have the expected structure
if json_data["models"]:
first_model = json_data["models"][0]
assert "name" in first_model
assert "type" in first_model
assert "url" in first_model
assert "filename" in first_model
assert "installed" in first_model
@pytest.mark.skip(reason="State-modifying operation that requires auth")
def test_install_model():
"""
Test queuing a model installation.
Skipped to avoid modifying state and requires authentication.
This feature is also scheduled for deprecation.
"""
pass

View File

@@ -1,213 +0,0 @@
"""
Tests for queue management endpoints.
"""
import pytest
import time
from pathlib import Path
from typing import Callable, Dict, Tuple
from utils.validation import validate_response
def test_get_queue_status(
api_request: Callable,
openapi_spec: Dict
):
"""
Test the endpoint for getting queue status.
"""
# Make the API request
path = "/manager/queue/status"
response, json_data = api_request(
method="get",
path=path,
expected_status=200,
)
# Validate response structure against the schema
assert json_data is not None
validate_response(
response_data=json_data,
path=path,
method="get",
spec=openapi_spec,
)
# Verify the response contains the expected fields
assert "total_count" in json_data
assert "done_count" in json_data
assert "in_progress_count" in json_data
assert "is_processing" in json_data
# Type checks
assert isinstance(json_data["total_count"], int)
assert isinstance(json_data["done_count"], int)
assert isinstance(json_data["in_progress_count"], int)
assert isinstance(json_data["is_processing"], bool)
def test_reset_queue(
api_request: Callable
):
"""
Test the endpoint for resetting the queue.
"""
# Make the API request
path = "/manager/queue/reset"
response, _ = api_request(
method="get",
path=path,
expected_status=200,
)
# Now check the queue status to verify it was reset
response2, json_data = api_request(
method="get",
path="/manager/queue/status",
expected_status=200,
)
# Queue should be empty after reset
assert json_data["total_count"] == json_data["done_count"] + json_data["in_progress_count"]
@pytest.mark.skip(reason="State-modifying operation that requires auth")
def test_queue_install_node():
"""
Test queuing a node installation.
Skipped to avoid modifying state and requires authentication.
"""
pass
@pytest.mark.skip(reason="State-modifying operation that requires auth")
def test_queue_update_node():
"""
Test queuing a node update.
Skipped to avoid modifying state and requires authentication.
"""
pass
@pytest.mark.skip(reason="State-modifying operation that requires auth")
def test_queue_uninstall_node():
"""
Test queuing a node uninstallation.
Skipped to avoid modifying state and requires authentication.
"""
pass
@pytest.mark.skip(reason="State-modifying operation")
def test_queue_start():
"""
Test starting the queue.
Skipped to avoid modifying state.
"""
pass
class TestQueueOperations:
"""
Test a complete queue workflow.
These tests are grouped to ensure proper sequencing but are still skipped
to avoid modifying state in automated tests.
"""
@pytest.fixture(scope="class")
def node_data(self) -> Dict:
"""
Create test data for a node operation.
"""
# This would be replaced with actual data for a known safe node
return {
"ui_id": "test_node_1",
"id": "comfyui-manager", # Manager itself
"version": "latest",
"channel": "default",
"mode": "local",
}
@pytest.mark.skip(reason="State-modifying operation")
def test_queue_operation_sequence(
self,
api_request: Callable,
node_data: Dict
):
"""
Test the queue operation sequence.
"""
# 1. Reset the queue
api_request(
method="get",
path="/manager/queue/reset",
expected_status=200,
)
# 2. Queue a node operation (we'll use the manager itself)
api_request(
method="post",
path="/manager/queue/update",
json_data=node_data,
expected_status=200,
)
# 3. Check queue status - should have one operation
response, json_data = api_request(
method="get",
path="/manager/queue/status",
expected_status=200,
)
assert json_data["total_count"] > 0
assert not json_data["is_processing"] # Queue hasn't started yet
# 4. Start the queue
api_request(
method="get",
path="/manager/queue/start",
expected_status=200,
)
# 5. Check queue status again - should be processing
response, json_data = api_request(
method="get",
path="/manager/queue/status",
expected_status=200,
)
# Queue should be processing or already done
assert json_data["is_processing"] or json_data["done_count"] == json_data["total_count"]
# 6. Wait for queue to complete (with timeout)
max_wait_time = 60 # seconds
start_time = time.time()
completed = False
while time.time() - start_time < max_wait_time:
response, json_data = api_request(
method="get",
path="/manager/queue/status",
expected_status=200,
)
if json_data["done_count"] == json_data["total_count"] and not json_data["is_processing"]:
completed = True
break
time.sleep(2) # Wait before checking again
assert completed, "Queue did not complete within timeout period"
@pytest.mark.skip(reason="State-modifying operation")
def test_concurrent_queue_operations(
self,
api_request: Callable,
node_data: Dict
):
"""
Test concurrent queue operations.
"""
# This would test adding multiple operations to the queue
# and verifying they all complete correctly
pass

View File

@@ -1,198 +0,0 @@
"""
Tests for snapshot management endpoints.
"""
import pytest
import time
from datetime import datetime
from pathlib import Path
from typing import Callable, Dict, List, Optional
from utils.validation import validate_response
def test_get_snapshot_list(
api_request: Callable,
openapi_spec: Dict
):
"""
Test the endpoint for listing snapshots.
"""
# Make the API request
path = "/snapshot/getlist"
response, json_data = api_request(
method="get",
path=path,
expected_status=200,
)
# Validate response structure against the schema
assert json_data is not None
validate_response(
response_data=json_data,
path=path,
method="get",
spec=openapi_spec,
)
# Verify the response contains the expected fields
assert "items" in json_data
assert isinstance(json_data["items"], list)
def test_get_current_snapshot(
api_request: Callable,
openapi_spec: Dict
):
"""
Test the endpoint for getting the current snapshot.
"""
# Make the API request
path = "/snapshot/get_current"
response, json_data = api_request(
method="get",
path=path,
expected_status=200,
)
# Validate response structure against the schema
assert json_data is not None
validate_response(
response_data=json_data,
path=path,
method="get",
spec=openapi_spec,
)
# Check for basic snapshot structure
assert "snapshot_date" in json_data
assert "custom_nodes" in json_data
@pytest.mark.skip(reason="This test creates a snapshot which is a state-modifying operation")
def test_save_snapshot(
api_request: Callable
):
"""
Test the endpoint for saving a new snapshot.
Skipped to avoid modifying state in tests.
"""
pass
@pytest.mark.skip(reason="This test removes a snapshot which is a destructive operation")
def test_remove_snapshot(
api_request: Callable
):
"""
Test the endpoint for removing a snapshot.
Skipped to avoid modifying state in tests.
"""
pass
@pytest.mark.skip(reason="This test restores a snapshot which is a state-modifying operation")
def test_restore_snapshot(
api_request: Callable
):
"""
Test the endpoint for restoring a snapshot.
Skipped to avoid modifying state in tests.
"""
pass
class TestSnapshotWorkflow:
"""
Test the complete snapshot workflow (create, list, get, remove).
These tests are grouped to ensure proper sequencing but are still skipped
to avoid modifying state in automated tests.
"""
@pytest.fixture(scope="class")
def snapshot_name(self) -> str:
"""
Generate a unique snapshot name for testing.
"""
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
return f"test_snapshot_{timestamp}"
@pytest.mark.skip(reason="State-modifying test")
def test_create_snapshot(
self,
api_request: Callable,
snapshot_name: str
):
"""
Test creating a snapshot.
"""
# Make the API request to save a snapshot
response, _ = api_request(
method="get",
path="/snapshot/save",
expected_status=200,
)
# Verify a snapshot was created (would need to check the snapshot list)
response2, json_data = api_request(
method="get",
path="/snapshot/getlist",
expected_status=200,
)
# The most recently created snapshot should be first in the list
assert json_data["items"]
# Store the snapshot name for later tests
self.actual_snapshot_name = json_data["items"][0]
@pytest.mark.skip(reason="State-modifying test")
def test_get_snapshot_details(
self,
api_request: Callable,
openapi_spec: Dict
):
"""
Test getting details of the created snapshot.
"""
# This would check the current snapshot, not a specific one
# since there's no direct API to get a specific snapshot
response, json_data = api_request(
method="get",
path="/snapshot/get_current",
expected_status=200,
)
# Validate the snapshot data
assert json_data is not None
validate_response(
response_data=json_data,
path="/snapshot/get_current",
method="get",
spec=openapi_spec,
)
@pytest.mark.skip(reason="State-modifying test")
def test_remove_test_snapshot(
self,
api_request: Callable
):
"""
Test removing the test snapshot.
"""
# Make the API request to remove the snapshot
response, _ = api_request(
method="get",
path="/snapshot/remove",
params={"target": self.actual_snapshot_name},
expected_status=200,
)
# Verify the snapshot was removed
response2, json_data = api_request(
method="get",
path="/snapshot/getlist",
expected_status=200,
)
# The snapshot should no longer be in the list
assert self.actual_snapshot_name not in json_data["items"]

View File

@@ -1,150 +0,0 @@
"""
Tests for validating the OpenAPI specification.
"""
import json
import pytest
import yaml
from typing import Dict, Any, List, Tuple
from pathlib import Path
from openapi_spec_validator import validate_spec
from utils.validation import load_openapi_spec
from utils.schema_utils import (
get_all_paths,
get_methods_for_path,
find_paths_with_security,
get_required_parameters
)
def test_spec_is_valid():
"""
Test that the OpenAPI specification is valid according to the spec validator.
"""
spec = load_openapi_spec()
validate_spec(spec)
def test_spec_has_info():
"""
Test that the OpenAPI specification has basic info.
"""
spec = load_openapi_spec()
assert "info" in spec
assert "title" in spec["info"]
assert "version" in spec["info"]
assert spec["info"]["title"] == "ComfyUI-Manager API"
def test_spec_has_paths():
"""
Test that the OpenAPI specification has paths defined.
"""
spec = load_openapi_spec()
assert "paths" in spec
assert len(spec["paths"]) > 0
def test_paths_have_responses():
"""
Test that all paths have responses defined.
"""
spec = load_openapi_spec()
for path, path_item in spec["paths"].items():
for method, operation in path_item.items():
if method.lower() not in {"get", "post", "put", "delete", "patch", "options", "head"}:
continue
assert "responses" in operation, f"Path {path} method {method} has no responses"
assert len(operation["responses"]) > 0, f"Path {path} method {method} has empty responses"
def test_responses_have_schemas():
"""
Test that responses with application/json content type have schemas.
"""
spec = load_openapi_spec()
for path, path_item in spec["paths"].items():
for method, operation in path_item.items():
if method.lower() not in {"get", "post", "put", "delete", "patch", "options", "head"}:
continue
for status, response in operation["responses"].items():
if "content" not in response:
continue
if "application/json" in response["content"]:
assert "schema" in response["content"]["application/json"], (
f"Path {path} method {method} status {status} "
f"application/json content has no schema"
)
def test_required_parameters_have_schemas():
"""
Test that all required parameters have schemas.
"""
spec = load_openapi_spec()
for path, path_item in spec["paths"].items():
for method, operation in path_item.items():
if method.lower() not in {"get", "post", "put", "delete", "patch", "options", "head"}:
continue
if "parameters" not in operation:
continue
for param in operation["parameters"]:
if param.get("required", False):
assert "schema" in param, (
f"Path {path} method {method} required parameter {param.get('name')} has no schema"
)
def test_security_schemes_defined():
"""
Test that security schemes are properly defined.
"""
spec = load_openapi_spec()
# Get paths requiring security
secure_paths = find_paths_with_security(spec)
if secure_paths:
assert "components" in spec, "Spec has secure paths but no components"
assert "securitySchemes" in spec["components"], "Spec has secure paths but no securitySchemes"
# Check each security reference is defined
for path, method in secure_paths:
operation = spec["paths"][path][method]
for security_req in operation["security"]:
for scheme_name in security_req:
assert scheme_name in spec["components"]["securitySchemes"], (
f"Security scheme {scheme_name} used by {method.upper()} {path} "
f"is not defined in components.securitySchemes"
)
def test_common_endpoint_groups_present():
"""
Test that the spec includes the main endpoint groups.
"""
spec = load_openapi_spec()
paths = get_all_paths(spec)
# Define the expected endpoint prefixes
expected_prefixes = [
"/customnode/",
"/externalmodel/",
"/manager/",
"/snapshot/",
"/comfyui_manager/",
]
# Check that at least one path exists for each expected prefix
for prefix in expected_prefixes:
matching_paths = [p for p in paths if p.startswith(prefix)]
assert matching_paths, f"No endpoints found with prefix {prefix}"

View File

@@ -1 +0,0 @@
# Make utils directory a proper package

View File

@@ -1,174 +0,0 @@
"""
Schema utilities for extracting and manipulating OpenAPI schemas.
"""
import json
from pathlib import Path
from typing import Any, Dict, List, Optional, Set, Tuple
from .validation import load_openapi_spec
def get_all_paths(spec: Dict[str, Any]) -> List[str]:
"""
Get all paths defined in the OpenAPI specification.
Args:
spec: The OpenAPI specification
Returns:
List of all paths
"""
return list(spec.get("paths", {}).keys())
def get_grouped_paths(spec: Dict[str, Any]) -> Dict[str, List[str]]:
"""
Group paths by their top-level segment.
Args:
spec: The OpenAPI specification
Returns:
Dictionary mapping top-level segments to lists of paths
"""
result = {}
for path in get_all_paths(spec):
segments = path.strip("/").split("/")
if not segments:
continue
top_segment = segments[0]
if top_segment not in result:
result[top_segment] = []
result[top_segment].append(path)
return result
def get_methods_for_path(spec: Dict[str, Any], path: str) -> List[str]:
"""
Get all HTTP methods defined for a path.
Args:
spec: The OpenAPI specification
path: The API path
Returns:
List of HTTP methods (lowercase)
"""
if path not in spec.get("paths", {}):
return []
return [
method.lower()
for method in spec["paths"][path].keys()
if method.lower() in {"get", "post", "put", "delete", "patch", "options", "head"}
]
def find_paths_with_security(
spec: Dict[str, Any],
security_scheme: Optional[str] = None
) -> List[Tuple[str, str]]:
"""
Find all paths that require security.
Args:
spec: The OpenAPI specification
security_scheme: Optional specific security scheme to filter by
Returns:
List of (path, method) tuples that require security
"""
result = []
for path, path_item in spec.get("paths", {}).items():
for method, operation in path_item.items():
if method.lower() not in {"get", "post", "put", "delete", "patch", "options", "head"}:
continue
if "security" in operation:
if security_scheme is None:
result.append((path, method.lower()))
else:
# Check if this security scheme is required
for security_req in operation["security"]:
if security_scheme in security_req:
result.append((path, method.lower()))
break
return result
def get_content_types_for_response(
spec: Dict[str, Any],
path: str,
method: str,
status_code: str = "200"
) -> List[str]:
"""
Get content types defined for a response.
Args:
spec: The OpenAPI specification
path: The API path
method: The HTTP method
status_code: The HTTP status code
Returns:
List of content types
"""
method = method.lower()
if path not in spec["paths"]:
return []
if method not in spec["paths"][path]:
return []
if "responses" not in spec["paths"][path][method]:
return []
if status_code not in spec["paths"][path][method]["responses"]:
return []
response_def = spec["paths"][path][method]["responses"][status_code]
if "content" not in response_def:
return []
return list(response_def["content"].keys())
def get_required_parameters(
spec: Dict[str, Any],
path: str,
method: str
) -> List[Dict[str, Any]]:
"""
Get all required parameters for a path/method.
Args:
spec: The OpenAPI specification
path: The API path
method: The HTTP method
Returns:
List of parameter objects that are required
"""
method = method.lower()
if path not in spec["paths"]:
return []
if method not in spec["paths"][path]:
return []
if "parameters" not in spec["paths"][path][method]:
return []
return [
param for param in spec["paths"][path][method]["parameters"]
if param.get("required", False)
]

View File

@@ -1,155 +0,0 @@
"""
Validation utilities for API tests.
"""
import json
import jsonschema
import yaml
from pathlib import Path
from typing import Any, Dict, Optional, Union
def load_openapi_spec(spec_path: Union[str, Path] = None) -> Dict[str, Any]:
"""
Load the OpenAPI specification document.
Args:
spec_path: Path to the OpenAPI specification file
Returns:
The OpenAPI specification as a dictionary
"""
if spec_path is None:
# Default to the root openapi.yaml file
spec_path = Path(__file__).parents[2] / "openapi.yaml"
with open(spec_path, "r") as f:
if str(spec_path).endswith(".yaml") or str(spec_path).endswith(".yml"):
return yaml.safe_load(f)
else:
return json.load(f)
def get_schema_for_path(
spec: Dict[str, Any],
path: str,
method: str,
status_code: str = "200",
content_type: str = "application/json"
) -> Optional[Dict[str, Any]]:
"""
Extract the response schema for a specific path, method, and status code.
Args:
spec: The OpenAPI specification
path: The API path (e.g., "/customnode/getlist")
method: The HTTP method (e.g., "get", "post")
status_code: The HTTP status code (default: "200")
content_type: The response content type (default: "application/json")
Returns:
The schema for the specified path and method, or None if not found
"""
method = method.lower()
if path not in spec["paths"]:
return None
if method not in spec["paths"][path]:
return None
if "responses" not in spec["paths"][path][method]:
return None
if status_code not in spec["paths"][path][method]["responses"]:
return None
response_def = spec["paths"][path][method]["responses"][status_code]
if "content" not in response_def:
return None
if content_type not in response_def["content"]:
return None
if "schema" not in response_def["content"][content_type]:
return None
return response_def["content"][content_type]["schema"]
def validate_response_schema(
response_data: Any,
schema: Dict[str, Any],
spec: Dict[str, Any] = None
) -> bool:
"""
Validate a response against a schema from the OpenAPI specification.
Args:
response_data: The response data to validate
schema: The schema to validate against
spec: The complete OpenAPI specification (for resolving references)
Returns:
True if validation succeeds, raises an exception otherwise
"""
if spec is None:
spec = load_openapi_spec()
# Create a resolver for references within the schema
resolver = jsonschema.RefResolver.from_schema(spec)
# Validate the response against the schema
jsonschema.validate(
instance=response_data,
schema=schema,
resolver=resolver
)
return True
def validate_response(
response_data: Any,
path: str,
method: str,
status_code: str = "200",
content_type: str = "application/json",
spec: Dict[str, Any] = None
) -> bool:
"""
Validate a response against the schema defined in the OpenAPI specification.
Args:
response_data: The response data to validate
path: The API path
method: The HTTP method
status_code: The HTTP status code (default: "200")
content_type: The response content type (default: "application/json")
spec: The OpenAPI specification (loaded from default location if None)
Returns:
True if validation succeeds, raises an exception otherwise
"""
if spec is None:
spec = load_openapi_spec()
schema = get_schema_for_path(
spec=spec,
path=path,
method=method,
status_code=status_code,
content_type=content_type
)
if schema is None:
raise ValueError(
f"No schema found for {method.upper()} {path} "
f"with status {status_code} and content type {content_type}"
)
return validate_response_schema(
response_data=response_data,
schema=schema,
spec=spec
)