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.
@@ -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("env create")
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("env use")
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("env delete")
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
- @with_env_logging("env run")
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(preserve_workflows=True, remove_extra_nodes=False)
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("env status")
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("env node add")
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} env status' to review changes")
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} env status' to review changes")
1206
+ print(f"\nRun 'cg -e {env.name} status' to review changes")
969
1207
 
970
- @with_env_logging("env node remove")
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} env status' to review changes")
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} env status' to review changes")
1285
+ print(f"\nRun 'cg -e {env.name} status' to review changes")
1048
1286
 
1049
- @with_env_logging("env node prune")
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("env node list")
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("env node update")
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("env constraint add")
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("env constraint list")
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("env constraint remove")
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("env py add")
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("env py remove")
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("env py remove-group")
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("env py uv")
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("env py list")
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("env repair")
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("env checkout")
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("env branch")
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("env switch")
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("env reset")
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("env merge")
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("env revert")
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("env commit")
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("env pull")
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
- print("💡 Set up a remote first:")
1989
- print(f" cg remote add {args.remote} <url>")
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("env push")
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
- print("💡 Set up a remote first:")
2205
- print(f" cg remote add {args.remote} <url>")
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("env remote")
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)