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,293 @@
|
|
|
1
|
+
"""models/environment.py - Environment models for ComfyDock."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import List, TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from .workflow import DetailedWorkflowStatus
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from .manifest import ManifestModel
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class PackageSyncStatus:
|
|
16
|
+
"""Status of package synchronization."""
|
|
17
|
+
|
|
18
|
+
in_sync: bool
|
|
19
|
+
message: str
|
|
20
|
+
details: str | None = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class GitStatus:
|
|
25
|
+
"""Encapsulated git status information."""
|
|
26
|
+
|
|
27
|
+
has_changes: bool
|
|
28
|
+
current_branch: str | None = None # None = detached HEAD
|
|
29
|
+
has_other_changes: bool = False # Changes beyond workflows/pyproject
|
|
30
|
+
# diff: str
|
|
31
|
+
workflow_changes: dict[str, str] = field(default_factory=dict)
|
|
32
|
+
|
|
33
|
+
# Git change details (populated by parser if needed)
|
|
34
|
+
nodes_added: list[dict] = field(default_factory=list) # {"name": str, "is_development": bool}
|
|
35
|
+
nodes_removed: list[dict] = field(default_factory=list) # {"name": str, "is_development": bool}
|
|
36
|
+
dependencies_added: list[dict] = field(default_factory=list)
|
|
37
|
+
dependencies_removed: list[dict] = field(default_factory=list)
|
|
38
|
+
dependencies_updated: list[dict] = field(default_factory=list)
|
|
39
|
+
constraints_added: list[str] = field(default_factory=list)
|
|
40
|
+
constraints_removed: list[str] = field(default_factory=list)
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class GitInfo:
|
|
44
|
+
commit: str | None = None
|
|
45
|
+
branch: str | None = None
|
|
46
|
+
tag: str | None = None
|
|
47
|
+
is_dirty: bool = False
|
|
48
|
+
remote_url: str | None = None
|
|
49
|
+
github_owner: str | None = None
|
|
50
|
+
github_repo: str | None = None
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class EnvironmentComparison:
|
|
54
|
+
"""Comparison between current and expected environment states."""
|
|
55
|
+
|
|
56
|
+
missing_nodes: list[str] = field(default_factory=list)
|
|
57
|
+
extra_nodes: list[str] = field(default_factory=list)
|
|
58
|
+
version_mismatches: list[dict] = field(
|
|
59
|
+
default_factory=list
|
|
60
|
+
) # {name, expected, actual}
|
|
61
|
+
packages_in_sync: bool = True
|
|
62
|
+
package_sync_message: str = ""
|
|
63
|
+
potential_dev_rename: bool = False
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def is_synced(self) -> bool:
|
|
67
|
+
"""Check if environment is fully synced."""
|
|
68
|
+
return (
|
|
69
|
+
not self.missing_nodes
|
|
70
|
+
and not self.extra_nodes
|
|
71
|
+
and not self.version_mismatches
|
|
72
|
+
and self.packages_in_sync
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class NodeState:
|
|
77
|
+
"""State of an installed custom node."""
|
|
78
|
+
|
|
79
|
+
name: str
|
|
80
|
+
path: Path
|
|
81
|
+
disabled: bool = False
|
|
82
|
+
git_commit: str | None = None
|
|
83
|
+
git_branch: str | None = None
|
|
84
|
+
version: str | None = None # From git tag or pyproject
|
|
85
|
+
is_dirty: bool = False
|
|
86
|
+
source: str | None = None # 'registry', 'git', 'development', etc.
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass
|
|
90
|
+
class EnvironmentState:
|
|
91
|
+
"""Current state of an environment."""
|
|
92
|
+
|
|
93
|
+
custom_nodes: dict[str, NodeState] # name -> state
|
|
94
|
+
packages: dict[str, str] | None # name -> version
|
|
95
|
+
python_version: str | None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass
|
|
99
|
+
class MissingModelInfo:
|
|
100
|
+
"""Information about a model that's in pyproject but not in local index."""
|
|
101
|
+
model: "ManifestModel" # From global models table
|
|
102
|
+
workflow_names: list[str] # Which workflows need it
|
|
103
|
+
criticality: str # "required", "flexible", "optional" (worst case across workflows)
|
|
104
|
+
can_download: bool # Has sources available
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def is_required(self) -> bool:
|
|
108
|
+
return self.criticality == "required"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# === Semantic Value Objects ===
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class UserAction(Enum):
|
|
115
|
+
"""Recommended user actions."""
|
|
116
|
+
|
|
117
|
+
SYNC_REQUIRED = "sync"
|
|
118
|
+
COMMIT_REQUIRED = "commit"
|
|
119
|
+
NO_ACTION_NEEDED = "none"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass
|
|
123
|
+
class ChangesSummary:
|
|
124
|
+
"""Summary of changes with semantic meaning."""
|
|
125
|
+
|
|
126
|
+
primary_changes: List[str] = field(default_factory=list)
|
|
127
|
+
secondary_changes: List[str] = field(default_factory=list)
|
|
128
|
+
has_breaking_changes: bool = False
|
|
129
|
+
|
|
130
|
+
def get_headline(self) -> str:
|
|
131
|
+
"""Get a headline summary of changes."""
|
|
132
|
+
if not self.primary_changes and not self.secondary_changes:
|
|
133
|
+
return "No changes"
|
|
134
|
+
|
|
135
|
+
if self.has_breaking_changes:
|
|
136
|
+
return "Breaking changes detected"
|
|
137
|
+
|
|
138
|
+
if len(self.primary_changes) == 1 and not self.secondary_changes:
|
|
139
|
+
return self.primary_changes[0]
|
|
140
|
+
|
|
141
|
+
total = len(self.primary_changes) + len(self.secondary_changes)
|
|
142
|
+
return f"{total} changes"
|
|
143
|
+
|
|
144
|
+
def get_commit_message(self) -> str:
|
|
145
|
+
"""Generate a commit message from changes."""
|
|
146
|
+
parts = self.primary_changes + self.secondary_changes
|
|
147
|
+
if not parts:
|
|
148
|
+
return "Update environment configuration"
|
|
149
|
+
return "; ".join(parts)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@dataclass
|
|
155
|
+
class EnvironmentStatus:
|
|
156
|
+
"""Complete environment status including comparison and git/workflow state."""
|
|
157
|
+
|
|
158
|
+
comparison: EnvironmentComparison
|
|
159
|
+
git: GitStatus
|
|
160
|
+
workflow: DetailedWorkflowStatus
|
|
161
|
+
missing_models: list[MissingModelInfo] = field(default_factory=list)
|
|
162
|
+
|
|
163
|
+
@classmethod
|
|
164
|
+
def create(
|
|
165
|
+
cls,
|
|
166
|
+
comparison: EnvironmentComparison,
|
|
167
|
+
git_status: GitStatus,
|
|
168
|
+
workflow_status: DetailedWorkflowStatus,
|
|
169
|
+
missing_models: list[MissingModelInfo] | None = None,
|
|
170
|
+
) -> "EnvironmentStatus":
|
|
171
|
+
"""Factory method to create EnvironmentStatus from components."""
|
|
172
|
+
return cls(
|
|
173
|
+
comparison=comparison,
|
|
174
|
+
git=git_status,
|
|
175
|
+
workflow=workflow_status,
|
|
176
|
+
missing_models=missing_models or []
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
@property
|
|
180
|
+
def is_synced(self) -> bool:
|
|
181
|
+
"""Check if environment is fully synced (nodes, packages, workflows, and models)."""
|
|
182
|
+
return (
|
|
183
|
+
self.comparison.is_synced and
|
|
184
|
+
self.workflow.sync_status.is_synced and
|
|
185
|
+
not self.missing_models
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# === Semantic Methods ===
|
|
189
|
+
|
|
190
|
+
def get_changes_summary(self) -> ChangesSummary:
|
|
191
|
+
"""Analyze and categorize all changes."""
|
|
192
|
+
primary_changes = []
|
|
193
|
+
secondary_changes = []
|
|
194
|
+
|
|
195
|
+
# Node changes (most specific)
|
|
196
|
+
if self.git.nodes_added and self.git.nodes_removed:
|
|
197
|
+
primary_changes.append(
|
|
198
|
+
f"Update nodes: +{len(self.git.nodes_added)}, -{len(self.git.nodes_removed)}"
|
|
199
|
+
)
|
|
200
|
+
elif self.git.nodes_added:
|
|
201
|
+
if len(self.git.nodes_added) == 1:
|
|
202
|
+
primary_changes.append(f"Add {self.git.nodes_added[0]['name']}")
|
|
203
|
+
else:
|
|
204
|
+
primary_changes.append(f"Add {len(self.git.nodes_added)} nodes")
|
|
205
|
+
elif self.git.nodes_removed:
|
|
206
|
+
if len(self.git.nodes_removed) == 1:
|
|
207
|
+
primary_changes.append(f"Remove {self.git.nodes_removed[0]['name']}")
|
|
208
|
+
else:
|
|
209
|
+
primary_changes.append(f"Remove {len(self.git.nodes_removed)} nodes")
|
|
210
|
+
|
|
211
|
+
# Dependency changes
|
|
212
|
+
if (
|
|
213
|
+
self.git.dependencies_added
|
|
214
|
+
or self.git.dependencies_removed
|
|
215
|
+
or self.git.dependencies_updated
|
|
216
|
+
):
|
|
217
|
+
dep_count = (
|
|
218
|
+
len(self.git.dependencies_added)
|
|
219
|
+
+ len(self.git.dependencies_removed)
|
|
220
|
+
+ len(self.git.dependencies_updated)
|
|
221
|
+
)
|
|
222
|
+
secondary_changes.append(f"Update {dep_count} dependencies")
|
|
223
|
+
|
|
224
|
+
# Constraint changes
|
|
225
|
+
if self.git.constraints_added or self.git.constraints_removed:
|
|
226
|
+
secondary_changes.append("Update constraints")
|
|
227
|
+
|
|
228
|
+
# No more workflow tracking changes - all workflows are automatically managed
|
|
229
|
+
|
|
230
|
+
# Workflow file changes
|
|
231
|
+
if self.git.workflow_changes:
|
|
232
|
+
workflow_count = len(self.git.workflow_changes)
|
|
233
|
+
if workflow_count == 1:
|
|
234
|
+
workflow_name, workflow_status = list(
|
|
235
|
+
self.git.workflow_changes.items()
|
|
236
|
+
)[0]
|
|
237
|
+
if workflow_status == "modified":
|
|
238
|
+
primary_changes.append(f"Update {workflow_name}")
|
|
239
|
+
elif workflow_status == "added":
|
|
240
|
+
primary_changes.append(f"Add {workflow_name}")
|
|
241
|
+
elif workflow_status == "deleted":
|
|
242
|
+
primary_changes.append(f"Remove {workflow_name}")
|
|
243
|
+
else:
|
|
244
|
+
primary_changes.append(f"Update {workflow_count} workflows")
|
|
245
|
+
|
|
246
|
+
# Detect breaking changes
|
|
247
|
+
has_breaking = bool(
|
|
248
|
+
self.git.nodes_removed
|
|
249
|
+
or self.git.dependencies_removed
|
|
250
|
+
or self.git.constraints_removed
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
return ChangesSummary(
|
|
254
|
+
primary_changes=primary_changes,
|
|
255
|
+
secondary_changes=secondary_changes,
|
|
256
|
+
has_breaking_changes=has_breaking,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
def get_recommended_action(self) -> UserAction:
|
|
260
|
+
"""Determine what the user should do next."""
|
|
261
|
+
if not self.is_synced:
|
|
262
|
+
return UserAction.SYNC_REQUIRED
|
|
263
|
+
elif self.git.has_changes:
|
|
264
|
+
return UserAction.COMMIT_REQUIRED
|
|
265
|
+
else:
|
|
266
|
+
return UserAction.NO_ACTION_NEEDED
|
|
267
|
+
|
|
268
|
+
def generate_commit_message(self) -> str:
|
|
269
|
+
"""Generate a semantic commit message."""
|
|
270
|
+
summary = self.get_changes_summary()
|
|
271
|
+
return summary.get_commit_message()
|
|
272
|
+
|
|
273
|
+
def get_sync_preview(self) -> dict:
|
|
274
|
+
"""Get preview of what sync operation will do.
|
|
275
|
+
|
|
276
|
+
Note: WorkflowSyncStatus is from ComfyUI's perspective:
|
|
277
|
+
- new: in ComfyUI but not .cec → will be REMOVED
|
|
278
|
+
- deleted: in .cec but not ComfyUI → will be ADDED
|
|
279
|
+
- modified: differs between ComfyUI and .cec → will be UPDATED
|
|
280
|
+
"""
|
|
281
|
+
return {
|
|
282
|
+
'nodes_to_install': self.comparison.missing_nodes,
|
|
283
|
+
'nodes_to_remove': self.comparison.extra_nodes,
|
|
284
|
+
'nodes_to_update': self.comparison.version_mismatches,
|
|
285
|
+
'packages_to_sync': not self.comparison.packages_in_sync,
|
|
286
|
+
'workflows_to_add': self.workflow.sync_status.deleted, # Deleted from ComfyUI, will restore
|
|
287
|
+
'workflows_to_update': self.workflow.sync_status.modified, # Modified, will sync
|
|
288
|
+
'workflows_to_remove': self.workflow.sync_status.new, # New in ComfyUI, will remove
|
|
289
|
+
'models_missing': self.missing_models,
|
|
290
|
+
'models_downloadable': [m for m in self.missing_models if m.can_download],
|
|
291
|
+
'models_unavailable': [m for m in self.missing_models if not m.can_download],
|
|
292
|
+
'models_required': [m for m in self.missing_models if m.is_required],
|
|
293
|
+
}
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
# models/exceptions.py
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ComfyDockError(Exception):
|
|
8
|
+
"""Base exception for ComfyDock errors."""
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
# ====================================================
|
|
12
|
+
# Workspace exceptions
|
|
13
|
+
# ====================================================
|
|
14
|
+
|
|
15
|
+
class CDWorkspaceNotFoundError(ComfyDockError):
|
|
16
|
+
"""Workspace doesn't exist."""
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
class CDWorkspaceExistsError(ComfyDockError):
|
|
20
|
+
"""Workspace already exists."""
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
class CDWorkspaceError(ComfyDockError):
|
|
24
|
+
"""Workspace-related errors."""
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
# ===================================================
|
|
28
|
+
# Environment exceptions
|
|
29
|
+
# ===================================================
|
|
30
|
+
|
|
31
|
+
class CDEnvironmentError(ComfyDockError):
|
|
32
|
+
"""Environment-related errors."""
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
class CDEnvironmentNotFoundError(ComfyDockError):
|
|
36
|
+
"""Environment doesn't exist."""
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
class CDEnvironmentExistsError(ComfyDockError):
|
|
40
|
+
"""Environment already exists."""
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
# ===================================================
|
|
44
|
+
# Resolution exceptions
|
|
45
|
+
# ==================================================
|
|
46
|
+
|
|
47
|
+
class CDResolutionError(ComfyDockError):
|
|
48
|
+
"""Resolution errors."""
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
# ===================================================
|
|
52
|
+
# Node exceptions
|
|
53
|
+
# ===================================================
|
|
54
|
+
|
|
55
|
+
class CDNodeNotFoundError(ComfyDockError):
|
|
56
|
+
"""Raised when Node not found."""
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class NodeAction:
|
|
61
|
+
"""Represents a possible action to resolve an error."""
|
|
62
|
+
action_type: Literal[
|
|
63
|
+
'remove_node',
|
|
64
|
+
'add_node_dev',
|
|
65
|
+
'add_node_force',
|
|
66
|
+
'add_node_version',
|
|
67
|
+
'rename_directory',
|
|
68
|
+
'update_node',
|
|
69
|
+
'add_constraint',
|
|
70
|
+
'skip_node'
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
# Parameters needed for the action
|
|
74
|
+
node_identifier: str | None = None
|
|
75
|
+
node_name: str | None = None
|
|
76
|
+
directory_name: str | None = None
|
|
77
|
+
new_name: str | None = None
|
|
78
|
+
package_name: str | None = None # For add_constraint action
|
|
79
|
+
|
|
80
|
+
# Human-readable description (client-agnostic)
|
|
81
|
+
description: str = ""
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass
|
|
85
|
+
class NodeConflictContext:
|
|
86
|
+
"""Context about what conflicted and why."""
|
|
87
|
+
conflict_type: Literal[
|
|
88
|
+
'already_tracked',
|
|
89
|
+
'directory_exists_non_git',
|
|
90
|
+
'directory_exists_no_remote',
|
|
91
|
+
'same_repo_exists',
|
|
92
|
+
'different_repo_exists',
|
|
93
|
+
'dev_node_replacement',
|
|
94
|
+
'user_cancelled'
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
node_name: str
|
|
98
|
+
identifier: str | None = None
|
|
99
|
+
existing_identifier: str | None = None
|
|
100
|
+
filesystem_path: str | None = None
|
|
101
|
+
local_remote_url: str | None = None
|
|
102
|
+
expected_remote_url: str | None = None
|
|
103
|
+
is_development: bool = False
|
|
104
|
+
|
|
105
|
+
# Suggested actions
|
|
106
|
+
suggested_actions: list[NodeAction] = field(default_factory=list)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class CDNodeConflictError(ComfyDockError):
|
|
110
|
+
"""Raised when Node has conflicts with enhanced context."""
|
|
111
|
+
|
|
112
|
+
def __init__(self, message: str, context: NodeConflictContext | None = None):
|
|
113
|
+
super().__init__(message)
|
|
114
|
+
self.context = context
|
|
115
|
+
|
|
116
|
+
def get_actions(self) -> list[NodeAction]:
|
|
117
|
+
"""Get suggested actions for resolving this conflict."""
|
|
118
|
+
return self.context.suggested_actions if self.context else []
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@dataclass
|
|
122
|
+
class DependencyConflictContext:
|
|
123
|
+
"""Context about dependency conflicts during node installation."""
|
|
124
|
+
|
|
125
|
+
# The node being added
|
|
126
|
+
node_name: str
|
|
127
|
+
|
|
128
|
+
# Parsed conflict information
|
|
129
|
+
conflicting_packages: list[tuple[str, str]] # Package pairs that conflict
|
|
130
|
+
conflict_descriptions: list[str] # Simplified conflict messages
|
|
131
|
+
|
|
132
|
+
# Raw UV stderr for verbose mode
|
|
133
|
+
raw_stderr: str = ""
|
|
134
|
+
|
|
135
|
+
# Suggested resolutions
|
|
136
|
+
suggested_actions: list[NodeAction] = field(default_factory=list)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class CDDependencyConflictError(ComfyDockError):
|
|
140
|
+
"""Raised when dependency resolution fails during node installation."""
|
|
141
|
+
|
|
142
|
+
def __init__(self, message: str, context: DependencyConflictContext | None = None):
|
|
143
|
+
super().__init__(message)
|
|
144
|
+
self.context = context
|
|
145
|
+
|
|
146
|
+
def get_actions(self) -> list[NodeAction]:
|
|
147
|
+
"""Get suggested actions for resolving this conflict."""
|
|
148
|
+
return self.context.suggested_actions if self.context else []
|
|
149
|
+
|
|
150
|
+
# ===================================================
|
|
151
|
+
# Registry exceptions
|
|
152
|
+
# ===================================================
|
|
153
|
+
|
|
154
|
+
class CDRegistryError(ComfyDockError):
|
|
155
|
+
"""Base class for registry errors."""
|
|
156
|
+
pass
|
|
157
|
+
|
|
158
|
+
class CDRegistryAuthError(CDRegistryError):
|
|
159
|
+
"""Authentication/authorization errors with registry."""
|
|
160
|
+
pass
|
|
161
|
+
|
|
162
|
+
class CDRegistryServerError(CDRegistryError):
|
|
163
|
+
"""Registry server errors (5xx)."""
|
|
164
|
+
pass
|
|
165
|
+
|
|
166
|
+
class CDRegistryConnectionError(CDRegistryError):
|
|
167
|
+
"""Network/connection errors to registry."""
|
|
168
|
+
pass
|
|
169
|
+
|
|
170
|
+
class CDRegistryDataError(ComfyDockError):
|
|
171
|
+
"""Registry data is not available or cannot be loaded.
|
|
172
|
+
|
|
173
|
+
This error indicates that registry node mappings are missing or corrupted.
|
|
174
|
+
Recovery typically involves downloading or updating the registry data.
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
def __init__(
|
|
178
|
+
self,
|
|
179
|
+
message: str,
|
|
180
|
+
cache_path: str | None = None,
|
|
181
|
+
can_retry: bool = True
|
|
182
|
+
):
|
|
183
|
+
super().__init__(message)
|
|
184
|
+
self.cache_path = cache_path
|
|
185
|
+
self.can_retry = can_retry
|
|
186
|
+
|
|
187
|
+
# ===================================================
|
|
188
|
+
# Pyproject exceptions
|
|
189
|
+
# ===================================================
|
|
190
|
+
|
|
191
|
+
class CDPyprojectError(ComfyDockError):
|
|
192
|
+
"""Errors related to pyproject.toml operations."""
|
|
193
|
+
pass
|
|
194
|
+
|
|
195
|
+
class CDPyprojectNotFoundError(CDPyprojectError):
|
|
196
|
+
"""pyproject.toml file not found."""
|
|
197
|
+
pass
|
|
198
|
+
|
|
199
|
+
class CDPyprojectInvalidError(CDPyprojectError):
|
|
200
|
+
"""pyproject.toml file is invalid or corrupted."""
|
|
201
|
+
pass
|
|
202
|
+
|
|
203
|
+
# ===================================================
|
|
204
|
+
# Dependency exceptions
|
|
205
|
+
# ===================================================
|
|
206
|
+
|
|
207
|
+
class CDDependencyError(ComfyDockError):
|
|
208
|
+
"""Dependency-related errors."""
|
|
209
|
+
pass
|
|
210
|
+
|
|
211
|
+
class CDPackageSyncError(CDDependencyError):
|
|
212
|
+
"""Package synchronization errors."""
|
|
213
|
+
pass
|
|
214
|
+
|
|
215
|
+
# ===================================================
|
|
216
|
+
# Index exceptions
|
|
217
|
+
# ===================================================
|
|
218
|
+
|
|
219
|
+
class CDIndexError(ComfyDockError):
|
|
220
|
+
"""Index configuration errors."""
|
|
221
|
+
pass
|
|
222
|
+
|
|
223
|
+
# ===================================================
|
|
224
|
+
# Process/Command exceptions
|
|
225
|
+
# ===================================================
|
|
226
|
+
|
|
227
|
+
class CDProcessError(ComfyDockError):
|
|
228
|
+
"""Raised when subprocess command execution fails."""
|
|
229
|
+
|
|
230
|
+
def __init__(
|
|
231
|
+
self,
|
|
232
|
+
message: str,
|
|
233
|
+
command: list[str] | None = None,
|
|
234
|
+
stderr: str | None = None,
|
|
235
|
+
stdout: str | None = None,
|
|
236
|
+
returncode: int | None = None,
|
|
237
|
+
):
|
|
238
|
+
super().__init__(message)
|
|
239
|
+
self.command = command
|
|
240
|
+
self.stderr = stderr
|
|
241
|
+
self.stdout = stdout
|
|
242
|
+
self.returncode = returncode
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
# ===================================================
|
|
246
|
+
# UV exceptions
|
|
247
|
+
# ==================================================
|
|
248
|
+
|
|
249
|
+
class UVNotInstalledError(ComfyDockError):
|
|
250
|
+
"""Raised when UV is not installed."""
|
|
251
|
+
pass
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class UVCommandError(ComfyDockError):
|
|
255
|
+
"""Raised when UV command execution fails."""
|
|
256
|
+
|
|
257
|
+
def __init__(
|
|
258
|
+
self,
|
|
259
|
+
message: str,
|
|
260
|
+
command: list[str] | None = None,
|
|
261
|
+
stderr: str | None = None,
|
|
262
|
+
stdout: str | None = None,
|
|
263
|
+
returncode: int | None = None,
|
|
264
|
+
):
|
|
265
|
+
super().__init__(message)
|
|
266
|
+
self.command = command
|
|
267
|
+
self.stderr = stderr
|
|
268
|
+
self.stdout = stdout
|
|
269
|
+
self.returncode = returncode
|
|
270
|
+
|
|
271
|
+
def __str__(self) -> str:
|
|
272
|
+
"""Include stderr/stdout in string representation for better error messages."""
|
|
273
|
+
parts = [super().__str__()]
|
|
274
|
+
|
|
275
|
+
if self.stderr and self.stderr.strip():
|
|
276
|
+
parts.append(f"\nStderr: {self.stderr.strip()}")
|
|
277
|
+
|
|
278
|
+
if self.stdout and self.stdout.strip():
|
|
279
|
+
parts.append(f"\nStdout: {self.stdout.strip()}")
|
|
280
|
+
|
|
281
|
+
return "".join(parts)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
# ===================================================
|
|
285
|
+
# Export/Import exceptions
|
|
286
|
+
# ===================================================
|
|
287
|
+
|
|
288
|
+
@dataclass
|
|
289
|
+
class ExportErrorContext:
|
|
290
|
+
"""Context about an export failure."""
|
|
291
|
+
uncommitted_workflows: list[str] = field(default_factory=list)
|
|
292
|
+
uncommitted_git_changes: bool = False
|
|
293
|
+
has_unresolved_issues: bool = False
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
class CDExportError(ComfyDockError):
|
|
297
|
+
"""Export operation failed with detailed context."""
|
|
298
|
+
|
|
299
|
+
def __init__(self, message: str, context: ExportErrorContext | None = None):
|
|
300
|
+
super().__init__(message)
|
|
301
|
+
self.context = context
|
|
302
|
+
|
|
303
|
+
@property
|
|
304
|
+
def uncommitted_workflows(self) -> list[str] | None:
|
|
305
|
+
"""Get list of uncommitted workflows for backward compatibility."""
|
|
306
|
+
return self.context.uncommitted_workflows if self.context else None
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
# ===================================================
|
|
310
|
+
# Model Download exceptions
|
|
311
|
+
# ===================================================
|
|
312
|
+
|
|
313
|
+
@dataclass
|
|
314
|
+
class DownloadErrorContext:
|
|
315
|
+
"""Detailed context about a download failure."""
|
|
316
|
+
provider: str # 'civitai', 'huggingface', 'custom'
|
|
317
|
+
error_category: str # 'auth_missing', 'auth_invalid', 'forbidden', 'not_found', 'network', 'server', 'unknown'
|
|
318
|
+
http_status: int | None
|
|
319
|
+
url: str
|
|
320
|
+
has_configured_auth: bool # Was auth configured (even if invalid)?
|
|
321
|
+
raw_error: str # Original error message for debugging
|
|
322
|
+
|
|
323
|
+
def get_user_message(self) -> str:
|
|
324
|
+
"""Generate user-friendly error message."""
|
|
325
|
+
if self.provider == "civitai":
|
|
326
|
+
if self.error_category == "auth_missing":
|
|
327
|
+
return (
|
|
328
|
+
f"CivitAI model requires authentication (HTTP {self.http_status}). "
|
|
329
|
+
"No API key found. Get your key from https://civitai.com/user/account "
|
|
330
|
+
"and add it with: comfygit config --civitai-key <your-key>"
|
|
331
|
+
)
|
|
332
|
+
elif self.error_category == "auth_invalid":
|
|
333
|
+
return (
|
|
334
|
+
f"CivitAI authentication failed (HTTP {self.http_status}). "
|
|
335
|
+
"Your API key may be invalid or expired. "
|
|
336
|
+
"Update it with: comfygit config --civitai-key <your-key>"
|
|
337
|
+
)
|
|
338
|
+
elif self.error_category == "forbidden":
|
|
339
|
+
return (
|
|
340
|
+
f"CivitAI access forbidden (HTTP {self.http_status}). "
|
|
341
|
+
"This model may require special permissions or may not be publicly available."
|
|
342
|
+
)
|
|
343
|
+
elif self.error_category == "not_found":
|
|
344
|
+
return f"CivitAI model not found (HTTP {self.http_status}). The URL may be incorrect or the model was removed."
|
|
345
|
+
|
|
346
|
+
elif self.provider == "huggingface":
|
|
347
|
+
if self.error_category in ("auth_missing", "auth_invalid"):
|
|
348
|
+
return (
|
|
349
|
+
f"HuggingFace model requires authentication (HTTP {self.http_status}). "
|
|
350
|
+
"Set the HF_TOKEN environment variable with your HuggingFace token. "
|
|
351
|
+
"Get your token from: https://huggingface.co/settings/tokens"
|
|
352
|
+
)
|
|
353
|
+
elif self.error_category == "not_found":
|
|
354
|
+
return f"HuggingFace model not found (HTTP {self.http_status}). Check the URL is correct."
|
|
355
|
+
|
|
356
|
+
# Generic provider or fallback
|
|
357
|
+
if self.error_category == "network":
|
|
358
|
+
return f"Network error downloading from {self.provider}: {self.raw_error}"
|
|
359
|
+
elif self.error_category == "server":
|
|
360
|
+
return f"Server error from {self.provider} (HTTP {self.http_status}). Try again later."
|
|
361
|
+
elif self.http_status:
|
|
362
|
+
return f"Download failed from {self.provider} (HTTP {self.http_status}): {self.raw_error}"
|
|
363
|
+
else:
|
|
364
|
+
return f"Download failed from {self.provider}: {self.raw_error}"
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
class CDModelDownloadError(ComfyDockError):
|
|
368
|
+
"""Model download error with provider-specific context."""
|
|
369
|
+
|
|
370
|
+
def __init__(self, message: str, context: DownloadErrorContext | None = None):
|
|
371
|
+
super().__init__(message)
|
|
372
|
+
self.context = context
|
|
373
|
+
|
|
374
|
+
def get_user_message(self) -> str:
|
|
375
|
+
"""Get user-friendly error message."""
|
|
376
|
+
if self.context:
|
|
377
|
+
return self.context.get_user_message()
|
|
378
|
+
return str(self)
|