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.
Files changed (93) hide show
  1. comfygit_core/analyzers/custom_node_scanner.py +109 -0
  2. comfygit_core/analyzers/git_change_parser.py +156 -0
  3. comfygit_core/analyzers/model_scanner.py +318 -0
  4. comfygit_core/analyzers/node_classifier.py +58 -0
  5. comfygit_core/analyzers/node_git_analyzer.py +77 -0
  6. comfygit_core/analyzers/status_scanner.py +362 -0
  7. comfygit_core/analyzers/workflow_dependency_parser.py +143 -0
  8. comfygit_core/caching/__init__.py +16 -0
  9. comfygit_core/caching/api_cache.py +210 -0
  10. comfygit_core/caching/base.py +212 -0
  11. comfygit_core/caching/comfyui_cache.py +100 -0
  12. comfygit_core/caching/custom_node_cache.py +320 -0
  13. comfygit_core/caching/workflow_cache.py +797 -0
  14. comfygit_core/clients/__init__.py +4 -0
  15. comfygit_core/clients/civitai_client.py +412 -0
  16. comfygit_core/clients/github_client.py +349 -0
  17. comfygit_core/clients/registry_client.py +230 -0
  18. comfygit_core/configs/comfyui_builtin_nodes.py +1614 -0
  19. comfygit_core/configs/comfyui_models.py +62 -0
  20. comfygit_core/configs/model_config.py +151 -0
  21. comfygit_core/constants.py +82 -0
  22. comfygit_core/core/environment.py +1635 -0
  23. comfygit_core/core/workspace.py +898 -0
  24. comfygit_core/factories/environment_factory.py +419 -0
  25. comfygit_core/factories/uv_factory.py +61 -0
  26. comfygit_core/factories/workspace_factory.py +109 -0
  27. comfygit_core/infrastructure/sqlite_manager.py +156 -0
  28. comfygit_core/integrations/__init__.py +7 -0
  29. comfygit_core/integrations/uv_command.py +318 -0
  30. comfygit_core/logging/logging_config.py +15 -0
  31. comfygit_core/managers/environment_git_orchestrator.py +316 -0
  32. comfygit_core/managers/environment_model_manager.py +296 -0
  33. comfygit_core/managers/export_import_manager.py +116 -0
  34. comfygit_core/managers/git_manager.py +667 -0
  35. comfygit_core/managers/model_download_manager.py +252 -0
  36. comfygit_core/managers/model_symlink_manager.py +166 -0
  37. comfygit_core/managers/node_manager.py +1378 -0
  38. comfygit_core/managers/pyproject_manager.py +1321 -0
  39. comfygit_core/managers/user_content_symlink_manager.py +436 -0
  40. comfygit_core/managers/uv_project_manager.py +569 -0
  41. comfygit_core/managers/workflow_manager.py +1944 -0
  42. comfygit_core/models/civitai.py +432 -0
  43. comfygit_core/models/commit.py +18 -0
  44. comfygit_core/models/environment.py +293 -0
  45. comfygit_core/models/exceptions.py +378 -0
  46. comfygit_core/models/manifest.py +132 -0
  47. comfygit_core/models/node_mapping.py +201 -0
  48. comfygit_core/models/protocols.py +248 -0
  49. comfygit_core/models/registry.py +63 -0
  50. comfygit_core/models/shared.py +356 -0
  51. comfygit_core/models/sync.py +42 -0
  52. comfygit_core/models/system.py +204 -0
  53. comfygit_core/models/workflow.py +914 -0
  54. comfygit_core/models/workspace_config.py +71 -0
  55. comfygit_core/py.typed +0 -0
  56. comfygit_core/repositories/migrate_paths.py +49 -0
  57. comfygit_core/repositories/model_repository.py +958 -0
  58. comfygit_core/repositories/node_mappings_repository.py +246 -0
  59. comfygit_core/repositories/workflow_repository.py +57 -0
  60. comfygit_core/repositories/workspace_config_repository.py +121 -0
  61. comfygit_core/resolvers/global_node_resolver.py +459 -0
  62. comfygit_core/resolvers/model_resolver.py +250 -0
  63. comfygit_core/services/import_analyzer.py +218 -0
  64. comfygit_core/services/model_downloader.py +422 -0
  65. comfygit_core/services/node_lookup_service.py +251 -0
  66. comfygit_core/services/registry_data_manager.py +161 -0
  67. comfygit_core/strategies/__init__.py +4 -0
  68. comfygit_core/strategies/auto.py +72 -0
  69. comfygit_core/strategies/confirmation.py +69 -0
  70. comfygit_core/utils/comfyui_ops.py +125 -0
  71. comfygit_core/utils/common.py +164 -0
  72. comfygit_core/utils/conflict_parser.py +232 -0
  73. comfygit_core/utils/dependency_parser.py +231 -0
  74. comfygit_core/utils/download.py +216 -0
  75. comfygit_core/utils/environment_cleanup.py +111 -0
  76. comfygit_core/utils/filesystem.py +178 -0
  77. comfygit_core/utils/git.py +1184 -0
  78. comfygit_core/utils/input_signature.py +145 -0
  79. comfygit_core/utils/model_categories.py +52 -0
  80. comfygit_core/utils/pytorch.py +71 -0
  81. comfygit_core/utils/requirements.py +211 -0
  82. comfygit_core/utils/retry.py +242 -0
  83. comfygit_core/utils/symlink_utils.py +119 -0
  84. comfygit_core/utils/system_detector.py +258 -0
  85. comfygit_core/utils/uuid.py +28 -0
  86. comfygit_core/utils/uv_error_handler.py +158 -0
  87. comfygit_core/utils/version.py +73 -0
  88. comfygit_core/utils/workflow_hash.py +90 -0
  89. comfygit_core/validation/resolution_tester.py +297 -0
  90. comfygit_core-0.2.0.dist-info/METADATA +939 -0
  91. comfygit_core-0.2.0.dist-info/RECORD +93 -0
  92. comfygit_core-0.2.0.dist-info/WHEEL +4 -0
  93. 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
+ ]