comfygit-core 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- comfygit_core/analyzers/custom_node_scanner.py +109 -0
- comfygit_core/analyzers/git_change_parser.py +156 -0
- comfygit_core/analyzers/model_scanner.py +318 -0
- comfygit_core/analyzers/node_classifier.py +58 -0
- comfygit_core/analyzers/node_git_analyzer.py +77 -0
- comfygit_core/analyzers/status_scanner.py +362 -0
- comfygit_core/analyzers/workflow_dependency_parser.py +143 -0
- comfygit_core/caching/__init__.py +16 -0
- comfygit_core/caching/api_cache.py +210 -0
- comfygit_core/caching/base.py +212 -0
- comfygit_core/caching/comfyui_cache.py +100 -0
- comfygit_core/caching/custom_node_cache.py +320 -0
- comfygit_core/caching/workflow_cache.py +797 -0
- comfygit_core/clients/__init__.py +4 -0
- comfygit_core/clients/civitai_client.py +412 -0
- comfygit_core/clients/github_client.py +349 -0
- comfygit_core/clients/registry_client.py +230 -0
- comfygit_core/configs/comfyui_builtin_nodes.py +1614 -0
- comfygit_core/configs/comfyui_models.py +62 -0
- comfygit_core/configs/model_config.py +151 -0
- comfygit_core/constants.py +82 -0
- comfygit_core/core/environment.py +1635 -0
- comfygit_core/core/workspace.py +898 -0
- comfygit_core/factories/environment_factory.py +419 -0
- comfygit_core/factories/uv_factory.py +61 -0
- comfygit_core/factories/workspace_factory.py +109 -0
- comfygit_core/infrastructure/sqlite_manager.py +156 -0
- comfygit_core/integrations/__init__.py +7 -0
- comfygit_core/integrations/uv_command.py +318 -0
- comfygit_core/logging/logging_config.py +15 -0
- comfygit_core/managers/environment_git_orchestrator.py +316 -0
- comfygit_core/managers/environment_model_manager.py +296 -0
- comfygit_core/managers/export_import_manager.py +116 -0
- comfygit_core/managers/git_manager.py +667 -0
- comfygit_core/managers/model_download_manager.py +252 -0
- comfygit_core/managers/model_symlink_manager.py +166 -0
- comfygit_core/managers/node_manager.py +1378 -0
- comfygit_core/managers/pyproject_manager.py +1321 -0
- comfygit_core/managers/user_content_symlink_manager.py +436 -0
- comfygit_core/managers/uv_project_manager.py +569 -0
- comfygit_core/managers/workflow_manager.py +1944 -0
- comfygit_core/models/civitai.py +432 -0
- comfygit_core/models/commit.py +18 -0
- comfygit_core/models/environment.py +293 -0
- comfygit_core/models/exceptions.py +378 -0
- comfygit_core/models/manifest.py +132 -0
- comfygit_core/models/node_mapping.py +201 -0
- comfygit_core/models/protocols.py +248 -0
- comfygit_core/models/registry.py +63 -0
- comfygit_core/models/shared.py +356 -0
- comfygit_core/models/sync.py +42 -0
- comfygit_core/models/system.py +204 -0
- comfygit_core/models/workflow.py +914 -0
- comfygit_core/models/workspace_config.py +71 -0
- comfygit_core/py.typed +0 -0
- comfygit_core/repositories/migrate_paths.py +49 -0
- comfygit_core/repositories/model_repository.py +958 -0
- comfygit_core/repositories/node_mappings_repository.py +246 -0
- comfygit_core/repositories/workflow_repository.py +57 -0
- comfygit_core/repositories/workspace_config_repository.py +121 -0
- comfygit_core/resolvers/global_node_resolver.py +459 -0
- comfygit_core/resolvers/model_resolver.py +250 -0
- comfygit_core/services/import_analyzer.py +218 -0
- comfygit_core/services/model_downloader.py +422 -0
- comfygit_core/services/node_lookup_service.py +251 -0
- comfygit_core/services/registry_data_manager.py +161 -0
- comfygit_core/strategies/__init__.py +4 -0
- comfygit_core/strategies/auto.py +72 -0
- comfygit_core/strategies/confirmation.py +69 -0
- comfygit_core/utils/comfyui_ops.py +125 -0
- comfygit_core/utils/common.py +164 -0
- comfygit_core/utils/conflict_parser.py +232 -0
- comfygit_core/utils/dependency_parser.py +231 -0
- comfygit_core/utils/download.py +216 -0
- comfygit_core/utils/environment_cleanup.py +111 -0
- comfygit_core/utils/filesystem.py +178 -0
- comfygit_core/utils/git.py +1184 -0
- comfygit_core/utils/input_signature.py +145 -0
- comfygit_core/utils/model_categories.py +52 -0
- comfygit_core/utils/pytorch.py +71 -0
- comfygit_core/utils/requirements.py +211 -0
- comfygit_core/utils/retry.py +242 -0
- comfygit_core/utils/symlink_utils.py +119 -0
- comfygit_core/utils/system_detector.py +258 -0
- comfygit_core/utils/uuid.py +28 -0
- comfygit_core/utils/uv_error_handler.py +158 -0
- comfygit_core/utils/version.py +73 -0
- comfygit_core/utils/workflow_hash.py +90 -0
- comfygit_core/validation/resolution_tester.py +297 -0
- comfygit_core-0.2.0.dist-info/METADATA +939 -0
- comfygit_core-0.2.0.dist-info/RECORD +93 -0
- comfygit_core-0.2.0.dist-info/WHEEL +4 -0
- comfygit_core-0.2.0.dist-info/licenses/LICENSE.txt +661 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Git information extraction for custom nodes."""
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from ..logging.logging_config import get_logger
|
|
5
|
+
from ..models.environment import GitInfo
|
|
6
|
+
from ..utils.git import (
|
|
7
|
+
git_describe_tags,
|
|
8
|
+
git_remote_get_url,
|
|
9
|
+
git_rev_parse,
|
|
10
|
+
git_status_porcelain,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
logger = get_logger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_node_git_info(node_path: Path) -> GitInfo | None:
|
|
17
|
+
"""Get git repository information for a custom node.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
node_path: Path to the custom node directory
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
GitInfo with git information or None if not a git repository
|
|
24
|
+
"""
|
|
25
|
+
import re
|
|
26
|
+
|
|
27
|
+
git_info = GitInfo()
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
# Check if it's a git repository
|
|
31
|
+
git_dir = node_path / ".git"
|
|
32
|
+
if not git_dir.exists():
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
# Get current commit hash
|
|
36
|
+
commit = git_rev_parse(node_path, "HEAD")
|
|
37
|
+
if commit:
|
|
38
|
+
git_info.commit = commit
|
|
39
|
+
|
|
40
|
+
# Get current branch
|
|
41
|
+
branch = git_rev_parse(node_path, "HEAD", abbrev_ref=True)
|
|
42
|
+
if branch and branch != "HEAD":
|
|
43
|
+
git_info.branch = branch
|
|
44
|
+
|
|
45
|
+
# Try to get current tag/version
|
|
46
|
+
tag = git_describe_tags(node_path, exact_match=True)
|
|
47
|
+
if tag:
|
|
48
|
+
git_info.tag = tag
|
|
49
|
+
else:
|
|
50
|
+
# Try to get the most recent tag
|
|
51
|
+
tag = git_describe_tags(node_path, abbrev=0)
|
|
52
|
+
if tag:
|
|
53
|
+
git_info.tag = tag
|
|
54
|
+
|
|
55
|
+
# Get remote URL
|
|
56
|
+
remote_url = git_remote_get_url(node_path)
|
|
57
|
+
if remote_url:
|
|
58
|
+
git_info.remote_url = remote_url
|
|
59
|
+
|
|
60
|
+
# Extract GitHub info if it's a GitHub URL
|
|
61
|
+
github_match = re.match(
|
|
62
|
+
r"(?:https?://github\.com/|git@github\.com:)([^/]+)/([^/\.]+)",
|
|
63
|
+
remote_url,
|
|
64
|
+
)
|
|
65
|
+
if github_match:
|
|
66
|
+
git_info.github_owner = github_match.group(1)
|
|
67
|
+
git_info.github_repo = github_match.group(2).replace(".git", "")
|
|
68
|
+
|
|
69
|
+
# Check if there are uncommitted changes
|
|
70
|
+
status_entries = git_status_porcelain(node_path)
|
|
71
|
+
git_info.is_dirty = bool(status_entries)
|
|
72
|
+
|
|
73
|
+
return git_info
|
|
74
|
+
|
|
75
|
+
except Exception as e:
|
|
76
|
+
logger.warning(f"Error getting git info for {node_path}: {e}")
|
|
77
|
+
return None
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
# core/status_scanner.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from comfygit_core.constants import CUSTOM_NODES_BLACKLIST
|
|
9
|
+
|
|
10
|
+
from ..logging.logging_config import get_logger
|
|
11
|
+
from ..models.environment import (
|
|
12
|
+
EnvironmentComparison,
|
|
13
|
+
PackageSyncStatus,
|
|
14
|
+
NodeState,
|
|
15
|
+
EnvironmentState,
|
|
16
|
+
)
|
|
17
|
+
from ..models.exceptions import UVCommandError
|
|
18
|
+
from .node_git_analyzer import get_node_git_info
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from ..managers.pyproject_manager import PyprojectManager
|
|
22
|
+
from ..managers.uv_project_manager import UVProjectManager
|
|
23
|
+
|
|
24
|
+
logger = get_logger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class StatusScanner:
|
|
28
|
+
"""Scans environment to get current state."""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
uv: UVProjectManager,
|
|
33
|
+
pyproject: PyprojectManager,
|
|
34
|
+
venv_path: Path,
|
|
35
|
+
comfyui_path: Path,
|
|
36
|
+
):
|
|
37
|
+
self._uv = uv
|
|
38
|
+
self._pyproject = pyproject
|
|
39
|
+
self._venv_path = venv_path
|
|
40
|
+
self._comfyui_path = comfyui_path
|
|
41
|
+
|
|
42
|
+
def get_full_comparison(self) -> EnvironmentComparison:
|
|
43
|
+
"""Get complete environment comparison with all details.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
comfyui_path: Path to ComfyUI directory
|
|
47
|
+
venv_path: Path to virtual environment
|
|
48
|
+
uv: UV interface for package operations
|
|
49
|
+
pyproject: PyprojectManager instance
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Complete environment comparison
|
|
53
|
+
"""
|
|
54
|
+
# Scan current and expected states
|
|
55
|
+
current = self.scan_environment()
|
|
56
|
+
expected = self.scan_manifest()
|
|
57
|
+
|
|
58
|
+
# Get basic comparison
|
|
59
|
+
comparison = self.compare_states(current, expected)
|
|
60
|
+
|
|
61
|
+
# Skip package sync check for performance (100-500ms saved)
|
|
62
|
+
# Rationale:
|
|
63
|
+
# - Users rarely manually modify .venv/
|
|
64
|
+
# - Operations like 'run', 'repair', 'node add' auto-sync before executing
|
|
65
|
+
# - Status is high-frequency with workflow caching - needs to be fast
|
|
66
|
+
# - Package drift self-corrects on next sync operation
|
|
67
|
+
# If thorough check needed, use 'comfygit repair --dry-run' (future)
|
|
68
|
+
# TODO: Add package sync status
|
|
69
|
+
# package_status = self.check_packages_sync()
|
|
70
|
+
comparison.packages_in_sync = True #package_status.in_sync
|
|
71
|
+
comparison.package_sync_message = "" #package_status.message
|
|
72
|
+
|
|
73
|
+
return comparison
|
|
74
|
+
|
|
75
|
+
def scan_environment(self) -> EnvironmentState:
|
|
76
|
+
"""Scan the environment for its current state.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
comfyui_path: Path to ComfyUI directory
|
|
80
|
+
venv_path: Path to virtual environment
|
|
81
|
+
uv: UV interface for package operations
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Current environment state
|
|
85
|
+
"""
|
|
86
|
+
# Scan custom nodes
|
|
87
|
+
custom_nodes = self._scan_custom_nodes()
|
|
88
|
+
|
|
89
|
+
# Get installed packages
|
|
90
|
+
# packages = self._scan_packages()
|
|
91
|
+
|
|
92
|
+
# Get Python version
|
|
93
|
+
# python_version = self._get_python_version()
|
|
94
|
+
|
|
95
|
+
return EnvironmentState(
|
|
96
|
+
custom_nodes=custom_nodes,
|
|
97
|
+
packages=None,#packages,
|
|
98
|
+
python_version=None,#python_version
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def _scan_custom_nodes(self) -> dict[str, NodeState]:
|
|
102
|
+
"""Scan the custom_nodes directory."""
|
|
103
|
+
nodes = {}
|
|
104
|
+
custom_nodes_path = self._comfyui_path / "custom_nodes"
|
|
105
|
+
|
|
106
|
+
if not custom_nodes_path.exists():
|
|
107
|
+
logger.debug("custom_nodes directory not found")
|
|
108
|
+
return nodes
|
|
109
|
+
|
|
110
|
+
# Skip these directories
|
|
111
|
+
skip_dirs = CUSTOM_NODES_BLACKLIST
|
|
112
|
+
|
|
113
|
+
# TODO: Support .comfygit_ignore
|
|
114
|
+
for node_dir in custom_nodes_path.iterdir():
|
|
115
|
+
if not node_dir.is_dir() or node_dir.name in skip_dirs:
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
# Skip hidden directories and disabled nodes
|
|
119
|
+
if node_dir.name.startswith(".") or node_dir.name.endswith(".disabled"):
|
|
120
|
+
continue
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
node_state = self._scan_single_node(node_dir)
|
|
124
|
+
nodes[node_dir.name] = node_state
|
|
125
|
+
except Exception as e:
|
|
126
|
+
logger.debug(f"Error scanning node {node_dir.name}: {e}")
|
|
127
|
+
# Still record it as present but with minimal info
|
|
128
|
+
nodes[node_dir.name] = NodeState(
|
|
129
|
+
name=node_dir.name,
|
|
130
|
+
path=node_dir,
|
|
131
|
+
disabled=node_dir.name.endswith(".disabled"),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
return nodes
|
|
135
|
+
|
|
136
|
+
def _scan_single_node(self, node_dir: Path) -> NodeState:
|
|
137
|
+
"""Scan a single custom node directory."""
|
|
138
|
+
state = NodeState(name=node_dir.name, path=node_dir)
|
|
139
|
+
|
|
140
|
+
# Check if is disabled (has .disabled appended to dir name)
|
|
141
|
+
if node_dir.name.endswith(".disabled"):
|
|
142
|
+
state.disabled = True
|
|
143
|
+
|
|
144
|
+
# Check for git info
|
|
145
|
+
if (node_dir / ".git").exists():
|
|
146
|
+
git_info = get_node_git_info(node_dir)
|
|
147
|
+
if git_info:
|
|
148
|
+
state.git_commit = git_info.commit
|
|
149
|
+
state.git_branch = git_info.branch
|
|
150
|
+
state.version = git_info.tag # Use git tag as version
|
|
151
|
+
state.is_dirty = git_info.is_dirty
|
|
152
|
+
|
|
153
|
+
# If no version from git, check pyproject.toml
|
|
154
|
+
if not state.version:
|
|
155
|
+
pyproject_path = node_dir / "pyproject.toml"
|
|
156
|
+
if pyproject_path.exists():
|
|
157
|
+
try:
|
|
158
|
+
import tomlkit
|
|
159
|
+
|
|
160
|
+
with open(pyproject_path, encoding='utf-8') as f:
|
|
161
|
+
data = tomlkit.load(f)
|
|
162
|
+
|
|
163
|
+
# Try different locations for version
|
|
164
|
+
project_section = data.get("project")
|
|
165
|
+
if project_section and isinstance(project_section, dict):
|
|
166
|
+
version = project_section.get("version")
|
|
167
|
+
if isinstance(version, str):
|
|
168
|
+
state.version = version
|
|
169
|
+
|
|
170
|
+
if not state.version:
|
|
171
|
+
tool_section = data.get("tool")
|
|
172
|
+
if tool_section and isinstance(tool_section, dict):
|
|
173
|
+
poetry_section = tool_section.get("poetry")
|
|
174
|
+
if poetry_section and isinstance(poetry_section, dict):
|
|
175
|
+
version = poetry_section.get("version")
|
|
176
|
+
if isinstance(version, str):
|
|
177
|
+
state.version = version
|
|
178
|
+
except Exception:
|
|
179
|
+
pass # Ignore parse errors
|
|
180
|
+
|
|
181
|
+
return state
|
|
182
|
+
|
|
183
|
+
def _scan_packages(self) -> dict[str, str]:
|
|
184
|
+
"""Get installed packages using UV."""
|
|
185
|
+
packages = {}
|
|
186
|
+
|
|
187
|
+
python_path = self._uv.python_executable
|
|
188
|
+
|
|
189
|
+
if not python_path.exists():
|
|
190
|
+
logger.warning(f"Python not found in venv: {self._venv_path}")
|
|
191
|
+
return packages
|
|
192
|
+
|
|
193
|
+
# TODO: Make this more robust
|
|
194
|
+
try:
|
|
195
|
+
output = self._uv.freeze_packages(python=python_path)
|
|
196
|
+
if output:
|
|
197
|
+
for line in output.strip().split("\n"):
|
|
198
|
+
if line and "==" in line and not line.startswith("#"):
|
|
199
|
+
if line.startswith("-e "):
|
|
200
|
+
# Skip editable installs for now
|
|
201
|
+
continue
|
|
202
|
+
name, version = line.split("==", 1)
|
|
203
|
+
packages[name.strip().lower()] = version.strip()
|
|
204
|
+
except Exception as e:
|
|
205
|
+
logger.warning(f"Failed to get packages: {e}")
|
|
206
|
+
|
|
207
|
+
return packages
|
|
208
|
+
|
|
209
|
+
def _get_python_version(self) -> str:
|
|
210
|
+
"""Get Python version from venv."""
|
|
211
|
+
python_path = self._uv.python_executable
|
|
212
|
+
|
|
213
|
+
if not python_path.exists():
|
|
214
|
+
return "unknown"
|
|
215
|
+
|
|
216
|
+
try:
|
|
217
|
+
result = subprocess.run(
|
|
218
|
+
[str(python_path), "--version"],
|
|
219
|
+
capture_output=True,
|
|
220
|
+
text=True,
|
|
221
|
+
timeout=5,
|
|
222
|
+
)
|
|
223
|
+
if result.returncode == 0:
|
|
224
|
+
# Parse "Python 3.11.5" -> "3.11.5"
|
|
225
|
+
return result.stdout.strip().split()[-1]
|
|
226
|
+
except Exception as e:
|
|
227
|
+
logger.debug(f"Failed to get Python version: {e}")
|
|
228
|
+
|
|
229
|
+
return "unknown"
|
|
230
|
+
|
|
231
|
+
def scan_manifest(self) -> EnvironmentState:
|
|
232
|
+
"""Scan expected state from pyproject.toml.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
pyproject: PyprojectManager instance
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
Expected environment state from configuration
|
|
239
|
+
"""
|
|
240
|
+
config = self._pyproject.load()
|
|
241
|
+
|
|
242
|
+
# Get expected custom nodes from pyproject
|
|
243
|
+
node_infos = self._pyproject.nodes.get_existing()
|
|
244
|
+
|
|
245
|
+
expected_nodes = {}
|
|
246
|
+
for _, node_info in node_infos.items():
|
|
247
|
+
expected_nodes[node_info.name] = NodeState(
|
|
248
|
+
name=node_info.name,
|
|
249
|
+
path=Path("custom_nodes") / node_info.name,
|
|
250
|
+
version=node_info.version,
|
|
251
|
+
source=node_info.source,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# # Get expected packages from dependency groups
|
|
255
|
+
# expected_packages = {}
|
|
256
|
+
# dependencies = config.get("dependency-groups", {})
|
|
257
|
+
|
|
258
|
+
# for _, deps in dependencies.items():
|
|
259
|
+
# for dep in deps:
|
|
260
|
+
# if isinstance(dep, str):
|
|
261
|
+
# # Parse package spec like "torch>=2.0.0"
|
|
262
|
+
# if "==" in dep:
|
|
263
|
+
# name, version = dep.split("==", 1)
|
|
264
|
+
# expected_packages[name.strip().lower()] = version.strip()
|
|
265
|
+
# elif ">=" in dep or "<=" in dep or ">" in dep or "<" in dep:
|
|
266
|
+
# # For now, just record that the package should be present
|
|
267
|
+
# name = dep.split(">")[0].split("<")[0].split("=")[0]
|
|
268
|
+
# expected_packages[name.strip().lower()] = "*"
|
|
269
|
+
# else:
|
|
270
|
+
# expected_packages[dep.strip().lower()] = "*"
|
|
271
|
+
|
|
272
|
+
# # Get Python version from project settings
|
|
273
|
+
# python_version = (
|
|
274
|
+
# config.get("project", {}).get("requires-python", "").strip(">=")
|
|
275
|
+
# )
|
|
276
|
+
# if not python_version:
|
|
277
|
+
# python_version = "3.11" # Default
|
|
278
|
+
|
|
279
|
+
return EnvironmentState(
|
|
280
|
+
custom_nodes=expected_nodes,
|
|
281
|
+
packages=None,#expected_packages,
|
|
282
|
+
python_version=None,#python_version,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
def compare_states(
|
|
286
|
+
self, current: EnvironmentState, expected: EnvironmentState
|
|
287
|
+
) -> EnvironmentComparison:
|
|
288
|
+
"""Compare current and expected environment states.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
current: Current environment state
|
|
292
|
+
expected: Expected environment state
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
Comparison results
|
|
296
|
+
"""
|
|
297
|
+
comparison = EnvironmentComparison()
|
|
298
|
+
|
|
299
|
+
# Compare custom nodes
|
|
300
|
+
current_nodes = set(current.custom_nodes.keys())
|
|
301
|
+
expected_nodes = set(expected.custom_nodes.keys())
|
|
302
|
+
|
|
303
|
+
comparison.missing_nodes = list(expected_nodes - current_nodes)
|
|
304
|
+
comparison.extra_nodes = list(current_nodes - expected_nodes)
|
|
305
|
+
|
|
306
|
+
# Check version mismatches (skip development nodes)
|
|
307
|
+
for name in current_nodes & expected_nodes:
|
|
308
|
+
current_node = current.custom_nodes[name]
|
|
309
|
+
expected_node = expected.custom_nodes[name]
|
|
310
|
+
|
|
311
|
+
# Skip version comparison for development nodes - they're user-managed
|
|
312
|
+
if expected_node.source == 'development':
|
|
313
|
+
continue
|
|
314
|
+
|
|
315
|
+
if expected_node.version and current_node.version != expected_node.version:
|
|
316
|
+
comparison.version_mismatches.append(
|
|
317
|
+
{
|
|
318
|
+
"name": name,
|
|
319
|
+
"expected": expected_node.version,
|
|
320
|
+
"actual": current_node.version,
|
|
321
|
+
}
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
# Detect potential dev node renames (simple heuristic)
|
|
325
|
+
if comparison.missing_nodes and comparison.extra_nodes:
|
|
326
|
+
missing_dev = any(
|
|
327
|
+
expected.custom_nodes[n].source == 'development'
|
|
328
|
+
for n in comparison.missing_nodes
|
|
329
|
+
if n in expected.custom_nodes
|
|
330
|
+
)
|
|
331
|
+
extra_git = any(
|
|
332
|
+
(self._comfyui_path / 'custom_nodes' / n / '.git').exists()
|
|
333
|
+
for n in comparison.extra_nodes
|
|
334
|
+
)
|
|
335
|
+
comparison.potential_dev_rename = missing_dev and extra_git
|
|
336
|
+
|
|
337
|
+
# Package comparison is handled separately since it requires UV
|
|
338
|
+
# This will be set by check_packages_sync
|
|
339
|
+
|
|
340
|
+
return comparison
|
|
341
|
+
|
|
342
|
+
def check_packages_sync(self) -> PackageSyncStatus:
|
|
343
|
+
"""Check if packages are in sync with pyproject.toml.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
uv: UV interface for package operations
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
Package sync status
|
|
350
|
+
"""
|
|
351
|
+
try:
|
|
352
|
+
# Use UV's dry-run to check if sync would change anything
|
|
353
|
+
self._uv.sync_project(dry_run=True, all_groups=True)
|
|
354
|
+
return PackageSyncStatus(
|
|
355
|
+
in_sync=True, message="Packages match pyproject.toml"
|
|
356
|
+
)
|
|
357
|
+
except UVCommandError as e:
|
|
358
|
+
return PackageSyncStatus(
|
|
359
|
+
in_sync=False,
|
|
360
|
+
message="Packages out of sync (run 'env sync' to update)",
|
|
361
|
+
details=str(e),
|
|
362
|
+
)
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Workflow dependency analysis and resolution manager."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import TYPE_CHECKING, Any, List
|
|
6
|
+
|
|
7
|
+
from comfygit_core.repositories.workflow_repository import WorkflowRepository
|
|
8
|
+
|
|
9
|
+
from ..logging.logging_config import get_logger
|
|
10
|
+
from .node_classifier import NodeClassifier
|
|
11
|
+
from ..configs.model_config import ModelConfig
|
|
12
|
+
from ..models.workflow import (
|
|
13
|
+
WorkflowNodeWidgetRef,
|
|
14
|
+
WorkflowNode,
|
|
15
|
+
WorkflowDependencies,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
logger = get_logger(__name__)
|
|
19
|
+
|
|
20
|
+
class WorkflowDependencyParser:
|
|
21
|
+
"""Manages workflow dependency analysis and resolution."""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
workflow_path: Path,
|
|
26
|
+
model_config: ModelConfig | None = None
|
|
27
|
+
):
|
|
28
|
+
|
|
29
|
+
self.model_config = model_config or ModelConfig.load()
|
|
30
|
+
|
|
31
|
+
# Load workflow
|
|
32
|
+
self.workflow = WorkflowRepository.load(workflow_path)
|
|
33
|
+
logger.debug(f"Loaded workflow '{workflow_path.stem}' with {len(self.workflow.nodes)} nodes")
|
|
34
|
+
|
|
35
|
+
# Store workflow name for pyproject lookup
|
|
36
|
+
self.workflow_name = workflow_path.stem
|
|
37
|
+
|
|
38
|
+
def analyze_dependencies(self) -> WorkflowDependencies:
|
|
39
|
+
"""Analyze workflow for model information and node types"""
|
|
40
|
+
try:
|
|
41
|
+
nodes_data = self.workflow.nodes
|
|
42
|
+
|
|
43
|
+
if not nodes_data:
|
|
44
|
+
logger.warning("No nodes found in workflow")
|
|
45
|
+
return WorkflowDependencies(workflow_name=self.workflow_name)
|
|
46
|
+
|
|
47
|
+
found_models: list[WorkflowNodeWidgetRef] = []
|
|
48
|
+
builtin_nodes: list[WorkflowNode] = []
|
|
49
|
+
missing_nodes: list[WorkflowNode] = []
|
|
50
|
+
|
|
51
|
+
# Analyze and resolve models and nodes
|
|
52
|
+
# Iterate over items() to preserve scoped IDs for subgraph nodes
|
|
53
|
+
for node_id, node_info in nodes_data.items():
|
|
54
|
+
node_classification = NodeClassifier.classify_single_node(node_info)
|
|
55
|
+
model_refs = self._extract_model_node_refs(node_id, node_info)
|
|
56
|
+
|
|
57
|
+
found_models.extend(model_refs)
|
|
58
|
+
|
|
59
|
+
if node_classification == 'builtin':
|
|
60
|
+
builtin_nodes.append(node_info)
|
|
61
|
+
else:
|
|
62
|
+
missing_nodes.append(node_info)
|
|
63
|
+
|
|
64
|
+
# Log results
|
|
65
|
+
if found_models:
|
|
66
|
+
logger.debug(f"Found {len(found_models)} model references in workflow")
|
|
67
|
+
if builtin_nodes:
|
|
68
|
+
logger.debug(f"Found {len(builtin_nodes)} builtin nodes in workflow")
|
|
69
|
+
if missing_nodes:
|
|
70
|
+
logger.debug(f"Found {len(missing_nodes)} missing nodes in workflow")
|
|
71
|
+
|
|
72
|
+
return WorkflowDependencies(
|
|
73
|
+
workflow_name=self.workflow_name,
|
|
74
|
+
found_models=found_models,
|
|
75
|
+
builtin_nodes=builtin_nodes,
|
|
76
|
+
non_builtin_nodes=missing_nodes
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
except Exception as e:
|
|
80
|
+
logger.error(f"Failed to analyze workflow dependencies: {e}")
|
|
81
|
+
return WorkflowDependencies(workflow_name=self.workflow_name)
|
|
82
|
+
|
|
83
|
+
def _extract_model_node_refs(self, node_id: str, node_info: WorkflowNode) -> List["WorkflowNodeWidgetRef"]:
|
|
84
|
+
"""Extract possible model references from a single node.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
node_id: Scoped node ID from workflow.nodes dict key (e.g., "uuid:12" for subgraph nodes)
|
|
88
|
+
node_info: WorkflowNode object containing node data
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
refs = []
|
|
92
|
+
|
|
93
|
+
# Handle multi-model nodes specially
|
|
94
|
+
if node_info.type == "CheckpointLoader":
|
|
95
|
+
# Index 0: checkpoint, Index 1: config
|
|
96
|
+
widgets = node_info.widgets_values or []
|
|
97
|
+
if len(widgets) > 0 and widgets[0]:
|
|
98
|
+
refs.append(WorkflowNodeWidgetRef(
|
|
99
|
+
node_id=node_id, # Use scoped ID from dict key
|
|
100
|
+
node_type=node_info.type,
|
|
101
|
+
widget_index=0,
|
|
102
|
+
widget_value=widgets[0]
|
|
103
|
+
))
|
|
104
|
+
if len(widgets) > 1 and widgets[1]:
|
|
105
|
+
refs.append(WorkflowNodeWidgetRef(
|
|
106
|
+
node_id=node_id, # Use scoped ID from dict key
|
|
107
|
+
node_type=node_info.type,
|
|
108
|
+
widget_index=1,
|
|
109
|
+
widget_value=widgets[1]
|
|
110
|
+
))
|
|
111
|
+
|
|
112
|
+
# Standard single-model loaders
|
|
113
|
+
elif self.model_config.is_model_loader_node(node_info.type):
|
|
114
|
+
widget_idx = self.model_config.get_widget_index_for_node(node_info.type)
|
|
115
|
+
widgets = node_info.widgets_values or []
|
|
116
|
+
if widget_idx < len(widgets) and widgets[widget_idx]:
|
|
117
|
+
refs.append(WorkflowNodeWidgetRef(
|
|
118
|
+
node_id=node_id, # Use scoped ID from dict key
|
|
119
|
+
node_type=node_info.type,
|
|
120
|
+
widget_index=widget_idx,
|
|
121
|
+
widget_value=widgets[widget_idx]
|
|
122
|
+
))
|
|
123
|
+
|
|
124
|
+
# Pattern match all widgets for custom nodes
|
|
125
|
+
else:
|
|
126
|
+
widgets = node_info.widgets_values or []
|
|
127
|
+
for idx, value in enumerate(widgets):
|
|
128
|
+
if self._looks_like_model(value):
|
|
129
|
+
refs.append(WorkflowNodeWidgetRef(
|
|
130
|
+
node_id=node_id, # Use scoped ID from dict key
|
|
131
|
+
node_type=node_info.type,
|
|
132
|
+
widget_index=idx,
|
|
133
|
+
widget_value=value
|
|
134
|
+
))
|
|
135
|
+
|
|
136
|
+
return refs
|
|
137
|
+
|
|
138
|
+
def _looks_like_model(self, value: Any) -> bool:
|
|
139
|
+
"""Check if value looks like a model path"""
|
|
140
|
+
if not isinstance(value, str):
|
|
141
|
+
return False
|
|
142
|
+
extensions = self.model_config.default_extensions
|
|
143
|
+
return any(value.endswith(ext) for ext in extensions)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Caching modules for ComfyDock."""
|
|
2
|
+
|
|
3
|
+
from .api_cache import APICacheManager
|
|
4
|
+
from .base import CacheBase, ContentCacheBase
|
|
5
|
+
from .comfyui_cache import ComfyUICacheManager, ComfyUISpec
|
|
6
|
+
from .custom_node_cache import CachedNodeInfo, CustomNodeCacheManager
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
'APICacheManager',
|
|
10
|
+
'CacheBase',
|
|
11
|
+
'ContentCacheBase',
|
|
12
|
+
'ComfyUICacheManager',
|
|
13
|
+
'ComfyUISpec',
|
|
14
|
+
'CustomNodeCacheManager',
|
|
15
|
+
'CachedNodeInfo',
|
|
16
|
+
]
|