comfygit 0.3.6__py3-none-any.whl → 0.3.10__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-0.3.6.dist-info → comfygit-0.3.10.dist-info}/METADATA +123 -120
- {comfygit-0.3.6.dist-info → comfygit-0.3.10.dist-info}/RECORD +12 -12
- comfygit_cli/cli.py +135 -9
- comfygit_cli/cli_utils.py +1 -0
- comfygit_cli/completers.py +6 -6
- comfygit_cli/env_commands.py +379 -40
- comfygit_cli/global_commands.py +43 -65
- comfygit_cli/logging/environment_logger.py +7 -7
- comfygit_cli/utils/civitai_errors.py +1 -1
- {comfygit-0.3.6.dist-info → comfygit-0.3.10.dist-info}/WHEEL +0 -0
- {comfygit-0.3.6.dist-info → comfygit-0.3.10.dist-info}/entry_points.txt +0 -0
- {comfygit-0.3.6.dist-info → comfygit-0.3.10.dist-info}/licenses/LICENSE.txt +0 -0
comfygit_cli/env_commands.py
CHANGED
|
@@ -88,6 +88,57 @@ class EnvironmentCommands:
|
|
|
88
88
|
sys.exit(1)
|
|
89
89
|
return active
|
|
90
90
|
|
|
91
|
+
def _get_python_version(self, env: Environment) -> str:
|
|
92
|
+
"""Get Python version from environment."""
|
|
93
|
+
python_version_file = env.cec_path / ".python-version"
|
|
94
|
+
if python_version_file.exists():
|
|
95
|
+
return python_version_file.read_text(encoding="utf-8").strip()
|
|
96
|
+
return "3.12"
|
|
97
|
+
|
|
98
|
+
def _get_or_probe_backend(
|
|
99
|
+
self, env: Environment, override: str | None = None
|
|
100
|
+
) -> tuple[str, bool]:
|
|
101
|
+
"""Get torch backend from file or probe if missing.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
env: Environment to get backend for
|
|
105
|
+
override: Optional explicit backend override
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Tuple of (backend_string, was_probed) where was_probed is True
|
|
109
|
+
if we had to auto-probe because no backend was configured.
|
|
110
|
+
"""
|
|
111
|
+
if override:
|
|
112
|
+
return override, False
|
|
113
|
+
|
|
114
|
+
had_backend = env.pytorch_manager.has_backend()
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
python_version = self._get_python_version(env)
|
|
118
|
+
backend = env.pytorch_manager.ensure_backend(python_version)
|
|
119
|
+
|
|
120
|
+
was_probed = not had_backend
|
|
121
|
+
if was_probed:
|
|
122
|
+
print("⚠️ No PyTorch backend configured. Auto-detecting...")
|
|
123
|
+
print(f"✓ Backend detected and saved: {backend}")
|
|
124
|
+
print(" To change: cg env-config torch-backend set <backend>")
|
|
125
|
+
|
|
126
|
+
return backend, was_probed
|
|
127
|
+
except Exception as e:
|
|
128
|
+
print(f"✗ Error probing PyTorch backend: {e}")
|
|
129
|
+
print(" Try setting it explicitly: cg env-config torch-backend set <backend>")
|
|
130
|
+
sys.exit(1)
|
|
131
|
+
|
|
132
|
+
def _show_legacy_manager_notice(self, env: Environment) -> None:
|
|
133
|
+
"""Show legacy manager notice if environment uses symlinked manager."""
|
|
134
|
+
try:
|
|
135
|
+
status = env.get_manager_status()
|
|
136
|
+
if status.is_legacy:
|
|
137
|
+
print("")
|
|
138
|
+
print("Legacy manager detected. Run 'cg manager update' to migrate.")
|
|
139
|
+
except Exception:
|
|
140
|
+
pass # Silently fail - notice is informational only
|
|
141
|
+
|
|
91
142
|
def _format_size(self, size_bytes: int) -> str:
|
|
92
143
|
"""Format bytes as human-readable size."""
|
|
93
144
|
for unit in ("B", "KB", "MB", "GB"):
|
|
@@ -174,7 +225,7 @@ class EnvironmentCommands:
|
|
|
174
225
|
|
|
175
226
|
# === Commands that operate ON environments ===
|
|
176
227
|
|
|
177
|
-
@with_env_logging("
|
|
228
|
+
@with_env_logging("create")
|
|
178
229
|
def create(self, args: argparse.Namespace, logger=None) -> None:
|
|
179
230
|
"""Create a new environment."""
|
|
180
231
|
# Ensure workspace exists, creating it if necessary
|
|
@@ -220,7 +271,7 @@ class EnvironmentCommands:
|
|
|
220
271
|
print(f" • Add nodes: cg -e {args.name} node add <node-name>")
|
|
221
272
|
print(f" • Set as active: cg use {args.name}")
|
|
222
273
|
|
|
223
|
-
@with_env_logging("
|
|
274
|
+
@with_env_logging("use")
|
|
224
275
|
def use(self, args: argparse.Namespace, logger=None) -> None:
|
|
225
276
|
"""Set the active environment."""
|
|
226
277
|
from comfygit_cli.utils.progress import create_model_sync_progress
|
|
@@ -237,7 +288,7 @@ class EnvironmentCommands:
|
|
|
237
288
|
print(f"✓ Active environment set to: {args.name}")
|
|
238
289
|
print("You can now run commands without the -e flag")
|
|
239
290
|
|
|
240
|
-
@with_env_logging("
|
|
291
|
+
@with_env_logging("delete")
|
|
241
292
|
def delete(self, args: argparse.Namespace, logger=None) -> None:
|
|
242
293
|
"""Delete an environment."""
|
|
243
294
|
# Check that environment exists (don't require active environment)
|
|
@@ -270,7 +321,122 @@ class EnvironmentCommands:
|
|
|
270
321
|
|
|
271
322
|
# === Commands that operate IN environments ===
|
|
272
323
|
|
|
273
|
-
|
|
324
|
+
# === Environment Configuration ===
|
|
325
|
+
|
|
326
|
+
@with_env_logging("env-config torch-backend show")
|
|
327
|
+
def env_config_torch_show(self, args: argparse.Namespace, logger=None) -> None:
|
|
328
|
+
"""Show current PyTorch backend setting for this environment."""
|
|
329
|
+
env = self._get_env(args)
|
|
330
|
+
|
|
331
|
+
backend = env.pytorch_manager.get_backend()
|
|
332
|
+
backend_file = env.pytorch_manager.backend_file
|
|
333
|
+
versions = env.pytorch_manager.get_versions()
|
|
334
|
+
|
|
335
|
+
print(f"PyTorch Backend: {backend}")
|
|
336
|
+
if versions:
|
|
337
|
+
for pkg, ver in versions.items():
|
|
338
|
+
print(f" {pkg}={ver}")
|
|
339
|
+
|
|
340
|
+
if backend_file.exists():
|
|
341
|
+
print(f" Source: {backend_file}")
|
|
342
|
+
else:
|
|
343
|
+
print(" Source: auto-detected (no .pytorch-backend file)")
|
|
344
|
+
print()
|
|
345
|
+
print(f"💡 To save this setting: cg env-config torch-backend set {backend}")
|
|
346
|
+
|
|
347
|
+
@with_env_logging("env-config torch-backend set")
|
|
348
|
+
def env_config_torch_set(self, args: argparse.Namespace, logger=None) -> None:
|
|
349
|
+
"""Set PyTorch backend for this environment.
|
|
350
|
+
|
|
351
|
+
Probes for exact versions and stores both backend and version pins.
|
|
352
|
+
"""
|
|
353
|
+
from comfygit_core.utils.pytorch_prober import PyTorchProbeError
|
|
354
|
+
|
|
355
|
+
env = self._get_env(args)
|
|
356
|
+
backend = args.backend
|
|
357
|
+
|
|
358
|
+
# Validate backend format
|
|
359
|
+
if not env.pytorch_manager.is_valid_backend(backend):
|
|
360
|
+
print(f"✗ Invalid backend: {backend}")
|
|
361
|
+
print()
|
|
362
|
+
print("Valid formats:")
|
|
363
|
+
print(" • cu118, cu121, cu124, cu126, cu128 (CUDA)")
|
|
364
|
+
print(" • cpu")
|
|
365
|
+
print(" • rocm6.2, rocm6.3 (AMD)")
|
|
366
|
+
print(" • xpu (Intel)")
|
|
367
|
+
sys.exit(1)
|
|
368
|
+
|
|
369
|
+
# Read python version
|
|
370
|
+
python_version_file = env.cec_path / ".python-version"
|
|
371
|
+
python_version = (
|
|
372
|
+
python_version_file.read_text(encoding="utf-8").strip()
|
|
373
|
+
if python_version_file.exists()
|
|
374
|
+
else "3.12"
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
# Probe and set backend with versions
|
|
378
|
+
print(f"🔍 Probing PyTorch versions for {backend} (Python {python_version})...")
|
|
379
|
+
try:
|
|
380
|
+
resolved = env.pytorch_manager.probe_and_set_backend(python_version, backend)
|
|
381
|
+
except PyTorchProbeError as e:
|
|
382
|
+
print(f"✗ Error probing PyTorch: {e}")
|
|
383
|
+
sys.exit(1)
|
|
384
|
+
|
|
385
|
+
# Show what was stored
|
|
386
|
+
versions = env.pytorch_manager.get_versions()
|
|
387
|
+
print(f"✓ PyTorch backend set to: {resolved}")
|
|
388
|
+
if versions:
|
|
389
|
+
for pkg, ver in versions.items():
|
|
390
|
+
print(f" {pkg}={ver}")
|
|
391
|
+
print()
|
|
392
|
+
print("Run 'cg sync' to apply the new backend configuration.")
|
|
393
|
+
|
|
394
|
+
@with_env_logging("env-config torch-backend detect")
|
|
395
|
+
def env_config_torch_detect(self, args: argparse.Namespace, logger=None) -> None:
|
|
396
|
+
"""Auto-detect recommended PyTorch backend using uv probe."""
|
|
397
|
+
from comfygit_core.utils.pytorch_prober import PyTorchProbeError, probe_pytorch_versions
|
|
398
|
+
|
|
399
|
+
env = self._get_env(args)
|
|
400
|
+
backend_file = env.pytorch_manager.backend_file
|
|
401
|
+
|
|
402
|
+
# Read python version from file
|
|
403
|
+
python_version_file = env.cec_path / ".python-version"
|
|
404
|
+
python_version = (
|
|
405
|
+
python_version_file.read_text(encoding="utf-8").strip()
|
|
406
|
+
if python_version_file.exists()
|
|
407
|
+
else "3.12"
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
# Probe for recommended backend
|
|
411
|
+
print(f"🔍 Probing PyTorch compatibility for Python {python_version}...")
|
|
412
|
+
try:
|
|
413
|
+
_, detected = probe_pytorch_versions(python_version, "auto")
|
|
414
|
+
except PyTorchProbeError as e:
|
|
415
|
+
print(f"✗ Error probing PyTorch: {e}")
|
|
416
|
+
sys.exit(1)
|
|
417
|
+
|
|
418
|
+
# Get current backend (if any)
|
|
419
|
+
if env.pytorch_manager.has_backend():
|
|
420
|
+
current = env.pytorch_manager.get_backend()
|
|
421
|
+
else:
|
|
422
|
+
current = "(not configured)"
|
|
423
|
+
|
|
424
|
+
print(f"Detected backend: {detected}")
|
|
425
|
+
print(f"Current backend: {current}")
|
|
426
|
+
|
|
427
|
+
if backend_file.exists():
|
|
428
|
+
print(f" Source: {backend_file}")
|
|
429
|
+
else:
|
|
430
|
+
print(" Source: not configured")
|
|
431
|
+
|
|
432
|
+
if current != detected and current != "(not configured)":
|
|
433
|
+
print()
|
|
434
|
+
print(f"💡 Consider updating: cg env-config torch-backend set {detected}")
|
|
435
|
+
elif current == "(not configured)":
|
|
436
|
+
print()
|
|
437
|
+
print(f"💡 Set the backend: cg env-config torch-backend set {detected}")
|
|
438
|
+
|
|
439
|
+
@with_env_logging("run")
|
|
274
440
|
def run(self, args: argparse.Namespace) -> None:
|
|
275
441
|
"""Run ComfyUI in the specified environment."""
|
|
276
442
|
RESTART_EXIT_CODE = 42
|
|
@@ -278,14 +444,32 @@ class EnvironmentCommands:
|
|
|
278
444
|
comfyui_args = args.args if hasattr(args, 'args') else []
|
|
279
445
|
no_sync = getattr(args, 'no_sync', False)
|
|
280
446
|
|
|
447
|
+
# Handle torch-backend: use override, read from file, or probe if missing
|
|
448
|
+
torch_backend_override = getattr(args, 'torch_backend', None)
|
|
449
|
+
torch_backend, was_probed = self._get_or_probe_backend(env, torch_backend_override)
|
|
450
|
+
|
|
451
|
+
if torch_backend_override:
|
|
452
|
+
print(f"🔧 Using PyTorch backend override: {torch_backend}")
|
|
453
|
+
elif was_probed:
|
|
454
|
+
print(f"✓ Backend detected and saved: {torch_backend}")
|
|
455
|
+
print(f" To change: cg env-config torch-backend set <backend>")
|
|
456
|
+
else:
|
|
457
|
+
print(f"🔧 Using PyTorch backend: {torch_backend}")
|
|
458
|
+
|
|
281
459
|
current_branch = env.get_current_branch()
|
|
282
460
|
branch_display = f" (on {current_branch})" if current_branch else " (detached HEAD)"
|
|
283
461
|
|
|
284
462
|
while True:
|
|
285
463
|
# Sync before running (unless --no-sync)
|
|
464
|
+
# Use explicit override if provided, otherwise None (backend is now in file)
|
|
286
465
|
if not no_sync:
|
|
287
466
|
print(f"🔄 Syncing environment: {env.name}")
|
|
288
|
-
env.sync(
|
|
467
|
+
env.sync(
|
|
468
|
+
preserve_workflows=True,
|
|
469
|
+
remove_extra_nodes=False,
|
|
470
|
+
backend_override=torch_backend_override if torch_backend_override else None,
|
|
471
|
+
verbose=True,
|
|
472
|
+
)
|
|
289
473
|
|
|
290
474
|
print(f"🎮 Starting ComfyUI in environment: {env.name}{branch_display}")
|
|
291
475
|
if comfyui_args:
|
|
@@ -300,6 +484,54 @@ class EnvironmentCommands:
|
|
|
300
484
|
|
|
301
485
|
sys.exit(result.returncode)
|
|
302
486
|
|
|
487
|
+
@with_env_logging("sync")
|
|
488
|
+
def sync(self, args: argparse.Namespace, logger=None) -> None:
|
|
489
|
+
"""Sync environment packages and dependencies."""
|
|
490
|
+
env = self._get_env(args)
|
|
491
|
+
|
|
492
|
+
# Handle torch-backend: use override, read from file, or probe if missing
|
|
493
|
+
torch_backend_override = getattr(args, 'torch_backend', None)
|
|
494
|
+
torch_backend, was_probed = self._get_or_probe_backend(env, torch_backend_override)
|
|
495
|
+
|
|
496
|
+
if torch_backend_override:
|
|
497
|
+
print(f"🔧 Using PyTorch backend override: {torch_backend}")
|
|
498
|
+
elif was_probed:
|
|
499
|
+
print(f"✓ Backend detected and saved: {torch_backend}")
|
|
500
|
+
print(f" To change: cg env-config torch-backend set <backend>")
|
|
501
|
+
else:
|
|
502
|
+
print(f"🔧 Using PyTorch backend: {torch_backend}")
|
|
503
|
+
|
|
504
|
+
print(f"\n🔄 Syncing environment: {env.name}")
|
|
505
|
+
|
|
506
|
+
verbose = getattr(args, 'verbose', False)
|
|
507
|
+
|
|
508
|
+
try:
|
|
509
|
+
# Use explicit override if provided, otherwise None (backend is now in file)
|
|
510
|
+
result = env.sync(
|
|
511
|
+
dry_run=False,
|
|
512
|
+
model_strategy="skip", # Sync command focuses on packages
|
|
513
|
+
remove_extra_nodes=False, # Don't remove nodes, just sync
|
|
514
|
+
verbose=verbose,
|
|
515
|
+
backend_override=torch_backend_override if torch_backend_override else None,
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
if result.success:
|
|
519
|
+
print("\n✓ Sync complete")
|
|
520
|
+
if result.packages_synced:
|
|
521
|
+
print(f" Packages synced: {result.packages_synced}")
|
|
522
|
+
if result.dependency_groups_installed:
|
|
523
|
+
print(f" Dependency groups: {', '.join(result.dependency_groups_installed)}")
|
|
524
|
+
else:
|
|
525
|
+
print("\n⚠️ Sync completed with warnings")
|
|
526
|
+
for error in result.errors:
|
|
527
|
+
print(f" • {error}")
|
|
528
|
+
|
|
529
|
+
except Exception as e:
|
|
530
|
+
if logger:
|
|
531
|
+
logger.error(f"Sync failed: {e}", exc_info=True)
|
|
532
|
+
print(f"\n✗ Sync failed: {e}", file=sys.stderr)
|
|
533
|
+
sys.exit(1)
|
|
534
|
+
|
|
303
535
|
def manifest(self, args: argparse.Namespace) -> None:
|
|
304
536
|
"""Show environment manifest (pyproject.toml configuration)."""
|
|
305
537
|
env = self._get_env(args)
|
|
@@ -359,7 +591,7 @@ class EnvironmentCommands:
|
|
|
359
591
|
# Default: raw TOML (exact file representation)
|
|
360
592
|
print(tomlkit.dumps(config))
|
|
361
593
|
|
|
362
|
-
@with_env_logging("
|
|
594
|
+
@with_env_logging("status")
|
|
363
595
|
def status(self, args: argparse.Namespace) -> None:
|
|
364
596
|
"""Show environment status using semantic methods."""
|
|
365
597
|
env = self._get_env(args)
|
|
@@ -391,6 +623,9 @@ class EnvironmentCommands:
|
|
|
391
623
|
|
|
392
624
|
print("\n✓ No workflows")
|
|
393
625
|
print("✓ No uncommitted changes")
|
|
626
|
+
|
|
627
|
+
# Show legacy manager notice even in clean state
|
|
628
|
+
self._show_legacy_manager_notice(env)
|
|
394
629
|
return
|
|
395
630
|
|
|
396
631
|
# Show environment name with branch
|
|
@@ -541,6 +776,9 @@ class EnvironmentCommands:
|
|
|
541
776
|
# Suggested actions - smart and contextual
|
|
542
777
|
self._show_smart_suggestions(status)
|
|
543
778
|
|
|
779
|
+
# Show legacy manager notice if applicable
|
|
780
|
+
self._show_legacy_manager_notice(env)
|
|
781
|
+
|
|
544
782
|
# Removed: _has_uninstalled_packages - this logic is now in core's WorkflowAnalysisStatus
|
|
545
783
|
|
|
546
784
|
def _print_workflow_issues(self, wf_analysis: WorkflowAnalysisStatus, verbose: bool = False) -> None:
|
|
@@ -867,7 +1105,7 @@ class EnvironmentCommands:
|
|
|
867
1105
|
|
|
868
1106
|
# === Node management ===
|
|
869
1107
|
|
|
870
|
-
@with_env_logging("
|
|
1108
|
+
@with_env_logging("node add")
|
|
871
1109
|
def node_add(self, args: argparse.Namespace, logger=None) -> None:
|
|
872
1110
|
"""Add custom node(s) - directly modifies pyproject.toml."""
|
|
873
1111
|
env = self._get_env(args)
|
|
@@ -906,7 +1144,7 @@ class EnvironmentCommands:
|
|
|
906
1144
|
for node_id, error in failed_nodes:
|
|
907
1145
|
print(f" • {node_id}: {error}")
|
|
908
1146
|
|
|
909
|
-
print(f"\nRun 'cg -e {env.name}
|
|
1147
|
+
print(f"\nRun 'cg -e {env.name} status' to review changes")
|
|
910
1148
|
return
|
|
911
1149
|
|
|
912
1150
|
# Single node mode (original behavior)
|
|
@@ -965,9 +1203,9 @@ class EnvironmentCommands:
|
|
|
965
1203
|
else:
|
|
966
1204
|
print(f"✓ Node '{node_info.name}' added to pyproject.toml")
|
|
967
1205
|
|
|
968
|
-
print(f"\nRun 'cg -e {env.name}
|
|
1206
|
+
print(f"\nRun 'cg -e {env.name} status' to review changes")
|
|
969
1207
|
|
|
970
|
-
@with_env_logging("
|
|
1208
|
+
@with_env_logging("node remove")
|
|
971
1209
|
def node_remove(self, args: argparse.Namespace, logger=None) -> None:
|
|
972
1210
|
"""Remove custom node(s) - handles filesystem immediately."""
|
|
973
1211
|
env = self._get_env(args)
|
|
@@ -1006,7 +1244,7 @@ class EnvironmentCommands:
|
|
|
1006
1244
|
for node_id, error in failed_nodes:
|
|
1007
1245
|
print(f" • {node_id}: {error}")
|
|
1008
1246
|
|
|
1009
|
-
print(f"\nRun 'cg -e {env.name}
|
|
1247
|
+
print(f"\nRun 'cg -e {env.name} status' to review changes")
|
|
1010
1248
|
return
|
|
1011
1249
|
|
|
1012
1250
|
# Single node mode (original behavior)
|
|
@@ -1044,9 +1282,9 @@ class EnvironmentCommands:
|
|
|
1044
1282
|
if result.filesystem_action == "deleted":
|
|
1045
1283
|
print(" (cached globally, can reinstall)")
|
|
1046
1284
|
|
|
1047
|
-
print(f"\nRun 'cg -e {env.name}
|
|
1285
|
+
print(f"\nRun 'cg -e {env.name} status' to review changes")
|
|
1048
1286
|
|
|
1049
|
-
@with_env_logging("
|
|
1287
|
+
@with_env_logging("node prune")
|
|
1050
1288
|
def node_prune(self, args: argparse.Namespace, logger=None) -> None:
|
|
1051
1289
|
"""Remove unused custom nodes from environment."""
|
|
1052
1290
|
env = self._get_env(args)
|
|
@@ -1115,7 +1353,7 @@ class EnvironmentCommands:
|
|
|
1115
1353
|
print(f" • {node_id}: {error}")
|
|
1116
1354
|
sys.exit(1)
|
|
1117
1355
|
|
|
1118
|
-
@with_env_logging("
|
|
1356
|
+
@with_env_logging("node list")
|
|
1119
1357
|
def node_list(self, args: argparse.Namespace) -> None:
|
|
1120
1358
|
"""List custom nodes in the environment."""
|
|
1121
1359
|
env = self._get_env(args)
|
|
@@ -1140,7 +1378,7 @@ class EnvironmentCommands:
|
|
|
1140
1378
|
|
|
1141
1379
|
print(f" • {node.registry_id or node.name} ({node.source}){version_suffix}")
|
|
1142
1380
|
|
|
1143
|
-
@with_env_logging("
|
|
1381
|
+
@with_env_logging("node update")
|
|
1144
1382
|
def node_update(self, args: argparse.Namespace, logger=None) -> None:
|
|
1145
1383
|
"""Update a custom node."""
|
|
1146
1384
|
from comfygit_core.strategies.confirmation import (
|
|
@@ -1188,7 +1426,7 @@ class EnvironmentCommands:
|
|
|
1188
1426
|
|
|
1189
1427
|
# === Constraint management ===
|
|
1190
1428
|
|
|
1191
|
-
@with_env_logging("
|
|
1429
|
+
@with_env_logging("constraint add")
|
|
1192
1430
|
def constraint_add(self, args: argparse.Namespace, logger=None) -> None:
|
|
1193
1431
|
"""Add constraint dependencies to [tool.uv]."""
|
|
1194
1432
|
env = self._get_env(args)
|
|
@@ -1209,7 +1447,7 @@ class EnvironmentCommands:
|
|
|
1209
1447
|
print(f"✓ Added {len(args.packages)} constraint(s) to pyproject.toml")
|
|
1210
1448
|
print(f"\nRun 'cg -e {env.name} constraint list' to view all constraints")
|
|
1211
1449
|
|
|
1212
|
-
@with_env_logging("
|
|
1450
|
+
@with_env_logging("constraint list")
|
|
1213
1451
|
def constraint_list(self, args: argparse.Namespace) -> None:
|
|
1214
1452
|
"""List constraint dependencies from [tool.uv]."""
|
|
1215
1453
|
env = self._get_env(args)
|
|
@@ -1225,7 +1463,7 @@ class EnvironmentCommands:
|
|
|
1225
1463
|
for constraint in constraints:
|
|
1226
1464
|
print(f" • {constraint}")
|
|
1227
1465
|
|
|
1228
|
-
@with_env_logging("
|
|
1466
|
+
@with_env_logging("constraint remove")
|
|
1229
1467
|
def constraint_remove(self, args: argparse.Namespace, logger=None) -> None:
|
|
1230
1468
|
"""Remove constraint dependencies from [tool.uv]."""
|
|
1231
1469
|
env = self._get_env(args)
|
|
@@ -1256,7 +1494,7 @@ class EnvironmentCommands:
|
|
|
1256
1494
|
|
|
1257
1495
|
# === Python dependency management ===
|
|
1258
1496
|
|
|
1259
|
-
@with_env_logging("
|
|
1497
|
+
@with_env_logging("py add")
|
|
1260
1498
|
def py_add(self, args: argparse.Namespace, logger=None) -> None:
|
|
1261
1499
|
"""Add Python dependencies to the environment."""
|
|
1262
1500
|
env = self._get_env(args)
|
|
@@ -1316,7 +1554,7 @@ class EnvironmentCommands:
|
|
|
1316
1554
|
print(f"\n✓ Added {len(args.packages)} package(s) to dependencies")
|
|
1317
1555
|
print(f"\nRun 'cg -e {env.name} status' to review changes")
|
|
1318
1556
|
|
|
1319
|
-
@with_env_logging("
|
|
1557
|
+
@with_env_logging("py remove")
|
|
1320
1558
|
def py_remove(self, args: argparse.Namespace, logger=None) -> None:
|
|
1321
1559
|
"""Remove Python dependencies from the environment."""
|
|
1322
1560
|
env = self._get_env(args)
|
|
@@ -1390,7 +1628,7 @@ class EnvironmentCommands:
|
|
|
1390
1628
|
|
|
1391
1629
|
print(f"\nRun 'cg -e {env.name} status' to review changes")
|
|
1392
1630
|
|
|
1393
|
-
@with_env_logging("
|
|
1631
|
+
@with_env_logging("py remove-group")
|
|
1394
1632
|
def py_remove_group(self, args: argparse.Namespace, logger=None) -> None:
|
|
1395
1633
|
"""Remove an entire dependency group."""
|
|
1396
1634
|
env = self._get_env(args)
|
|
@@ -1407,7 +1645,7 @@ class EnvironmentCommands:
|
|
|
1407
1645
|
print(f"\n✓ Removed dependency group '{group_name}'")
|
|
1408
1646
|
print(f"\nRun 'cg -e {env.name} py list --all' to view remaining groups")
|
|
1409
1647
|
|
|
1410
|
-
@with_env_logging("
|
|
1648
|
+
@with_env_logging("py uv")
|
|
1411
1649
|
def py_uv(self, args: argparse.Namespace, logger=None) -> None:
|
|
1412
1650
|
"""Direct UV command passthrough for advanced users."""
|
|
1413
1651
|
env = self._get_env(args)
|
|
@@ -1434,7 +1672,7 @@ class EnvironmentCommands:
|
|
|
1434
1672
|
)
|
|
1435
1673
|
sys.exit(result.returncode)
|
|
1436
1674
|
|
|
1437
|
-
@with_env_logging("
|
|
1675
|
+
@with_env_logging("py list")
|
|
1438
1676
|
def py_list(self, args: argparse.Namespace) -> None:
|
|
1439
1677
|
"""List Python dependencies."""
|
|
1440
1678
|
env = self._get_env(args)
|
|
@@ -1473,7 +1711,7 @@ class EnvironmentCommands:
|
|
|
1473
1711
|
|
|
1474
1712
|
# === Git-based operations ===
|
|
1475
1713
|
|
|
1476
|
-
@with_env_logging("
|
|
1714
|
+
@with_env_logging("repair")
|
|
1477
1715
|
def repair(self, args: argparse.Namespace, logger=None) -> None:
|
|
1478
1716
|
"""Repair environment to match pyproject.toml (for manual edits or git operations)."""
|
|
1479
1717
|
env = self._get_env(args)
|
|
@@ -1653,7 +1891,7 @@ class EnvironmentCommands:
|
|
|
1653
1891
|
print("✓ Changes applied successfully!")
|
|
1654
1892
|
print(f"\nEnvironment '{env.name}' is ready to use")
|
|
1655
1893
|
|
|
1656
|
-
@with_env_logging("
|
|
1894
|
+
@with_env_logging("checkout")
|
|
1657
1895
|
def checkout(self, args: argparse.Namespace, logger=None) -> None:
|
|
1658
1896
|
"""Checkout commits, branches, or files."""
|
|
1659
1897
|
from .strategies.rollback import AutoRollbackStrategy, InteractiveRollbackStrategy
|
|
@@ -1694,7 +1932,7 @@ class EnvironmentCommands:
|
|
|
1694
1932
|
print(f"✗ Checkout failed: {e}", file=sys.stderr)
|
|
1695
1933
|
sys.exit(1)
|
|
1696
1934
|
|
|
1697
|
-
@with_env_logging("
|
|
1935
|
+
@with_env_logging("branch")
|
|
1698
1936
|
def branch(self, args: argparse.Namespace, logger=None) -> None:
|
|
1699
1937
|
"""Manage branches."""
|
|
1700
1938
|
env = self._get_env(args)
|
|
@@ -1738,7 +1976,7 @@ class EnvironmentCommands:
|
|
|
1738
1976
|
print(f"✗ Branch operation failed: {e}", file=sys.stderr)
|
|
1739
1977
|
sys.exit(1)
|
|
1740
1978
|
|
|
1741
|
-
@with_env_logging("
|
|
1979
|
+
@with_env_logging("switch")
|
|
1742
1980
|
def switch(self, args: argparse.Namespace, logger=None) -> None:
|
|
1743
1981
|
"""Switch to branch."""
|
|
1744
1982
|
env = self._get_env(args)
|
|
@@ -1753,7 +1991,7 @@ class EnvironmentCommands:
|
|
|
1753
1991
|
print(f"✗ Switch failed: {e}", file=sys.stderr)
|
|
1754
1992
|
sys.exit(1)
|
|
1755
1993
|
|
|
1756
|
-
@with_env_logging("
|
|
1994
|
+
@with_env_logging("reset")
|
|
1757
1995
|
def reset_git(self, args: argparse.Namespace, logger=None) -> None:
|
|
1758
1996
|
"""Reset HEAD to ref (git-native reset)."""
|
|
1759
1997
|
from .strategies.rollback import InteractiveRollbackStrategy
|
|
@@ -1783,7 +2021,7 @@ class EnvironmentCommands:
|
|
|
1783
2021
|
print(f"✗ Reset failed: {e}", file=sys.stderr)
|
|
1784
2022
|
sys.exit(1)
|
|
1785
2023
|
|
|
1786
|
-
@with_env_logging("
|
|
2024
|
+
@with_env_logging("merge")
|
|
1787
2025
|
def merge(self, args: argparse.Namespace, logger=None) -> None:
|
|
1788
2026
|
"""Merge branch into current with atomic conflict resolution."""
|
|
1789
2027
|
env = self._get_env(args)
|
|
@@ -1878,7 +2116,7 @@ class EnvironmentCommands:
|
|
|
1878
2116
|
print(f"✗ Merge failed: {e}", file=sys.stderr)
|
|
1879
2117
|
sys.exit(1)
|
|
1880
2118
|
|
|
1881
|
-
@with_env_logging("
|
|
2119
|
+
@with_env_logging("revert")
|
|
1882
2120
|
def revert(self, args: argparse.Namespace, logger=None) -> None:
|
|
1883
2121
|
"""Revert a commit."""
|
|
1884
2122
|
env = self._get_env(args)
|
|
@@ -1893,7 +2131,7 @@ class EnvironmentCommands:
|
|
|
1893
2131
|
print(f"✗ Revert failed: {e}", file=sys.stderr)
|
|
1894
2132
|
sys.exit(1)
|
|
1895
2133
|
|
|
1896
|
-
@with_env_logging("
|
|
2134
|
+
@with_env_logging("commit")
|
|
1897
2135
|
def commit(self, args: argparse.Namespace, logger=None) -> None:
|
|
1898
2136
|
"""Commit workflows with optional issue resolution."""
|
|
1899
2137
|
env = self._get_env(args)
|
|
@@ -1976,7 +2214,7 @@ class EnvironmentCommands:
|
|
|
1976
2214
|
|
|
1977
2215
|
# === Git remote operations ===
|
|
1978
2216
|
|
|
1979
|
-
@with_env_logging("
|
|
2217
|
+
@with_env_logging("pull")
|
|
1980
2218
|
def pull(self, args: argparse.Namespace, logger=None) -> None:
|
|
1981
2219
|
"""Pull from remote and repair environment."""
|
|
1982
2220
|
env = self._get_env(args)
|
|
@@ -1985,15 +2223,23 @@ class EnvironmentCommands:
|
|
|
1985
2223
|
if not env.git_manager.has_remote(args.remote):
|
|
1986
2224
|
print(f"✗ Remote '{args.remote}' not configured")
|
|
1987
2225
|
print()
|
|
1988
|
-
|
|
1989
|
-
|
|
2226
|
+
# Check if other remotes exist
|
|
2227
|
+
remotes = env.git_manager.list_remotes()
|
|
2228
|
+
if remotes:
|
|
2229
|
+
remote_names = list({r[0] for r in remotes}) # Unique names
|
|
2230
|
+
print("💡 Use an existing remote:")
|
|
2231
|
+
print(f" cg pull -r {remote_names[0]}")
|
|
2232
|
+
else:
|
|
2233
|
+
print("💡 Set up a remote first:")
|
|
2234
|
+
print(f" cg remote add {args.remote} <url>")
|
|
1990
2235
|
sys.exit(1)
|
|
1991
2236
|
|
|
1992
2237
|
# Preview mode - read-only, just show what would change
|
|
2238
|
+
branch = getattr(args, 'branch', None)
|
|
1993
2239
|
if getattr(args, "preview", False):
|
|
1994
2240
|
try:
|
|
1995
2241
|
print(f"Fetching from {args.remote}...")
|
|
1996
|
-
diff = env.preview_pull(remote=args.remote)
|
|
2242
|
+
diff = env.preview_pull(remote=args.remote, branch=branch)
|
|
1997
2243
|
|
|
1998
2244
|
if not diff.has_changes:
|
|
1999
2245
|
if diff.is_already_merged:
|
|
@@ -2037,7 +2283,7 @@ class EnvironmentCommands:
|
|
|
2037
2283
|
else:
|
|
2038
2284
|
# Check for conflicts before pull
|
|
2039
2285
|
print(f"Checking for conflicts with {args.remote}...")
|
|
2040
|
-
diff = env.preview_pull(remote=args.remote)
|
|
2286
|
+
diff = env.preview_pull(remote=args.remote, branch=branch)
|
|
2041
2287
|
if diff.has_conflicts:
|
|
2042
2288
|
# Interactive conflict resolution
|
|
2043
2289
|
from .strategies.conflict_resolver import InteractiveConflictResolver
|
|
@@ -2066,6 +2312,18 @@ class EnvironmentCommands:
|
|
|
2066
2312
|
|
|
2067
2313
|
print(f"📥 Pulling from {args.remote}...")
|
|
2068
2314
|
|
|
2315
|
+
# Handle torch-backend: use override, read from file, or probe if missing
|
|
2316
|
+
torch_backend_override = getattr(args, 'torch_backend', None)
|
|
2317
|
+
torch_backend, was_probed = self._get_or_probe_backend(env, torch_backend_override)
|
|
2318
|
+
|
|
2319
|
+
if torch_backend_override:
|
|
2320
|
+
print(f"🔧 Using PyTorch backend override: {torch_backend}")
|
|
2321
|
+
elif was_probed:
|
|
2322
|
+
print(f"🔧 Auto-detected PyTorch backend: {torch_backend}")
|
|
2323
|
+
print(f" To save: cg env-config torch-backend set {torch_backend}")
|
|
2324
|
+
else:
|
|
2325
|
+
print(f"🔧 Using PyTorch backend: {torch_backend}")
|
|
2326
|
+
|
|
2069
2327
|
# Create callbacks for node and model progress (reuse repair command patterns)
|
|
2070
2328
|
from comfygit_core.models.workflow import BatchDownloadCallbacks, NodeInstallCallbacks
|
|
2071
2329
|
from .utils.progress import create_progress_callback
|
|
@@ -2103,12 +2361,16 @@ class EnvironmentCommands:
|
|
|
2103
2361
|
)
|
|
2104
2362
|
|
|
2105
2363
|
# Pull and repair with progress callbacks
|
|
2364
|
+
force = getattr(args, 'force', False)
|
|
2106
2365
|
result = env.pull_and_repair(
|
|
2107
2366
|
remote=args.remote,
|
|
2367
|
+
branch=branch,
|
|
2108
2368
|
model_strategy=getattr(args, 'models', 'all'),
|
|
2109
2369
|
model_callbacks=model_callbacks,
|
|
2110
2370
|
node_callbacks=node_callbacks,
|
|
2111
2371
|
strategy_option=strategy_option,
|
|
2372
|
+
force=force,
|
|
2373
|
+
backend_override=torch_backend,
|
|
2112
2374
|
)
|
|
2113
2375
|
|
|
2114
2376
|
# Extract sync result for summary
|
|
@@ -2184,7 +2446,7 @@ class EnvironmentCommands:
|
|
|
2184
2446
|
print(f"✗ Pull failed: {e}", file=sys.stderr)
|
|
2185
2447
|
sys.exit(1)
|
|
2186
2448
|
|
|
2187
|
-
@with_env_logging("
|
|
2449
|
+
@with_env_logging("push")
|
|
2188
2450
|
def push(self, args: argparse.Namespace, logger=None) -> None:
|
|
2189
2451
|
"""Push commits to remote."""
|
|
2190
2452
|
env = self._get_env(args)
|
|
@@ -2201,8 +2463,15 @@ class EnvironmentCommands:
|
|
|
2201
2463
|
if not env.git_manager.has_remote(args.remote):
|
|
2202
2464
|
print(f"✗ Remote '{args.remote}' not configured")
|
|
2203
2465
|
print()
|
|
2204
|
-
|
|
2205
|
-
|
|
2466
|
+
# Check if other remotes exist
|
|
2467
|
+
remotes = env.git_manager.list_remotes()
|
|
2468
|
+
if remotes:
|
|
2469
|
+
remote_names = list({r[0] for r in remotes}) # Unique names
|
|
2470
|
+
print("💡 Use an existing remote:")
|
|
2471
|
+
print(f" cg push -r {remote_names[0]}")
|
|
2472
|
+
else:
|
|
2473
|
+
print("💡 Set up a remote first:")
|
|
2474
|
+
print(f" cg remote add {args.remote} <url>")
|
|
2206
2475
|
sys.exit(1)
|
|
2207
2476
|
|
|
2208
2477
|
try:
|
|
@@ -2246,7 +2515,7 @@ class EnvironmentCommands:
|
|
|
2246
2515
|
print(f"✗ Push failed: {e}", file=sys.stderr)
|
|
2247
2516
|
sys.exit(1)
|
|
2248
2517
|
|
|
2249
|
-
@with_env_logging("
|
|
2518
|
+
@with_env_logging("remote")
|
|
2250
2519
|
def remote(self, args: argparse.Namespace, logger=None) -> None:
|
|
2251
2520
|
"""Manage git remotes."""
|
|
2252
2521
|
env = self._get_env(args)
|
|
@@ -2699,3 +2968,73 @@ class EnvironmentCommands:
|
|
|
2699
2968
|
print(f" {m.actual_category}/{m.name} → {expected}/")
|
|
2700
2969
|
else:
|
|
2701
2970
|
print("✓ No changes needed - all dependencies already resolved")
|
|
2971
|
+
|
|
2972
|
+
# ================================================================================
|
|
2973
|
+
# Manager Commands - Per-environment comfygit-manager management
|
|
2974
|
+
# ================================================================================
|
|
2975
|
+
|
|
2976
|
+
@with_env_logging("manager status")
|
|
2977
|
+
def manager_status(self, args: argparse.Namespace, logger: Any = None) -> None:
|
|
2978
|
+
"""Show manager version and update availability."""
|
|
2979
|
+
env = self._get_env(args)
|
|
2980
|
+
|
|
2981
|
+
status = env.get_manager_status()
|
|
2982
|
+
|
|
2983
|
+
print("comfygit-manager")
|
|
2984
|
+
print(f" Current: {status.current_version or 'not installed'}")
|
|
2985
|
+
print(f" Latest: {status.latest_version or 'unknown'}")
|
|
2986
|
+
|
|
2987
|
+
if status.is_legacy:
|
|
2988
|
+
print(" Legacy installation (symlinked)")
|
|
2989
|
+
print(f" Run 'cg -e {env.name} manager update' to migrate")
|
|
2990
|
+
elif not status.is_tracked:
|
|
2991
|
+
print(" Not installed")
|
|
2992
|
+
print(f" Run 'cg -e {env.name} manager update' to install")
|
|
2993
|
+
elif status.update_available:
|
|
2994
|
+
print(" Update available!")
|
|
2995
|
+
else:
|
|
2996
|
+
print(" Up to date")
|
|
2997
|
+
|
|
2998
|
+
@with_env_logging("manager update")
|
|
2999
|
+
def manager_update(self, args: argparse.Namespace, logger: Any = None) -> None:
|
|
3000
|
+
"""Update or migrate comfygit-manager."""
|
|
3001
|
+
from comfygit_core.strategies.confirmation import AutoConfirmStrategy, InteractiveConfirmStrategy
|
|
3002
|
+
|
|
3003
|
+
env = self._get_env(args)
|
|
3004
|
+
version = getattr(args, 'version', None) or "latest"
|
|
3005
|
+
use_yes = getattr(args, 'yes', False)
|
|
3006
|
+
|
|
3007
|
+
# Ensure backend is configured (same as sync/run commands)
|
|
3008
|
+
had_backend = env.pytorch_manager.has_backend()
|
|
3009
|
+
if not had_backend:
|
|
3010
|
+
print("⚠️ No PyTorch backend configured. Auto-detecting...")
|
|
3011
|
+
python_version = self._get_python_version(env)
|
|
3012
|
+
backend = env.pytorch_manager.ensure_backend(python_version)
|
|
3013
|
+
print(f"✓ Backend detected and saved: {backend}")
|
|
3014
|
+
print(" To change: cg env-config torch-backend set <backend>\n")
|
|
3015
|
+
|
|
3016
|
+
status = env.get_manager_status()
|
|
3017
|
+
|
|
3018
|
+
if status.is_legacy:
|
|
3019
|
+
print("Migrating comfygit-manager to per-environment installation...")
|
|
3020
|
+
elif not status.is_tracked:
|
|
3021
|
+
print("Installing comfygit-manager...")
|
|
3022
|
+
else:
|
|
3023
|
+
print("Updating comfygit-manager...")
|
|
3024
|
+
|
|
3025
|
+
strategy = AutoConfirmStrategy() if use_yes else InteractiveConfirmStrategy()
|
|
3026
|
+
|
|
3027
|
+
try:
|
|
3028
|
+
result = env.update_manager(version=version, confirmation_strategy=strategy)
|
|
3029
|
+
|
|
3030
|
+
if result.changed:
|
|
3031
|
+
print(f" {result.message}")
|
|
3032
|
+
print("\n Restart this environment to use the new version")
|
|
3033
|
+
else:
|
|
3034
|
+
print(f" {result.message}")
|
|
3035
|
+
|
|
3036
|
+
except Exception as e:
|
|
3037
|
+
print(f" Failed to update manager: {e}", file=sys.stderr)
|
|
3038
|
+
if logger:
|
|
3039
|
+
logger.error(f"Manager update failed: {e}", exc_info=True)
|
|
3040
|
+
sys.exit(1)
|