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,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