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,419 @@
|
|
|
1
|
+
"""Factory for creating new environments."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import shutil
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from comfygit_core.core.environment import Environment
|
|
9
|
+
|
|
10
|
+
from ..logging.logging_config import get_logger
|
|
11
|
+
from ..managers.git_manager import GitManager
|
|
12
|
+
from ..models.exceptions import (
|
|
13
|
+
CDEnvironmentExistsError,
|
|
14
|
+
)
|
|
15
|
+
from ..utils.pytorch import extract_pip_show_package_version
|
|
16
|
+
from ..utils.comfyui_ops import clone_comfyui
|
|
17
|
+
from ..utils.environment_cleanup import mark_environment_complete
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from comfygit_core.core.workspace import Workspace
|
|
21
|
+
|
|
22
|
+
logger = get_logger(__name__)
|
|
23
|
+
|
|
24
|
+
class EnvironmentFactory:
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
def create(
|
|
28
|
+
name: str,
|
|
29
|
+
env_path: Path,
|
|
30
|
+
workspace: Workspace,
|
|
31
|
+
python_version: str = "3.12",
|
|
32
|
+
comfyui_version: str | None = None,
|
|
33
|
+
torch_backend: str = "auto",
|
|
34
|
+
) -> Environment:
|
|
35
|
+
"""Create a new environment."""
|
|
36
|
+
if env_path.exists():
|
|
37
|
+
raise CDEnvironmentExistsError(f"Environment path already exists: {env_path}")
|
|
38
|
+
|
|
39
|
+
# Create structure
|
|
40
|
+
env_path.mkdir(parents=True)
|
|
41
|
+
cec_path = env_path / ".cec"
|
|
42
|
+
cec_path.mkdir()
|
|
43
|
+
|
|
44
|
+
# Pin Python version for uv
|
|
45
|
+
python_version_file = cec_path / ".python-version"
|
|
46
|
+
python_version_file.write_text(python_version + "\n")
|
|
47
|
+
logger.debug(f"Created .python-version: {python_version}")
|
|
48
|
+
|
|
49
|
+
# Log torch backend selection
|
|
50
|
+
if torch_backend == "auto":
|
|
51
|
+
logger.info("PyTorch backend: auto (will detect GPU)")
|
|
52
|
+
else:
|
|
53
|
+
logger.info(f"PyTorch backend: {torch_backend}")
|
|
54
|
+
|
|
55
|
+
# Initialize environment
|
|
56
|
+
env = Environment(
|
|
57
|
+
name=name,
|
|
58
|
+
path=env_path,
|
|
59
|
+
workspace=workspace,
|
|
60
|
+
torch_backend=torch_backend,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Resolve ComfyUI version
|
|
64
|
+
from ..caching.api_cache import APICacheManager
|
|
65
|
+
from ..caching.comfyui_cache import ComfyUICacheManager, ComfyUISpec
|
|
66
|
+
from ..clients.github_client import GitHubClient
|
|
67
|
+
from ..utils.comfyui_ops import resolve_comfyui_version
|
|
68
|
+
from ..utils.git import git_rev_parse
|
|
69
|
+
|
|
70
|
+
api_cache = APICacheManager(cache_base_path=workspace.paths.cache)
|
|
71
|
+
github_client = GitHubClient(cache_manager=api_cache)
|
|
72
|
+
|
|
73
|
+
version_to_clone, version_type, _ = resolve_comfyui_version(
|
|
74
|
+
comfyui_version,
|
|
75
|
+
github_client
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Check ComfyUI cache first
|
|
79
|
+
comfyui_cache = ComfyUICacheManager(cache_base_path=workspace.paths.cache)
|
|
80
|
+
spec = ComfyUISpec(
|
|
81
|
+
version=version_to_clone,
|
|
82
|
+
version_type=version_type,
|
|
83
|
+
commit_sha=None # Will be set after cloning
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
cached_path = comfyui_cache.get_cached_comfyui(spec)
|
|
87
|
+
|
|
88
|
+
if cached_path:
|
|
89
|
+
# Restore from cache
|
|
90
|
+
logger.info(f"Restoring ComfyUI {version_type} {version_to_clone} from cache...")
|
|
91
|
+
shutil.copytree(cached_path, env.comfyui_path)
|
|
92
|
+
commit_sha = git_rev_parse(env.comfyui_path, "HEAD")
|
|
93
|
+
sha_display = f" ({commit_sha[:7]})" if commit_sha else ""
|
|
94
|
+
logger.info(f"Restored ComfyUI from cache{sha_display}")
|
|
95
|
+
else:
|
|
96
|
+
# Clone fresh
|
|
97
|
+
logger.info(f"Cloning ComfyUI {version_type} {version_to_clone}...")
|
|
98
|
+
try:
|
|
99
|
+
comfyui_version_output = clone_comfyui(env.comfyui_path, version_to_clone)
|
|
100
|
+
if comfyui_version_output:
|
|
101
|
+
logger.info(f"Successfully cloned ComfyUI version: {comfyui_version_output}")
|
|
102
|
+
else:
|
|
103
|
+
logger.warning("ComfyUI clone failed")
|
|
104
|
+
raise RuntimeError("ComfyUI clone failed")
|
|
105
|
+
except Exception as e:
|
|
106
|
+
logger.warning(f"ComfyUI clone failed: {e}")
|
|
107
|
+
raise e
|
|
108
|
+
|
|
109
|
+
# Get actual commit SHA and cache it
|
|
110
|
+
commit_sha = git_rev_parse(env.comfyui_path, "HEAD")
|
|
111
|
+
if commit_sha:
|
|
112
|
+
spec.commit_sha = commit_sha
|
|
113
|
+
comfyui_cache.cache_comfyui(spec, env.comfyui_path)
|
|
114
|
+
logger.info(f"Cached ComfyUI {version_type} {version_to_clone} ({commit_sha[:7]})")
|
|
115
|
+
else:
|
|
116
|
+
logger.warning(f"Could not determine commit SHA for ComfyUI {version_type} {version_to_clone}")
|
|
117
|
+
|
|
118
|
+
# Remove ComfyUI's default models directory (will be replaced with symlink)
|
|
119
|
+
models_dir = env.comfyui_path / "models"
|
|
120
|
+
if models_dir.exists() and not models_dir.is_symlink():
|
|
121
|
+
shutil.rmtree(models_dir)
|
|
122
|
+
logger.debug("Removed ComfyUI's default models directory")
|
|
123
|
+
|
|
124
|
+
# Remove ComfyUI's default input/output directories (will be replaced with symlinks)
|
|
125
|
+
from ..utils.symlink_utils import is_link
|
|
126
|
+
|
|
127
|
+
input_dir = env.comfyui_path / "input"
|
|
128
|
+
if input_dir.exists() and not is_link(input_dir):
|
|
129
|
+
shutil.rmtree(input_dir)
|
|
130
|
+
logger.debug("Removed ComfyUI's default input directory")
|
|
131
|
+
|
|
132
|
+
output_dir = env.comfyui_path / "output"
|
|
133
|
+
if output_dir.exists() and not is_link(output_dir):
|
|
134
|
+
shutil.rmtree(output_dir)
|
|
135
|
+
logger.debug("Removed ComfyUI's default output directory")
|
|
136
|
+
|
|
137
|
+
# Create workspace directories and symlinks for user content
|
|
138
|
+
env.user_content_manager.create_directories()
|
|
139
|
+
env.user_content_manager.create_symlinks()
|
|
140
|
+
logger.debug("Created user content symlinks")
|
|
141
|
+
|
|
142
|
+
# Create initial pyproject.toml
|
|
143
|
+
config = EnvironmentFactory._create_initial_pyproject(
|
|
144
|
+
name,
|
|
145
|
+
python_version,
|
|
146
|
+
version_to_clone,
|
|
147
|
+
version_type,
|
|
148
|
+
commit_sha,
|
|
149
|
+
torch_backend
|
|
150
|
+
)
|
|
151
|
+
env.pyproject.save(config)
|
|
152
|
+
|
|
153
|
+
# Phase 1: Create empty venv
|
|
154
|
+
logger.info("Creating virtual environment...")
|
|
155
|
+
env.uv_manager.sync_project(verbose=False)
|
|
156
|
+
|
|
157
|
+
# Phase 2: Install PyTorch with uv pip install --torch-backend
|
|
158
|
+
logger.info(f"Installing PyTorch with backend: {torch_backend}")
|
|
159
|
+
env.uv_manager.install_packages(
|
|
160
|
+
packages=["torch", "torchvision", "torchaudio"],
|
|
161
|
+
python=env.uv_manager.python_executable,
|
|
162
|
+
torch_backend=torch_backend,
|
|
163
|
+
verbose=True # Show progress to user
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Phase 3: Query installed PyTorch versions, extract backend, and configure index
|
|
167
|
+
from ..constants import PYTORCH_CORE_PACKAGES
|
|
168
|
+
from ..utils.pytorch import extract_backend_from_version, get_pytorch_index_url
|
|
169
|
+
|
|
170
|
+
# Get first package version to extract backend
|
|
171
|
+
first_version = extract_pip_show_package_version(
|
|
172
|
+
env.uv_manager.show_package("torch", env.uv_manager.python_executable)
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
if first_version:
|
|
176
|
+
# Extract backend from version (e.g., "2.9.0+cu128" -> "cu128")
|
|
177
|
+
backend = extract_backend_from_version(first_version)
|
|
178
|
+
logger.info(f"Detected PyTorch backend from installed version: {backend}")
|
|
179
|
+
|
|
180
|
+
# Configure PyTorch index for uv sync (works for any backend)
|
|
181
|
+
if backend:
|
|
182
|
+
index_name = f"pytorch-{backend}"
|
|
183
|
+
env.pyproject.uv_config.add_index(
|
|
184
|
+
name=index_name,
|
|
185
|
+
url=get_pytorch_index_url(backend),
|
|
186
|
+
explicit=True
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Add sources for PyTorch packages
|
|
190
|
+
for pkg in PYTORCH_CORE_PACKAGES:
|
|
191
|
+
env.pyproject.uv_config.add_source(pkg, {"index": index_name})
|
|
192
|
+
|
|
193
|
+
logger.info(f"Configured PyTorch index: {index_name}")
|
|
194
|
+
|
|
195
|
+
# Add constraints for all PyTorch packages
|
|
196
|
+
for pkg in PYTORCH_CORE_PACKAGES:
|
|
197
|
+
version = extract_pip_show_package_version(
|
|
198
|
+
env.uv_manager.show_package(pkg, env.uv_manager.python_executable)
|
|
199
|
+
)
|
|
200
|
+
if version:
|
|
201
|
+
env.pyproject.uv_config.add_constraint(f"{pkg}=={version}")
|
|
202
|
+
logger.info(f"Pinned {pkg}=={version}")
|
|
203
|
+
|
|
204
|
+
# Phase 4: Add ComfyUI requirements
|
|
205
|
+
comfyui_reqs = env.comfyui_path / "requirements.txt"
|
|
206
|
+
if comfyui_reqs.exists():
|
|
207
|
+
logger.info("Adding ComfyUI requirements...")
|
|
208
|
+
env.uv_manager.add_requirements_with_sources(comfyui_reqs, frozen=True)
|
|
209
|
+
|
|
210
|
+
# Phase 5: Final UV sync to install all dependencies
|
|
211
|
+
logger.info("Installing dependencies...")
|
|
212
|
+
env.uv_manager.sync_project(verbose=True)
|
|
213
|
+
|
|
214
|
+
# Use GitManager for repository initialization
|
|
215
|
+
git_mgr = GitManager(cec_path)
|
|
216
|
+
git_mgr.initialize_environment_repo("Initial environment setup")
|
|
217
|
+
|
|
218
|
+
# Create model symlink (should succeed now that models/ is removed)
|
|
219
|
+
try:
|
|
220
|
+
env.model_symlink_manager.create_symlink()
|
|
221
|
+
logger.info("Model directory linked successfully")
|
|
222
|
+
except Exception as e:
|
|
223
|
+
logger.error(f"Failed to create model symlink: {e}")
|
|
224
|
+
raise # FATAL - environment won't work without models
|
|
225
|
+
|
|
226
|
+
# Mark environment as fully initialized
|
|
227
|
+
mark_environment_complete(cec_path)
|
|
228
|
+
|
|
229
|
+
logger.info(f"Environment '{name}' created successfully")
|
|
230
|
+
return env
|
|
231
|
+
|
|
232
|
+
@staticmethod
|
|
233
|
+
def import_from_bundle(
|
|
234
|
+
tarball_path: Path,
|
|
235
|
+
name: str,
|
|
236
|
+
env_path: Path,
|
|
237
|
+
workspace: Workspace,
|
|
238
|
+
torch_backend: str = "auto",
|
|
239
|
+
) -> Environment:
|
|
240
|
+
"""Create environment structure from tarball (extraction only).
|
|
241
|
+
|
|
242
|
+
This creates the environment directory and extracts the .cec contents.
|
|
243
|
+
The environment is NOT fully initialized - caller must call
|
|
244
|
+
env.finalize_import() to complete setup.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
tarball_path: Path to .tar.gz bundle
|
|
248
|
+
name: Environment name
|
|
249
|
+
env_path: Target environment directory
|
|
250
|
+
workspace: Workspace instance
|
|
251
|
+
torch_backend: PyTorch backend (auto, cpu, cu118, cu121, etc.)
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
Environment instance with .cec extracted but not fully initialized
|
|
255
|
+
|
|
256
|
+
Raises:
|
|
257
|
+
CDEnvironmentExistsError: If env_path already exists
|
|
258
|
+
"""
|
|
259
|
+
if env_path.exists():
|
|
260
|
+
raise CDEnvironmentExistsError(f"Environment path already exists: {env_path}")
|
|
261
|
+
|
|
262
|
+
logger.info(f"Creating environment structure from bundle: {tarball_path}")
|
|
263
|
+
|
|
264
|
+
# Log torch backend selection
|
|
265
|
+
if torch_backend == "auto":
|
|
266
|
+
logger.info("PyTorch backend: auto (will detect GPU)")
|
|
267
|
+
else:
|
|
268
|
+
logger.info(f"PyTorch backend: {torch_backend}")
|
|
269
|
+
|
|
270
|
+
# Create environment directory structure
|
|
271
|
+
env_path.mkdir(parents=True, exist_ok=True)
|
|
272
|
+
cec_path = env_path / ".cec"
|
|
273
|
+
|
|
274
|
+
# Extract tarball to .cec
|
|
275
|
+
from ..managers.export_import_manager import ExportImportManager
|
|
276
|
+
manager = ExportImportManager(cec_path, env_path / "ComfyUI")
|
|
277
|
+
manager.extract_import(tarball_path, cec_path)
|
|
278
|
+
|
|
279
|
+
# Create and return Environment instance
|
|
280
|
+
# NOTE: ComfyUI is not cloned yet, workflows not copied, models not resolved
|
|
281
|
+
return Environment(
|
|
282
|
+
name=name,
|
|
283
|
+
path=env_path,
|
|
284
|
+
workspace=workspace,
|
|
285
|
+
torch_backend=torch_backend,
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
@staticmethod
|
|
289
|
+
def import_from_git(
|
|
290
|
+
git_url: str,
|
|
291
|
+
name: str,
|
|
292
|
+
env_path: Path,
|
|
293
|
+
workspace: Workspace,
|
|
294
|
+
branch: str | None = None,
|
|
295
|
+
torch_backend: str = "auto",
|
|
296
|
+
) -> Environment:
|
|
297
|
+
"""Create environment structure from git repository (clone only).
|
|
298
|
+
|
|
299
|
+
This clones the git repository to .cec directory.
|
|
300
|
+
The environment is NOT fully initialized - caller must call
|
|
301
|
+
env.finalize_import() to complete setup.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
git_url: Git repository URL
|
|
305
|
+
name: Environment name
|
|
306
|
+
env_path: Target environment directory
|
|
307
|
+
workspace: Workspace instance
|
|
308
|
+
branch: Optional branch/tag/commit to checkout
|
|
309
|
+
torch_backend: PyTorch backend (auto, cpu, cu118, cu121, etc.)
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
Environment instance with .cec cloned but not fully initialized
|
|
313
|
+
|
|
314
|
+
Raises:
|
|
315
|
+
CDEnvironmentExistsError: If env_path already exists
|
|
316
|
+
ValueError: If git clone fails
|
|
317
|
+
"""
|
|
318
|
+
if env_path.exists():
|
|
319
|
+
raise CDEnvironmentExistsError(f"Environment path already exists: {env_path}")
|
|
320
|
+
|
|
321
|
+
logger.info(f"Creating environment structure from git: {git_url}")
|
|
322
|
+
|
|
323
|
+
# Log torch backend selection
|
|
324
|
+
if torch_backend == "auto":
|
|
325
|
+
logger.info("PyTorch backend: auto (will detect GPU)")
|
|
326
|
+
else:
|
|
327
|
+
logger.info(f"PyTorch backend: {torch_backend}")
|
|
328
|
+
|
|
329
|
+
# Create environment directory structure
|
|
330
|
+
env_path.mkdir(parents=True, exist_ok=True)
|
|
331
|
+
cec_path = env_path / ".cec"
|
|
332
|
+
|
|
333
|
+
# Parse URL for subdirectory specification
|
|
334
|
+
from ..utils.git import git_clone, git_clone_subdirectory, parse_git_url_with_subdir
|
|
335
|
+
|
|
336
|
+
base_url, subdir = parse_git_url_with_subdir(git_url)
|
|
337
|
+
|
|
338
|
+
# Clone repository to .cec (with subdirectory extraction if specified)
|
|
339
|
+
if subdir:
|
|
340
|
+
logger.info(f"Cloning {base_url} and extracting subdirectory '{subdir}' to {cec_path}")
|
|
341
|
+
git_clone_subdirectory(base_url, cec_path, subdir, ref=branch)
|
|
342
|
+
# Note: git_clone_subdirectory validates pyproject.toml internally
|
|
343
|
+
|
|
344
|
+
# Subdirectory imports lose git history, need to init new repo
|
|
345
|
+
from ..utils.git import git_init, git_remote_get_url
|
|
346
|
+
if not (cec_path / ".git").exists():
|
|
347
|
+
logger.info("Initializing git repository for subdirectory import")
|
|
348
|
+
git_init(cec_path)
|
|
349
|
+
|
|
350
|
+
# WARNING: Do NOT auto-add remote for subdirectory imports!
|
|
351
|
+
# The base_url points to the parent repo, not a valid push target for this subdirectory.
|
|
352
|
+
# User must manually set up their own remote if they want to push back to a separate repo.
|
|
353
|
+
logger.warning(
|
|
354
|
+
f"Subdirectory import from {base_url}#{subdir} - no remote configured. "
|
|
355
|
+
"Set up a remote manually if you want to push changes: comfygit remote add origin <url>"
|
|
356
|
+
)
|
|
357
|
+
else:
|
|
358
|
+
logger.info(f"Cloning {base_url} to {cec_path}")
|
|
359
|
+
git_clone(base_url, cec_path, ref=branch)
|
|
360
|
+
|
|
361
|
+
# Validate it's a ComfyDock environment (only for non-subdir imports)
|
|
362
|
+
pyproject_path = cec_path / "pyproject.toml"
|
|
363
|
+
if not pyproject_path.exists():
|
|
364
|
+
raise ValueError(
|
|
365
|
+
"Repository does not contain pyproject.toml - not a valid ComfyDock environment"
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
# Auto-add the clone URL as 'origin' remote
|
|
369
|
+
# Note: git clone automatically sets up 'origin', but we validate it exists
|
|
370
|
+
from ..utils.git import git_remote_add, git_remote_get_url
|
|
371
|
+
|
|
372
|
+
origin_url = git_remote_get_url(cec_path, "origin")
|
|
373
|
+
if not origin_url:
|
|
374
|
+
# Should not happen after git clone, but add as safety
|
|
375
|
+
logger.info(f"Adding 'origin' remote: {base_url}")
|
|
376
|
+
git_remote_add(cec_path, "origin", base_url)
|
|
377
|
+
else:
|
|
378
|
+
logger.info(f"Remote 'origin' already configured: {origin_url}")
|
|
379
|
+
|
|
380
|
+
logger.info("Successfully prepared environment from git")
|
|
381
|
+
|
|
382
|
+
# Create and return Environment instance
|
|
383
|
+
# NOTE: ComfyUI is not cloned yet, workflows not copied, models not resolved
|
|
384
|
+
return Environment(
|
|
385
|
+
name=name,
|
|
386
|
+
path=env_path,
|
|
387
|
+
workspace=workspace,
|
|
388
|
+
torch_backend=torch_backend,
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
@staticmethod
|
|
392
|
+
def _create_initial_pyproject(
|
|
393
|
+
name: str,
|
|
394
|
+
python_version: str,
|
|
395
|
+
comfyui_version: str,
|
|
396
|
+
comfyui_version_type: str = "branch",
|
|
397
|
+
comfyui_commit_sha: str | None = None,
|
|
398
|
+
torch_backend: str = "auto"
|
|
399
|
+
) -> dict:
|
|
400
|
+
"""Create the initial pyproject.toml."""
|
|
401
|
+
config = {
|
|
402
|
+
"project": {
|
|
403
|
+
"name": f"comfygit-env-{name}",
|
|
404
|
+
"version": "0.1.0",
|
|
405
|
+
"requires-python": f">={python_version}",
|
|
406
|
+
"dependencies": []
|
|
407
|
+
},
|
|
408
|
+
"tool": {
|
|
409
|
+
"comfygit": {
|
|
410
|
+
"comfyui_version": comfyui_version,
|
|
411
|
+
"comfyui_version_type": comfyui_version_type,
|
|
412
|
+
"comfyui_commit_sha": comfyui_commit_sha,
|
|
413
|
+
"python_version": python_version,
|
|
414
|
+
"torch_backend": torch_backend,
|
|
415
|
+
"nodes": {}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return config
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Factory utility for creating UV project manager instances with consistent configuration."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from ..integrations.uv_command import UVCommand
|
|
6
|
+
from ..managers.pyproject_manager import PyprojectManager
|
|
7
|
+
from ..managers.uv_project_manager import UVProjectManager
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def create_uv_for_environment(
|
|
11
|
+
workspace_path: Path,
|
|
12
|
+
cec_path: Path | None = None,
|
|
13
|
+
venv_path: Path | None = None,
|
|
14
|
+
torch_backend: str | None = None,
|
|
15
|
+
) -> UVProjectManager:
|
|
16
|
+
"""Create a UV project manager configured for a specific environment.
|
|
17
|
+
|
|
18
|
+
This factory ensures consistent UV configuration across the codebase.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
workspace_path: Path to the workspace root
|
|
22
|
+
cec_path: Path to the .cec directory (where pyproject.toml lives)
|
|
23
|
+
venv_path: Path to the virtual environment
|
|
24
|
+
torch_backend: PyTorch backend to use (auto, cpu, cu118, cu121, etc.)
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Configured UVProjectManager instance
|
|
28
|
+
"""
|
|
29
|
+
# Workspace-level cache directories
|
|
30
|
+
uv_cache_path, uv_python_path = get_uv_cache_paths(workspace_path)
|
|
31
|
+
|
|
32
|
+
# Create UV command interface
|
|
33
|
+
uv_command = UVCommand(
|
|
34
|
+
project_env=venv_path,
|
|
35
|
+
cache_dir=uv_cache_path,
|
|
36
|
+
python_install_dir=uv_python_path,
|
|
37
|
+
link_mode="hardlink",
|
|
38
|
+
cwd=cec_path,
|
|
39
|
+
torch_backend=torch_backend,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Create PyprojectManager
|
|
43
|
+
pyproject_path = cec_path / "pyproject.toml" if cec_path else Path.cwd() / "pyproject.toml"
|
|
44
|
+
pyproject_manager = PyprojectManager(pyproject_path)
|
|
45
|
+
|
|
46
|
+
# Create and return the project manager
|
|
47
|
+
return UVProjectManager(uv_command, pyproject_manager)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_uv_cache_paths(workspace_path: Path) -> tuple[Path, Path]:
|
|
51
|
+
"""Get the standard UV cache paths for a workspace.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
workspace_path: Path to the workspace root
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Tuple of (uv_cache_dir, uv_python_install_dir)
|
|
58
|
+
"""
|
|
59
|
+
uv_cache_path = workspace_path / "uv_cache"
|
|
60
|
+
uv_python_path = workspace_path / "uv" / "python"
|
|
61
|
+
return uv_cache_path, uv_python_path
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Factory for creating and discovering workspaces."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from ..core.workspace import Workspace, WorkspacePaths
|
|
8
|
+
from ..logging.logging_config import get_logger
|
|
9
|
+
from ..models.exceptions import (
|
|
10
|
+
CDWorkspaceError,
|
|
11
|
+
CDWorkspaceExistsError,
|
|
12
|
+
CDWorkspaceNotFoundError,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
logger = get_logger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class WorkspaceFactory:
|
|
19
|
+
"""Factory for creating and discovering ComfyDock workspaces."""
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
def get_paths(path: Path | None = None) -> WorkspacePaths:
|
|
23
|
+
# Determine workspace path
|
|
24
|
+
if path:
|
|
25
|
+
workspace_path = path
|
|
26
|
+
elif comfydock_home := os.environ.get("COMFYGIT_HOME"):
|
|
27
|
+
workspace_path = Path(comfydock_home)
|
|
28
|
+
else:
|
|
29
|
+
workspace_path = Path.home() / "comfygit"
|
|
30
|
+
return WorkspacePaths(workspace_path)
|
|
31
|
+
|
|
32
|
+
@staticmethod
|
|
33
|
+
def find(path: Path | None = None) -> Workspace:
|
|
34
|
+
"""Find an existing workspace.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
path: Workspace path (defaults to ~/comfygit or COMFYGIT_HOME)
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Workspace instance
|
|
41
|
+
|
|
42
|
+
Raises:
|
|
43
|
+
CDWorkspaceNotFoundError: If workspace not found
|
|
44
|
+
"""
|
|
45
|
+
# Determine workspace path
|
|
46
|
+
workspace_paths = WorkspaceFactory.get_paths(path)
|
|
47
|
+
if not workspace_paths.exists():
|
|
48
|
+
raise CDWorkspaceNotFoundError(f"No workspace found at {workspace_paths.root}")
|
|
49
|
+
|
|
50
|
+
return Workspace(workspace_paths)
|
|
51
|
+
|
|
52
|
+
@staticmethod
|
|
53
|
+
def create(path: Path | None = None) -> Workspace:
|
|
54
|
+
"""Create a new ComfyDock workspace.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
path: Workspace directory (defaults to ~/comfygit)
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Initialized Workspace
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
CDWorkspaceExistsError: If workspace already exists
|
|
64
|
+
CDWorkspaceError: If directory exists and is not empty
|
|
65
|
+
PermissionError: If cannot create directories
|
|
66
|
+
OSError: If filesystem operations fail
|
|
67
|
+
"""
|
|
68
|
+
# Check if already exists
|
|
69
|
+
workspace_paths = WorkspaceFactory.get_paths(path)
|
|
70
|
+
if workspace_paths.exists():
|
|
71
|
+
logger.info(f"Workspace already exists at {workspace_paths.root}")
|
|
72
|
+
raise CDWorkspaceExistsError(f"Workspace already exists at {workspace_paths.root}")
|
|
73
|
+
|
|
74
|
+
# Check if path exists but is not empty
|
|
75
|
+
if workspace_paths.root.exists() and any(workspace_paths.root.iterdir()):
|
|
76
|
+
raise CDWorkspaceError(f"Directory exists and is not empty: {workspace_paths.root}")
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
# Create workspace structure (includes models/ directory)
|
|
80
|
+
workspace_paths.ensure_directories()
|
|
81
|
+
|
|
82
|
+
# Initialize metadata with default models directory
|
|
83
|
+
from datetime import datetime
|
|
84
|
+
metadata = {
|
|
85
|
+
"version": 1,
|
|
86
|
+
"active_environment": "",
|
|
87
|
+
"created_at": datetime.now().isoformat(),
|
|
88
|
+
"global_model_directory": {
|
|
89
|
+
"path": str(workspace_paths.models),
|
|
90
|
+
"added_at": datetime.now().isoformat(),
|
|
91
|
+
"last_sync": datetime.now().isoformat()
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
with open(workspace_paths.workspace_file, 'w', encoding='utf-8') as f:
|
|
96
|
+
json.dump(metadata, f, indent=2)
|
|
97
|
+
|
|
98
|
+
logger.info(f"Created workspace at {workspace_paths.root}")
|
|
99
|
+
logger.info(f"Default models directory: {workspace_paths.models}")
|
|
100
|
+
|
|
101
|
+
return Workspace(workspace_paths)
|
|
102
|
+
|
|
103
|
+
except PermissionError as e:
|
|
104
|
+
raise PermissionError(f"Cannot create workspace at {workspace_paths.root}: insufficient permissions") from e
|
|
105
|
+
except OSError as e:
|
|
106
|
+
# Clean up partial workspace if creation failed
|
|
107
|
+
if workspace_paths.exists() and not any(workspace_paths.root.iterdir()):
|
|
108
|
+
workspace_paths.root.rmdir()
|
|
109
|
+
raise OSError(f"Failed to create workspace at {workspace_paths.root}: {e}") from e
|