comfygit 0.3.8__tar.gz → 0.3.8.dev1__tar.gz

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 (73) hide show
  1. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/PKG-INFO +2 -2
  2. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/comfygit_cli/cli.py +74 -4
  3. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/comfygit_cli/env_commands.py +238 -3
  4. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/comfygit_cli/global_commands.py +20 -17
  5. comfygit-0.3.8.dev1/docs/architecture.md +145 -0
  6. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/pyproject.toml +2 -2
  7. comfygit-0.3.8.dev1/tests/test_status_uninstalled_reporting.py +78 -0
  8. comfygit-0.3.8.dev1/tests/test_torch_backend_cli.py +292 -0
  9. comfygit-0.3.8/docs/codebase-map.md +0 -131
  10. comfygit-0.3.8/tests/test_status_uninstalled_reporting.py +0 -151
  11. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/.github/workflows/publish_pypi.yml +0 -0
  12. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/.gitignore +0 -0
  13. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/.python-version +0 -0
  14. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/CLAUDE.md +0 -0
  15. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/LICENSE.txt +0 -0
  16. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/README.md +0 -0
  17. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/comfygit_cli/__init__.py +0 -0
  18. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/comfygit_cli/__main__.py +0 -0
  19. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/comfygit_cli/cli_utils.py +0 -0
  20. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/comfygit_cli/completers.py +0 -0
  21. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/comfygit_cli/completion_commands.py +0 -0
  22. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/comfygit_cli/formatters/__init__.py +0 -0
  23. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/comfygit_cli/formatters/error_formatter.py +0 -0
  24. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/comfygit_cli/interactive/__init__.py +0 -0
  25. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/comfygit_cli/logging/compressed_handler.py +0 -0
  26. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/comfygit_cli/logging/environment_logger.py +0 -0
  27. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/comfygit_cli/logging/log_compressor.py +0 -0
  28. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/comfygit_cli/logging/logging_config.py +0 -0
  29. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/comfygit_cli/resolution_strategies.py +0 -0
  30. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/comfygit_cli/strategies/__init__.py +0 -0
  31. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/comfygit_cli/strategies/conflict_resolver.py +0 -0
  32. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/comfygit_cli/strategies/interactive.py +0 -0
  33. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/comfygit_cli/strategies/rollback.py +0 -0
  34. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/comfygit_cli/utils/__init__.py +0 -0
  35. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/comfygit_cli/utils/civitai_errors.py +0 -0
  36. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/comfygit_cli/utils/orchestrator.py +0 -0
  37. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/comfygit_cli/utils/pagination.py +0 -0
  38. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/comfygit_cli/utils/progress.py +0 -0
  39. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/scripts/README_REGISTRY.md +0 -0
  40. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/scripts/augment_mappings.py +0 -0
  41. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/scripts/build_global_mappings.py +0 -0
  42. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/scripts/build_registry_cache.py +0 -0
  43. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/scripts/extract_builtin_nodes.py +0 -0
  44. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/scripts/extract_node_modules.py +0 -0
  45. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/scripts/get_hash.py +0 -0
  46. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/scripts/global-node-mappings.md +0 -0
  47. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/scripts/registry.py +0 -0
  48. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/scripts/registry_client.py +0 -0
  49. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/scripts/test_concurrent_api.py +0 -0
  50. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/tests/conftest.py +0 -0
  51. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/tests/test_batch_node_add.py +0 -0
  52. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/tests/test_batch_node_remove.py +0 -0
  53. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/tests/test_branch_commands.py +0 -0
  54. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/tests/test_completers.py +0 -0
  55. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/tests/test_completion_commands.py +0 -0
  56. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/tests/test_conflict_resolver.py +0 -0
  57. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/tests/test_detached_head_display.py +0 -0
  58. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/tests/test_error_formatter.py +0 -0
  59. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/tests/test_init_system_nodes.py +0 -0
  60. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/tests/test_log_command.py +0 -0
  61. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/tests/test_logging_structure.py +0 -0
  62. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/tests/test_manifest_command.py +0 -0
  63. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/tests/test_pagination.py +0 -0
  64. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/tests/test_preview_display.py +0 -0
  65. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/tests/test_py_commands.py +0 -0
  66. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/tests/test_py_remove_group_commands.py +0 -0
  67. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/tests/test_status_disabled_nodes_display.py +0 -0
  68. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/tests/test_status_displays_uninstalled_nodes.py +0 -0
  69. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/tests/test_status_path_sync_display.py +0 -0
  70. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/tests/test_status_real_bug_scenario.py +0 -0
  71. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/tests/test_status_suggestions.py +0 -0
  72. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/tests/test_update_workflow_model_paths.py +0 -0
  73. {comfygit-0.3.8 → comfygit-0.3.8.dev1}/tests/test_workflow_model_importance.py +0 -0
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: comfygit
3
- Version: 0.3.8
3
+ Version: 0.3.8.dev1
4
4
  Summary: ComfyGit - Git-based environment management for ComfyUI
5
5
  License-File: LICENSE.txt
6
6
  Requires-Python: >=3.10
7
7
  Requires-Dist: argcomplete>=3.5.0
8
- Requires-Dist: comfygit-core==0.3.8
8
+ Requires-Dist: comfygit-core==0.3.8.dev1
9
9
  Description-Content-Type: text/markdown
10
10
 
11
11
  # ComfyGit CLI
@@ -3,13 +3,12 @@
3
3
 
4
4
  import argparse
5
5
  import sys
6
- from importlib.metadata import version, PackageNotFoundError
6
+ from collections.abc import Callable
7
+ from importlib.metadata import PackageNotFoundError, version
7
8
  from pathlib import Path
8
- from typing import Callable
9
9
 
10
10
  import argcomplete
11
11
 
12
- from .completion_commands import CompletionCommands
13
12
  from .completers import (
14
13
  branch_completer,
15
14
  commit_hash_completer,
@@ -18,6 +17,7 @@ from .completers import (
18
17
  ref_completer,
19
18
  workflow_completer,
20
19
  )
20
+ from .completion_commands import CompletionCommands
21
21
  from .env_commands import EnvironmentCommands
22
22
  from .global_commands import GlobalCommands
23
23
  from .logging.logging_config import setup_logging
@@ -274,8 +274,11 @@ def _add_global_commands(subparsers: argparse._SubParsersAction) -> None:
274
274
  registry_update_parser = registry_subparsers.add_parser("update", help="Update registry data from GitHub")
275
275
  registry_update_parser.set_defaults(func=global_cmds.registry_update)
276
276
 
277
- # Config management
277
+ # Config management - now with subcommands
278
278
  config_parser = subparsers.add_parser("config", help="Manage configuration settings")
279
+ config_subparsers = config_parser.add_subparsers(dest="config_command", help="Configuration commands")
280
+
281
+ # Legacy flags - still supported at root level for backward compatibility
279
282
  config_parser.add_argument("--civitai-key", type=str, help="Set Civitai API key (use empty string to clear)")
280
283
  config_parser.add_argument("--show", action="store_true", help="Show current configuration")
281
284
  config_parser.set_defaults(func=global_cmds.config)
@@ -386,9 +389,42 @@ def _add_env_commands(subparsers: argparse._SubParsersAction) -> None:
386
389
 
387
390
  # Environment Operation Commands (operate IN environments, require -e or active)
388
391
 
392
+ # env-config - Environment-scoped configuration (requires -e or active env)
393
+ env_config_parser = subparsers.add_parser("env-config", help="Manage environment-specific configuration")
394
+ env_config_subparsers = env_config_parser.add_subparsers(dest="env_config_command", help="Environment config commands")
395
+ env_config_parser.set_defaults(func=_make_help_func(env_config_parser))
396
+
397
+ # env-config torch-backend - Manage PyTorch backend for this environment
398
+ env_config_torch_parser = env_config_subparsers.add_parser("torch-backend", help="Manage PyTorch backend settings")
399
+ env_config_torch_subparsers = env_config_torch_parser.add_subparsers(dest="torch_command", help="PyTorch backend commands")
400
+ env_config_torch_parser.set_defaults(func=_make_help_func(env_config_torch_parser))
401
+
402
+ # env-config torch-backend show
403
+ env_config_torch_show_parser = env_config_torch_subparsers.add_parser("show", help="Show current PyTorch backend")
404
+ env_config_torch_show_parser.set_defaults(func=env_cmds.env_config_torch_show)
405
+
406
+ # env-config torch-backend set <backend>
407
+ env_config_torch_set_parser = env_config_torch_subparsers.add_parser("set", help="Set PyTorch backend")
408
+ env_config_torch_set_parser.add_argument("backend", help="Backend to set (e.g., cu128, cpu, rocm6.3, xpu)")
409
+ env_config_torch_set_parser.set_defaults(func=env_cmds.env_config_torch_set)
410
+
411
+ # env-config torch-backend detect
412
+ env_config_torch_detect_parser = env_config_torch_subparsers.add_parser("detect", help="Auto-detect and show recommended backend")
413
+ env_config_torch_detect_parser.set_defaults(func=env_cmds.env_config_torch_detect)
414
+
389
415
  # run - Run ComfyUI (special handling for ComfyUI args)
390
416
  run_parser = subparsers.add_parser("run", help="Run ComfyUI")
391
417
  run_parser.add_argument("--no-sync", action="store_true", help="Skip environment sync before running")
418
+ run_parser.add_argument(
419
+ "--torch-backend",
420
+ default=None,
421
+ metavar="BACKEND",
422
+ help=(
423
+ "PyTorch backend override (one-time, not saved). Examples: cpu, "
424
+ "cu128 (CUDA 12.8), cu126, cu124, rocm6.3 (AMD), xpu (Intel). "
425
+ "Reads from .pytorch-backend file if not specified."
426
+ ),
427
+ )
392
428
  run_parser.set_defaults(func=env_cmds.run, args=[])
393
429
 
394
430
  # status - Show environment status
@@ -414,6 +450,25 @@ def _add_env_commands(subparsers: argparse._SubParsersAction) -> None:
414
450
  )
415
451
  repair_parser.set_defaults(func=env_cmds.repair)
416
452
 
453
+ # sync - Sync environment (packages, nodes, models)
454
+ sync_parser = subparsers.add_parser("sync", help="Sync environment packages and dependencies")
455
+ sync_parser.add_argument(
456
+ "--torch-backend",
457
+ default=None,
458
+ metavar="BACKEND",
459
+ help=(
460
+ "PyTorch backend override (one-time, not saved). Examples: cpu, "
461
+ "cu128 (CUDA 12.8), cu126, cu124, rocm6.3 (AMD), xpu (Intel). "
462
+ "Reads from .pytorch-backend file if not specified."
463
+ ),
464
+ )
465
+ sync_parser.add_argument(
466
+ "-v", "--verbose",
467
+ action="store_true",
468
+ help="Show full UV output during sync"
469
+ )
470
+ sync_parser.set_defaults(func=env_cmds.sync)
471
+
417
472
  # log - Show commit history
418
473
  log_parser = subparsers.add_parser("log", help="Show commit history")
419
474
  log_parser.add_argument("-n", "--limit", type=int, default=20, metavar="N", help="Number of commits to show (default: 20)")
@@ -489,6 +544,11 @@ def _add_env_commands(subparsers: argparse._SubParsersAction) -> None:
489
544
  default="origin",
490
545
  help="Git remote name (default: origin)"
491
546
  )
547
+ pull_parser.add_argument(
548
+ "-b", "--branch",
549
+ default=None,
550
+ help="Remote branch to pull (default: current local branch). Use when remote has different default branch (e.g., master vs main)"
551
+ )
492
552
  pull_parser.add_argument(
493
553
  "--models",
494
554
  choices=["all", "required", "skip"],
@@ -510,6 +570,16 @@ def _add_env_commands(subparsers: argparse._SubParsersAction) -> None:
510
570
  choices=["mine", "theirs"],
511
571
  help="Auto-resolve conflicts: 'mine' keeps local, 'theirs' takes incoming"
512
572
  )
573
+ pull_parser.add_argument(
574
+ "--torch-backend",
575
+ default=None,
576
+ metavar="BACKEND",
577
+ help=(
578
+ "PyTorch backend override (one-time, not saved). Examples: cpu, "
579
+ "cu128 (CUDA 12.8), cu126, cu124, rocm6.3 (AMD), xpu (Intel). "
580
+ "Reads from .pytorch-backend file if not specified."
581
+ ),
582
+ )
513
583
  pull_parser.set_defaults(func=env_cmds.pull)
514
584
 
515
585
  # push - Push commits to remote
@@ -88,6 +88,45 @@ class EnvironmentCommands:
88
88
  sys.exit(1)
89
89
  return active
90
90
 
91
+ def _get_or_probe_backend(
92
+ self, env: Environment, override: str | None = None
93
+ ) -> tuple[str, bool]:
94
+ """Get torch backend from file or probe if missing.
95
+
96
+ Args:
97
+ env: Environment to get backend for
98
+ override: Optional explicit backend override
99
+
100
+ Returns:
101
+ Tuple of (backend_string, was_probed) where was_probed is True
102
+ if we had to auto-probe because no backend was configured.
103
+ """
104
+ if override:
105
+ return override, False
106
+
107
+ if env.pytorch_manager.has_backend():
108
+ return env.pytorch_manager.get_backend(), False
109
+
110
+ # No backend configured - probe and set it
111
+ print("⚠️ No PyTorch backend configured. Auto-detecting...")
112
+ try:
113
+ # Read Python version from .python-version file
114
+ python_version_file = env.cec_path / ".python-version"
115
+ python_version = (
116
+ python_version_file.read_text(encoding="utf-8").strip()
117
+ if python_version_file.exists()
118
+ else "3.12"
119
+ )
120
+ backend = env.pytorch_manager.probe_and_set_backend(
121
+ python_version,
122
+ backend="auto",
123
+ )
124
+ return backend, True
125
+ except Exception as e:
126
+ print(f"✗ Error probing PyTorch backend: {e}")
127
+ print(" Try setting it explicitly: cg env-config torch-backend set <backend>")
128
+ sys.exit(1)
129
+
91
130
  def _format_size(self, size_bytes: int) -> str:
92
131
  """Format bytes as human-readable size."""
93
132
  for unit in ("B", "KB", "MB", "GB"):
@@ -270,6 +309,121 @@ class EnvironmentCommands:
270
309
 
271
310
  # === Commands that operate IN environments ===
272
311
 
312
+ # === Environment Configuration ===
313
+
314
+ @with_env_logging("env-config torch-backend show")
315
+ def env_config_torch_show(self, args: argparse.Namespace, logger=None) -> None:
316
+ """Show current PyTorch backend setting for this environment."""
317
+ env = self._get_env(args)
318
+
319
+ backend = env.pytorch_manager.get_backend()
320
+ backend_file = env.pytorch_manager.backend_file
321
+ versions = env.pytorch_manager.get_versions()
322
+
323
+ print(f"PyTorch Backend: {backend}")
324
+ if versions:
325
+ for pkg, ver in versions.items():
326
+ print(f" {pkg}={ver}")
327
+
328
+ if backend_file.exists():
329
+ print(f" Source: {backend_file}")
330
+ else:
331
+ print(" Source: auto-detected (no .pytorch-backend file)")
332
+ print()
333
+ print(f"💡 To save this setting: cg env-config torch-backend set {backend}")
334
+
335
+ @with_env_logging("env-config torch-backend set")
336
+ def env_config_torch_set(self, args: argparse.Namespace, logger=None) -> None:
337
+ """Set PyTorch backend for this environment.
338
+
339
+ Probes for exact versions and stores both backend and version pins.
340
+ """
341
+ from comfygit_core.utils.pytorch_prober import PyTorchProbeError
342
+
343
+ env = self._get_env(args)
344
+ backend = args.backend
345
+
346
+ # Validate backend format
347
+ if not env.pytorch_manager.is_valid_backend(backend):
348
+ print(f"✗ Invalid backend: {backend}")
349
+ print()
350
+ print("Valid formats:")
351
+ print(" • cu118, cu121, cu124, cu126, cu128 (CUDA)")
352
+ print(" • cpu")
353
+ print(" • rocm6.2, rocm6.3 (AMD)")
354
+ print(" • xpu (Intel)")
355
+ sys.exit(1)
356
+
357
+ # Read python version
358
+ python_version_file = env.cec_path / ".python-version"
359
+ python_version = (
360
+ python_version_file.read_text(encoding="utf-8").strip()
361
+ if python_version_file.exists()
362
+ else "3.12"
363
+ )
364
+
365
+ # Probe and set backend with versions
366
+ print(f"🔍 Probing PyTorch versions for {backend} (Python {python_version})...")
367
+ try:
368
+ resolved = env.pytorch_manager.probe_and_set_backend(python_version, backend)
369
+ except PyTorchProbeError as e:
370
+ print(f"✗ Error probing PyTorch: {e}")
371
+ sys.exit(1)
372
+
373
+ # Show what was stored
374
+ versions = env.pytorch_manager.get_versions()
375
+ print(f"✓ PyTorch backend set to: {resolved}")
376
+ if versions:
377
+ for pkg, ver in versions.items():
378
+ print(f" {pkg}={ver}")
379
+ print()
380
+ print("Run 'cg sync' to apply the new backend configuration.")
381
+
382
+ @with_env_logging("env-config torch-backend detect")
383
+ def env_config_torch_detect(self, args: argparse.Namespace, logger=None) -> None:
384
+ """Auto-detect recommended PyTorch backend using uv probe."""
385
+ from comfygit_core.utils.pytorch_prober import PyTorchProbeError, probe_pytorch_versions
386
+
387
+ env = self._get_env(args)
388
+ backend_file = env.pytorch_manager.backend_file
389
+
390
+ # Read python version from file
391
+ python_version_file = env.cec_path / ".python-version"
392
+ python_version = (
393
+ python_version_file.read_text(encoding="utf-8").strip()
394
+ if python_version_file.exists()
395
+ else "3.12"
396
+ )
397
+
398
+ # Probe for recommended backend
399
+ print(f"🔍 Probing PyTorch compatibility for Python {python_version}...")
400
+ try:
401
+ _, detected = probe_pytorch_versions(python_version, "auto")
402
+ except PyTorchProbeError as e:
403
+ print(f"✗ Error probing PyTorch: {e}")
404
+ sys.exit(1)
405
+
406
+ # Get current backend (if any)
407
+ if env.pytorch_manager.has_backend():
408
+ current = env.pytorch_manager.get_backend()
409
+ else:
410
+ current = "(not configured)"
411
+
412
+ print(f"Detected backend: {detected}")
413
+ print(f"Current backend: {current}")
414
+
415
+ if backend_file.exists():
416
+ print(f" Source: {backend_file}")
417
+ else:
418
+ print(" Source: not configured")
419
+
420
+ if current != detected and current != "(not configured)":
421
+ print()
422
+ print(f"💡 Consider updating: cg env-config torch-backend set {detected}")
423
+ elif current == "(not configured)":
424
+ print()
425
+ print(f"💡 Set the backend: cg env-config torch-backend set {detected}")
426
+
273
427
  @with_env_logging("run")
274
428
  def run(self, args: argparse.Namespace) -> None:
275
429
  """Run ComfyUI in the specified environment."""
@@ -278,14 +432,32 @@ class EnvironmentCommands:
278
432
  comfyui_args = args.args if hasattr(args, 'args') else []
279
433
  no_sync = getattr(args, 'no_sync', False)
280
434
 
435
+ # Handle torch-backend: use override, read from file, or probe if missing
436
+ torch_backend_override = getattr(args, 'torch_backend', None)
437
+ torch_backend, was_probed = self._get_or_probe_backend(env, torch_backend_override)
438
+
439
+ if torch_backend_override:
440
+ print(f"🔧 Using PyTorch backend override: {torch_backend}")
441
+ elif was_probed:
442
+ print(f"✓ Backend detected and saved: {torch_backend}")
443
+ print(f" To change: cg env-config torch-backend set <backend>")
444
+ else:
445
+ print(f"🔧 Using PyTorch backend: {torch_backend}")
446
+
281
447
  current_branch = env.get_current_branch()
282
448
  branch_display = f" (on {current_branch})" if current_branch else " (detached HEAD)"
283
449
 
284
450
  while True:
285
451
  # Sync before running (unless --no-sync)
452
+ # Use explicit override if provided, otherwise None (backend is now in file)
286
453
  if not no_sync:
287
454
  print(f"🔄 Syncing environment: {env.name}")
288
- env.sync(preserve_workflows=True, remove_extra_nodes=False)
455
+ env.sync(
456
+ preserve_workflows=True,
457
+ remove_extra_nodes=False,
458
+ backend_override=torch_backend_override if torch_backend_override else None,
459
+ verbose=True,
460
+ )
289
461
 
290
462
  print(f"🎮 Starting ComfyUI in environment: {env.name}{branch_display}")
291
463
  if comfyui_args:
@@ -300,6 +472,54 @@ class EnvironmentCommands:
300
472
 
301
473
  sys.exit(result.returncode)
302
474
 
475
+ @with_env_logging("sync")
476
+ def sync(self, args: argparse.Namespace, logger=None) -> None:
477
+ """Sync environment packages and dependencies."""
478
+ env = self._get_env(args)
479
+
480
+ # Handle torch-backend: use override, read from file, or probe if missing
481
+ torch_backend_override = getattr(args, 'torch_backend', None)
482
+ torch_backend, was_probed = self._get_or_probe_backend(env, torch_backend_override)
483
+
484
+ if torch_backend_override:
485
+ print(f"🔧 Using PyTorch backend override: {torch_backend}")
486
+ elif was_probed:
487
+ print(f"✓ Backend detected and saved: {torch_backend}")
488
+ print(f" To change: cg env-config torch-backend set <backend>")
489
+ else:
490
+ print(f"🔧 Using PyTorch backend: {torch_backend}")
491
+
492
+ print(f"\n🔄 Syncing environment: {env.name}")
493
+
494
+ verbose = getattr(args, 'verbose', False)
495
+
496
+ try:
497
+ # Use explicit override if provided, otherwise None (backend is now in file)
498
+ result = env.sync(
499
+ dry_run=False,
500
+ model_strategy="skip", # Sync command focuses on packages
501
+ remove_extra_nodes=False, # Don't remove nodes, just sync
502
+ verbose=verbose,
503
+ backend_override=torch_backend_override if torch_backend_override else None,
504
+ )
505
+
506
+ if result.success:
507
+ print("\n✓ Sync complete")
508
+ if result.packages_synced:
509
+ print(f" Packages synced: {result.packages_synced}")
510
+ if result.dependency_groups_installed:
511
+ print(f" Dependency groups: {', '.join(result.dependency_groups_installed)}")
512
+ else:
513
+ print("\n⚠️ Sync completed with warnings")
514
+ for error in result.errors:
515
+ print(f" • {error}")
516
+
517
+ except Exception as e:
518
+ if logger:
519
+ logger.error(f"Sync failed: {e}", exc_info=True)
520
+ print(f"\n✗ Sync failed: {e}", file=sys.stderr)
521
+ sys.exit(1)
522
+
303
523
  def manifest(self, args: argparse.Namespace) -> None:
304
524
  """Show environment manifest (pyproject.toml configuration)."""
305
525
  env = self._get_env(args)
@@ -1997,10 +2217,11 @@ class EnvironmentCommands:
1997
2217
  sys.exit(1)
1998
2218
 
1999
2219
  # Preview mode - read-only, just show what would change
2220
+ branch = getattr(args, 'branch', None)
2000
2221
  if getattr(args, "preview", False):
2001
2222
  try:
2002
2223
  print(f"Fetching from {args.remote}...")
2003
- diff = env.preview_pull(remote=args.remote)
2224
+ diff = env.preview_pull(remote=args.remote, branch=branch)
2004
2225
 
2005
2226
  if not diff.has_changes:
2006
2227
  if diff.is_already_merged:
@@ -2044,7 +2265,7 @@ class EnvironmentCommands:
2044
2265
  else:
2045
2266
  # Check for conflicts before pull
2046
2267
  print(f"Checking for conflicts with {args.remote}...")
2047
- diff = env.preview_pull(remote=args.remote)
2268
+ diff = env.preview_pull(remote=args.remote, branch=branch)
2048
2269
  if diff.has_conflicts:
2049
2270
  # Interactive conflict resolution
2050
2271
  from .strategies.conflict_resolver import InteractiveConflictResolver
@@ -2073,6 +2294,18 @@ class EnvironmentCommands:
2073
2294
 
2074
2295
  print(f"📥 Pulling from {args.remote}...")
2075
2296
 
2297
+ # Handle torch-backend: use override, read from file, or probe if missing
2298
+ torch_backend_override = getattr(args, 'torch_backend', None)
2299
+ torch_backend, was_probed = self._get_or_probe_backend(env, torch_backend_override)
2300
+
2301
+ if torch_backend_override:
2302
+ print(f"🔧 Using PyTorch backend override: {torch_backend}")
2303
+ elif was_probed:
2304
+ print(f"🔧 Auto-detected PyTorch backend: {torch_backend}")
2305
+ print(f" To save: cg env-config torch-backend set {torch_backend}")
2306
+ else:
2307
+ print(f"🔧 Using PyTorch backend: {torch_backend}")
2308
+
2076
2309
  # Create callbacks for node and model progress (reuse repair command patterns)
2077
2310
  from comfygit_core.models.workflow import BatchDownloadCallbacks, NodeInstallCallbacks
2078
2311
  from .utils.progress import create_progress_callback
@@ -2113,11 +2346,13 @@ class EnvironmentCommands:
2113
2346
  force = getattr(args, 'force', False)
2114
2347
  result = env.pull_and_repair(
2115
2348
  remote=args.remote,
2349
+ branch=branch,
2116
2350
  model_strategy=getattr(args, 'models', 'all'),
2117
2351
  model_callbacks=model_callbacks,
2118
2352
  node_callbacks=node_callbacks,
2119
2353
  strategy_option=strategy_option,
2120
2354
  force=force,
2355
+ backend_override=torch_backend,
2121
2356
  )
2122
2357
 
2123
2358
  # Extract sync result for summary
@@ -94,7 +94,6 @@ class GlobalCommands:
94
94
  - uv_cache/ for package management
95
95
  - environments/ for ComfyUI environments
96
96
  """
97
- from pathlib import Path
98
97
 
99
98
  # Validate models directory if provided (before creating workspace)
100
99
  explicit_models_dir = getattr(args, 'models_dir', None)
@@ -193,7 +192,7 @@ class GlobalCommands:
193
192
  logger.info(f"Installed system node: {node_name}")
194
193
  except Exception as e:
195
194
  print(f" ⚠️ Failed to install {node_name}: {e}")
196
- print(f" You can install it manually later")
195
+ print(" You can install it manually later")
197
196
  logger.warning(f"Failed to install system node {node_name}: {e}")
198
197
 
199
198
  def _show_workspace_env_setup(self, workspace_path: Path) -> None:
@@ -231,8 +230,7 @@ class GlobalCommands:
231
230
  args: CLI arguments containing models_dir and yes flags
232
231
  """
233
232
  from pathlib import Path
234
- from comfygit_cli.utils.progress import create_model_sync_progress
235
- from comfygit_core.utils.common import format_size
233
+
236
234
 
237
235
  # Check for explicit flags
238
236
  use_interactive = not getattr(args, 'yes', False)
@@ -256,7 +254,7 @@ class GlobalCommands:
256
254
  print("\nOptions:")
257
255
  print(" 1. Point to an existing ComfyUI models directory (recommended)")
258
256
  print(" → Access all your existing models immediately")
259
- print(f" → Example: ~/ComfyUI/models")
257
+ print(" → Example: ~/ComfyUI/models")
260
258
  print("\n 2. Use the default empty directory")
261
259
  print(f" → ComfyGit created: {workspace.paths.models}")
262
260
  print(" → Download models as needed later")
@@ -296,7 +294,7 @@ class GlobalCommands:
296
294
  # Auto-detect if they entered ComfyUI root instead of models subdir
297
295
  if (models_path / "models").exists() and models_path.name != "models":
298
296
  print(f"\n⚠️ Detected ComfyUI installation at: {models_path}")
299
- use_subdir = input(f"Use models/ subdirectory instead? (Y/n): ").strip().lower()
297
+ use_subdir = input("Use models/ subdirectory instead? (Y/n): ").strip().lower()
300
298
  if use_subdir != 'n':
301
299
  models_path = models_path / "models"
302
300
  print(f"Using: {models_path}")
@@ -322,9 +320,10 @@ class GlobalCommands:
322
320
  workspace: The workspace instance
323
321
  models_path: Path to the models directory to scan
324
322
  """
325
- from comfygit_cli.utils.progress import create_model_sync_progress
326
323
  from comfygit_core.utils.common import format_size
327
324
 
325
+ from comfygit_cli.utils.progress import create_model_sync_progress
326
+
328
327
  try:
329
328
  progress = create_model_sync_progress()
330
329
  workspace.set_models_directory(models_path, progress=progress)
@@ -404,7 +403,7 @@ class GlobalCommands:
404
403
 
405
404
  # Read log lines
406
405
  try:
407
- with open(log_file, 'r', encoding='utf-8') as f:
406
+ with open(log_file, encoding='utf-8') as f:
408
407
  lines = f.readlines()
409
408
  except Exception as e:
410
409
  print(f"✗ Failed to read log file: {e}", file=sys.stderr)
@@ -453,9 +452,9 @@ class GlobalCommands:
453
452
  for line in record:
454
453
  print(line.rstrip())
455
454
 
456
- print(f"\n=== End of logs ===")
455
+ print("\n=== End of logs ===")
457
456
  if not args.full and len(records) == args.lines:
458
- print(f"Tip: Use --full to see all logs, or increase --lines to see more")
457
+ print("Tip: Use --full to see all logs, or increase --lines to see more")
459
458
 
460
459
  @with_workspace_logging("migrate")
461
460
  def migrate(self, args: argparse.Namespace) -> None:
@@ -1519,10 +1518,10 @@ class GlobalCommands:
1519
1518
  def orch_status(self, args: argparse.Namespace) -> None:
1520
1519
  """Show orchestrator status."""
1521
1520
  from .utils.orchestrator import (
1521
+ format_uptime,
1522
+ get_orchestrator_uptime,
1522
1523
  is_orchestrator_running,
1523
1524
  read_switch_status,
1524
- get_orchestrator_uptime,
1525
- format_uptime
1526
1525
  )
1527
1526
 
1528
1527
  metadata_dir = self.workspace.path / ".metadata"
@@ -1578,7 +1577,7 @@ class GlobalCommands:
1578
1577
  try:
1579
1578
  port = control_port_file.read_text().strip()
1580
1579
  print(f"Control Port: {port}")
1581
- except IOError:
1580
+ except OSError:
1582
1581
  pass
1583
1582
 
1584
1583
  # Check switch status
@@ -1605,6 +1604,7 @@ class GlobalCommands:
1605
1604
  def orch_restart(self, args: argparse.Namespace) -> None:
1606
1605
  """Request orchestrator to restart ComfyUI."""
1607
1606
  import time
1607
+
1608
1608
  from .utils.orchestrator import is_orchestrator_running, safe_write_command
1609
1609
 
1610
1610
  metadata_dir = self.workspace.path / ".metadata"
@@ -1643,11 +1643,12 @@ class GlobalCommands:
1643
1643
  def orch_kill(self, args: argparse.Namespace) -> None:
1644
1644
  """Shutdown orchestrator."""
1645
1645
  import time
1646
+
1646
1647
  from .utils.orchestrator import (
1647
1648
  is_orchestrator_running,
1648
- safe_write_command,
1649
1649
  kill_orchestrator_process,
1650
- read_switch_status
1650
+ read_switch_status,
1651
+ safe_write_command,
1651
1652
  )
1652
1653
 
1653
1654
  metadata_dir = self.workspace.path / ".metadata"
@@ -1701,9 +1702,9 @@ class GlobalCommands:
1701
1702
  def orch_clean(self, args: argparse.Namespace) -> None:
1702
1703
  """Clean orchestrator state files."""
1703
1704
  from .utils.orchestrator import (
1705
+ cleanup_orchestrator_state,
1704
1706
  is_orchestrator_running,
1705
1707
  kill_orchestrator_process,
1706
- cleanup_orchestrator_state
1707
1708
  )
1708
1709
 
1709
1710
  metadata_dir = self.workspace.path / ".metadata"
@@ -1742,7 +1743,7 @@ class GlobalCommands:
1742
1743
  print("\nNote: workspace_config.json will be preserved")
1743
1744
 
1744
1745
  if args.kill:
1745
- print(f"\n⚠️ --kill flag: Will also terminate orchestrator process")
1746
+ print("\n⚠️ --kill flag: Will also terminate orchestrator process")
1746
1747
 
1747
1748
  response = input("\nContinue? [y/N]: ").strip().lower()
1748
1749
  if response not in ['y', 'yes']:
@@ -1784,6 +1785,7 @@ class GlobalCommands:
1784
1785
  def orch_logs(self, args: argparse.Namespace) -> None:
1785
1786
  """Show orchestrator logs."""
1786
1787
  import subprocess
1788
+
1787
1789
  from .utils.orchestrator import tail_log_file
1788
1790
 
1789
1791
  metadata_dir = self.workspace.path / ".metadata"
@@ -1808,3 +1810,4 @@ class GlobalCommands:
1808
1810
  print("".join(lines))
1809
1811
  else:
1810
1812
  print("(empty log file)")
1813
+