comfygit 0.5.0__tar.gz → 0.6.0__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 (35) hide show
  1. {comfygit-0.5.0 → comfygit-0.6.0}/PKG-INFO +4 -4
  2. {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/cli.py +18 -0
  3. {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/env_commands.py +387 -185
  4. {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/utils/update_checker.py +2 -1
  5. {comfygit-0.5.0 → comfygit-0.6.0}/pyproject.toml +4 -4
  6. {comfygit-0.5.0 → comfygit-0.6.0}/.gitignore +0 -0
  7. {comfygit-0.5.0 → comfygit-0.6.0}/LICENSE.txt +0 -0
  8. {comfygit-0.5.0 → comfygit-0.6.0}/README.md +0 -0
  9. {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/__init__.py +0 -0
  10. {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/__main__.py +0 -0
  11. {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/cli_utils.py +0 -0
  12. {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/completers.py +0 -0
  13. {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/completion_commands.py +0 -0
  14. {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/formatters/__init__.py +0 -0
  15. {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/formatters/error_formatter.py +0 -0
  16. {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/global_commands.py +0 -0
  17. {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/interactive/__init__.py +0 -0
  18. {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/logging/compressed_handler.py +0 -0
  19. {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/logging/environment_logger.py +0 -0
  20. {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/logging/log_compressor.py +0 -0
  21. {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/logging/logging_config.py +0 -0
  22. {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/resolution_strategies.py +0 -0
  23. {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/strategies/__init__.py +0 -0
  24. {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/strategies/confirmation.py +0 -0
  25. {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/strategies/conflict_resolver.py +0 -0
  26. {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/strategies/interactive.py +0 -0
  27. {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/strategies/rollback.py +0 -0
  28. {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/update_commands.py +0 -0
  29. {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/utils/__init__.py +0 -0
  30. {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/utils/civitai_errors.py +0 -0
  31. {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/utils/formatting.py +0 -0
  32. {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/utils/orchestrator.py +0 -0
  33. {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/utils/pagination.py +0 -0
  34. {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/utils/progress.py +0 -0
  35. {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/utils/update_notice.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: comfygit
3
- Version: 0.5.0
3
+ Version: 0.6.0
4
4
  Summary: ComfyGit - Git-based environment management for ComfyUI
5
5
  Project-URL: Documentation, https://docs.comfygit.org/
6
6
  Project-URL: Repository, https://github.com/comfygit-ai/comfygit
@@ -8,9 +8,9 @@ Project-URL: Issues, https://github.com/comfygit-ai/comfygit/issues
8
8
  License-File: LICENSE.txt
9
9
  Requires-Python: >=3.10
10
10
  Requires-Dist: argcomplete>=3.5.0
11
- Requires-Dist: comfygit-core==0.5.0
12
- Requires-Dist: comfygit-studio==0.5.0
13
- Requires-Dist: requests>=2.33.0
11
+ Requires-Dist: comfygit-core==0.6.0
12
+ Requires-Dist: comfygit-studio==0.6.0
13
+ Requires-Dist: requests>=2.32.4
14
14
  Description-Content-Type: text/markdown
15
15
 
16
16
  # ComfyGit CLI
@@ -2,6 +2,7 @@
2
2
  # PYTHON_ARGCOMPLETE_OK
3
3
 
4
4
  import argparse
5
+ import os
5
6
  import sys
6
7
  from collections.abc import Callable
7
8
  from importlib.metadata import PackageNotFoundError, version
@@ -75,8 +76,25 @@ def _check_for_old_docker_installation() -> None:
75
76
  warning_flag.touch()
76
77
 
77
78
 
79
+ def _configure_stdio_encoding() -> None:
80
+ """Avoid Windows terminal encoding crashes for Unicode status output."""
81
+ if os.name != "nt":
82
+ return
83
+
84
+ for stream in (sys.stdout, sys.stderr):
85
+ reconfigure = getattr(stream, "reconfigure", None)
86
+ if reconfigure is None:
87
+ continue
88
+ try:
89
+ reconfigure(errors="replace")
90
+ except (OSError, ValueError):
91
+ continue
92
+
93
+
78
94
  def main() -> None:
79
95
  """Main entry point for ComfyGit CLI."""
96
+ _configure_stdio_encoding()
97
+
80
98
  # Enable readline for input() line editing (arrow keys, history)
81
99
  # Unix/Linux/macOS: provides full editing capability
82
100
  # Windows: gracefully falls back to native console editing
@@ -19,6 +19,7 @@ from comfygit_core.models import (
19
19
  UVCommandError,
20
20
  )
21
21
  from comfygit_core.runtime import (
22
+ ACTIVE_TORCH_BACKEND_OVERRIDE_ENV,
22
23
  SwitchObserverServer,
23
24
  cleanup_supervisor_advertisement,
24
25
  cleanup_switch_status,
@@ -33,7 +34,11 @@ from .strategies.interactive import InteractiveModelStrategy, InteractiveNodeStr
33
34
 
34
35
  if TYPE_CHECKING:
35
36
  from comfygit_core import Environment, Workspace
36
- from comfygit_core.models import EnvironmentStatus, WorkflowAnalysisStatus
37
+ from comfygit_core.models import (
38
+ EnvironmentLifecycleStatus,
39
+ EnvironmentStatus,
40
+ WorkflowAnalysisStatus,
41
+ )
37
42
 
38
43
  from .cli_utils import get_workspace_or_exit
39
44
  from .logging.environment_logger import with_env_logging
@@ -41,6 +46,8 @@ from .logging.logging_config import get_logger
41
46
 
42
47
  logger = get_logger(__name__)
43
48
 
49
+ DEFAULT_SUPERVISOR_CONTROL_PORT_SPAN = 20
50
+
44
51
 
45
52
  class EnvironmentCommands:
46
53
  """Handler for environment-specific commands - simplified for MVP."""
@@ -136,6 +143,12 @@ class EnvironmentCommands:
136
143
  print(" Try setting it explicitly: cg env-config torch-backend set <backend>")
137
144
  sys.exit(1)
138
145
 
146
+ def _comfyui_args_for_backend(self, comfyui_args: list[str], backend: str) -> list[str]:
147
+ """Return ComfyUI launch args for the selected PyTorch backend."""
148
+ if backend == "cpu" and "--cpu" not in comfyui_args:
149
+ return ["--cpu", *comfyui_args]
150
+ return list(comfyui_args)
151
+
139
152
  def _show_legacy_manager_notice(self, env: Environment) -> None:
140
153
  """Show legacy manager notice if environment uses symlinked manager."""
141
154
  try:
@@ -575,14 +588,14 @@ class EnvironmentCommands:
575
588
  RESTART_EXIT_CODE = 42
576
589
  SWITCH_ENV_EXIT_CODE = 43
577
590
  env = self._get_env(args)
578
- comfyui_args = args.args if hasattr(args, 'args') else []
579
- if comfyui_args and comfyui_args[0] == "--":
580
- comfyui_args = comfyui_args[1:]
591
+ base_comfyui_args = args.args if hasattr(args, 'args') else []
592
+ if base_comfyui_args and base_comfyui_args[0] == "--":
593
+ base_comfyui_args = base_comfyui_args[1:]
581
594
  no_sync = getattr(args, 'no_sync', False)
582
595
  extras = getattr(args, 'extra', None) or []
583
596
  all_extras = getattr(args, 'all_extras', False)
584
597
  overlay_names = getattr(args, 'overlay', None) or []
585
- supervisor_control = self._start_supervisor_control()
598
+ supervisor_control = self._start_supervisor_control(base_comfyui_args)
586
599
  if supervisor_control:
587
600
  atexit.register(supervisor_control.stop)
588
601
 
@@ -590,23 +603,23 @@ class EnvironmentCommands:
590
603
  print("✗ Cannot use --overlay with --no-sync. Overlays require a sync to take effect.")
591
604
  sys.exit(1)
592
605
 
593
- # Handle torch-backend: use override, read from file, or probe if missing
594
606
  torch_backend_override = getattr(args, 'torch_backend', None)
595
- torch_backend, was_probed = self._get_or_probe_backend(env, torch_backend_override)
596
-
597
- if torch_backend_override:
598
- print(f"🔧 Using PyTorch backend override: {torch_backend}")
599
- elif was_probed:
600
- print(f"✓ Backend detected and saved: {torch_backend}")
601
- print(" To change: cg env-config torch-backend set <backend>")
602
- else:
603
- print(f"🔧 Using PyTorch backend: {torch_backend}")
604
-
605
- if torch_backend == "cpu" and "--cpu" not in comfyui_args:
606
- comfyui_args = ["--cpu", *comfyui_args]
607
607
 
608
608
  switch_source_env = self._consume_startup_switch_request(env)
609
609
  while True:
610
+ # Manager-driven switches keep this `cg run` supervisor alive while
611
+ # replacing `env`, so derive backend-specific launch args per target.
612
+ torch_backend, was_probed = self._get_or_probe_backend(env, torch_backend_override)
613
+ comfyui_args = self._comfyui_args_for_backend(base_comfyui_args, torch_backend)
614
+
615
+ if torch_backend_override:
616
+ print(f"🔧 Using PyTorch backend override: {torch_backend}")
617
+ elif was_probed:
618
+ print(f"✓ Backend detected and saved: {torch_backend}")
619
+ print(" To change: cg env-config torch-backend set <backend>")
620
+ else:
621
+ print(f"🔧 Using PyTorch backend: {torch_backend}")
622
+
610
623
  current_branch = env.get_current_branch()
611
624
  branch_display = f" (on {current_branch})" if current_branch else " (detached HEAD)"
612
625
 
@@ -646,10 +659,14 @@ class EnvironmentCommands:
646
659
  comfyui_args,
647
660
  source_env=switch_source_env,
648
661
  supervisor_control=supervisor_control,
662
+ backend_override=torch_backend_override,
649
663
  )
650
664
  switch_source_env = None
651
665
  else:
652
- result = env.run(comfyui_args)
666
+ result = env.run(
667
+ comfyui_args,
668
+ backend_override=torch_backend_override,
669
+ )
653
670
 
654
671
  if result.returncode == RESTART_EXIT_CODE:
655
672
  print("\n🔄 Restart requested, syncing dependencies...\n")
@@ -673,28 +690,56 @@ class EnvironmentCommands:
673
690
 
674
691
  sys.exit(result.returncode)
675
692
 
676
- def _start_supervisor_control(self) -> SwitchObserverServer | None:
693
+ def _start_supervisor_control(self, comfyui_args: list[str]) -> SwitchObserverServer | None:
677
694
  port_text = os.environ.get("COMFYGIT_SUPERVISOR_CONTROL_PORT")
678
- if not port_text:
679
- cleanup_supervisor_advertisement(self.workspace.path)
680
- return None
695
+ endpoint = resolve_comfyui_endpoint(comfyui_args)
696
+ explicit_port = port_text is not None
681
697
 
682
- try:
683
- port = int(port_text)
684
- except ValueError:
685
- print(f"⚠️ Invalid COMFYGIT_SUPERVISOR_CONTROL_PORT={port_text!r}; supervisor control disabled")
698
+ if port_text and port_text.lower() in {"0", "off", "false", "disabled"}:
686
699
  cleanup_supervisor_advertisement(self.workspace.path)
687
700
  return None
688
701
 
689
- host = os.environ.get("COMFYGIT_SUPERVISOR_CONTROL_HOST", "127.0.0.1")
690
- control = SwitchObserverServer(self.workspace.path, host, port)
691
- try:
692
- control.start()
693
- return control
694
- except OSError as exc:
695
- print(f"⚠️ Could not start supervisor control server on {host}:{port}: {exc}")
696
- control.stop()
697
- return None
702
+ if port_text:
703
+ try:
704
+ candidate_ports = [int(port_text)]
705
+ except ValueError:
706
+ print(f"⚠️ Invalid COMFYGIT_SUPERVISOR_CONTROL_PORT={port_text!r}; supervisor control disabled")
707
+ cleanup_supervisor_advertisement(self.workspace.path)
708
+ return None
709
+ else:
710
+ start_port = min(max(endpoint.port + 1, 1), 65535)
711
+ end_port = min(start_port + DEFAULT_SUPERVISOR_CONTROL_PORT_SPAN - 1, 65535)
712
+ candidate_ports = list(range(start_port, end_port + 1))
713
+
714
+ host = os.environ.get("COMFYGIT_SUPERVISOR_CONTROL_HOST") or endpoint.bind_host
715
+ public_origin = os.environ.get("COMFYGIT_SUPERVISOR_PUBLIC_ORIGIN")
716
+ last_error: OSError | None = None
717
+ for port in candidate_ports:
718
+ control = SwitchObserverServer(
719
+ self.workspace.path,
720
+ host,
721
+ port,
722
+ public_origin=public_origin,
723
+ )
724
+ try:
725
+ control.start()
726
+ return control
727
+ except OSError as exc:
728
+ last_error = exc
729
+ control.stop()
730
+ if explicit_port:
731
+ break
732
+ continue
733
+
734
+ if explicit_port:
735
+ print(f"⚠️ Could not start supervisor control server on {host}:{candidate_ports[0]}: {last_error}")
736
+ else:
737
+ print(
738
+ "⚠️ Could not start supervisor control server on "
739
+ f"{host}:{candidate_ports[0]}-{candidate_ports[-1]}: {last_error}"
740
+ )
741
+ cleanup_supervisor_advertisement(self.workspace.path)
742
+ return None
698
743
 
699
744
  def _append_switch_log(
700
745
  self,
@@ -712,6 +757,7 @@ class EnvironmentCommands:
712
757
  *,
713
758
  source_env: str,
714
759
  supervisor_control: SwitchObserverServer | None,
760
+ backend_override: str | None,
715
761
  ) -> subprocess.CompletedProcess:
716
762
  """Start target ComfyUI under `cg run` switch supervision.
717
763
 
@@ -738,6 +784,8 @@ class EnvironmentCommands:
738
784
  child_env = os.environ.copy()
739
785
  child_env["COMFYGIT_ENV_NAME"] = env.name
740
786
  child_env["COMFYGIT_CG_RUN_SUPERVISOR"] = "1"
787
+ if backend_override:
788
+ child_env[ACTIVE_TORCH_BACKEND_OVERRIDE_ENV] = backend_override
741
789
 
742
790
  proc = subprocess.Popen(
743
791
  cmd,
@@ -1101,6 +1149,8 @@ class EnvironmentCommands:
1101
1149
  env = self._get_env(args)
1102
1150
 
1103
1151
  status = env.status()
1152
+ lifecycle_status = env.get_lifecycle_status(status=status)
1153
+ has_lifecycle_attention = bool(lifecycle_status.issues or lifecycle_status.primary_action)
1104
1154
 
1105
1155
  # Always show git state - never leave it blank
1106
1156
  if status.git.current_branch:
@@ -1111,7 +1161,7 @@ class EnvironmentCommands:
1111
1161
  # Clean state - everything is good (but check for detached HEAD)
1112
1162
  if status.is_synced and not status.git.has_changes and status.workflow.sync_status.total_count == 0:
1113
1163
  # Determine status indicator
1114
- if status.git.current_branch is None:
1164
+ if status.git.current_branch is None or has_lifecycle_attention:
1115
1165
  status_indicator = "⚠️" # Warning for detached HEAD even when clean
1116
1166
  else:
1117
1167
  status_indicator = "✓" # All good
@@ -1127,6 +1177,10 @@ class EnvironmentCommands:
1127
1177
 
1128
1178
  print("\n✓ No workflows")
1129
1179
  print("✓ No uncommitted changes")
1180
+ self._show_disabled_nodes(status)
1181
+ self._show_lifecycle_attention(lifecycle_status)
1182
+ if status.git.current_branch is not None:
1183
+ self._show_smart_suggestions(status, lifecycle_status)
1130
1184
 
1131
1185
  # Show legacy manager notice even in clean state
1132
1186
  self._show_legacy_manager_notice(env)
@@ -1208,7 +1262,15 @@ class EnvironmentCommands:
1208
1262
 
1209
1263
  # Environment drift (manual edits)
1210
1264
  if not status.comparison.is_synced:
1211
- print("\n⚠️ Environment needs repair:")
1265
+ has_filesystem_drift = bool(
1266
+ status.comparison.missing_nodes
1267
+ or status.comparison.extra_nodes
1268
+ or status.comparison.version_mismatches
1269
+ )
1270
+ if has_filesystem_drift:
1271
+ print("\n⚠️ Environment needs repair:")
1272
+ else:
1273
+ print("\n⚠️ Environment needs sync:")
1212
1274
 
1213
1275
  if status.comparison.missing_nodes:
1214
1276
  print(f" • {len(status.comparison.missing_nodes)} nodes in pyproject.toml not installed")
@@ -1229,10 +1291,7 @@ class EnvironmentCommands:
1229
1291
  print(" • Python packages out of sync")
1230
1292
 
1231
1293
  # Disabled nodes (informational, not a warning)
1232
- if status.comparison.disabled_nodes:
1233
- print("\n📴 Disabled nodes:")
1234
- for node_name in status.comparison.disabled_nodes:
1235
- print(f" • {node_name}")
1294
+ self._show_disabled_nodes(status)
1236
1295
 
1237
1296
  # Git changes
1238
1297
  if status.git.has_changes:
@@ -1277,14 +1336,24 @@ class EnvironmentCommands:
1277
1336
  else:
1278
1337
  print(" • Configuration updated")
1279
1338
 
1339
+ self._show_lifecycle_attention(lifecycle_status)
1340
+
1280
1341
  # Suggested actions - smart and contextual
1281
- self._show_smart_suggestions(status)
1342
+ self._show_smart_suggestions(status, lifecycle_status)
1282
1343
 
1283
1344
  # Show legacy manager notice if applicable
1284
1345
  self._show_legacy_manager_notice(env)
1285
1346
 
1286
1347
  # Removed: _has_uninstalled_packages - this logic is now in core's WorkflowAnalysisStatus
1287
1348
 
1349
+ def _show_disabled_nodes(self, status: EnvironmentStatus) -> None:
1350
+ """Display disabled nodes consistently across clean and dirty status paths."""
1351
+ if not status.comparison.disabled_nodes:
1352
+ return
1353
+ print("\n📴 Disabled nodes:")
1354
+ for node_name in status.comparison.disabled_nodes:
1355
+ print(f" • {node_name}")
1356
+
1288
1357
  def _print_workflow_issues(self, wf_analysis: WorkflowAnalysisStatus, verbose: bool = False) -> None:
1289
1358
  """Print compact workflow issues summary using model properties only."""
1290
1359
  # Build compact summary using WorkflowAnalysisStatus properties (no pyproject access!)
@@ -1353,164 +1422,284 @@ class EnvironmentCommands:
1353
1422
  else:
1354
1423
  print(f" ↳ {model.name}: in {model.actual_category}/, needs {expected}/")
1355
1424
 
1356
- def _show_smart_suggestions(self, status: EnvironmentStatus) -> None:
1357
- """Show contextual suggestions based on current state."""
1358
- suggestions = []
1359
-
1360
- # Differentiate workflow-related nodes from orphan nodes
1361
- uninstalled_workflow_nodes = set()
1362
- for wf in status.workflow.analyzed_workflows:
1363
- uninstalled_workflow_nodes.update(wf.uninstalled_nodes)
1364
-
1365
- orphan_missing_nodes = set(status.comparison.missing_nodes) - uninstalled_workflow_nodes
1366
- has_orphan_nodes = bool(orphan_missing_nodes or status.comparison.extra_nodes)
1367
-
1368
- # Missing models + environment drift: check if repair needed first
1369
- if status.missing_models and has_orphan_nodes:
1370
- suggestions.append("Install missing nodes: cg repair")
1371
-
1372
- # Group workflows with missing models
1373
- workflows_with_missing = {}
1374
- for missing_info in status.missing_models:
1375
- for wf_name in missing_info.workflow_names:
1376
- if wf_name not in workflows_with_missing:
1377
- workflows_with_missing[wf_name] = []
1378
- workflows_with_missing[wf_name].append(missing_info)
1379
-
1380
- if len(workflows_with_missing) == 1:
1381
- wf_name = list(workflows_with_missing.keys())[0]
1382
- suggestions.append(f"Then resolve workflow: cg workflow resolve \"{wf_name}\"")
1383
- else:
1384
- suggestions.append("Then resolve workflow (pick one):")
1385
- for wf_name in list(workflows_with_missing.keys())[:2]:
1386
- suggestions.append(f" cg workflow resolve \"{wf_name}\"")
1387
-
1425
+ def _show_smart_suggestions(
1426
+ self,
1427
+ status: EnvironmentStatus,
1428
+ lifecycle_status: EnvironmentLifecycleStatus,
1429
+ ) -> None:
1430
+ """Show contextual suggestions from lifecycle action IDs."""
1431
+ suggestions = self._format_lifecycle_suggestions(status, lifecycle_status)
1432
+ if suggestions:
1388
1433
  print("\n💡 Next:")
1389
1434
  for s in suggestions:
1390
1435
  print(f" {s}")
1436
+
1437
+ def _show_lifecycle_attention(
1438
+ self,
1439
+ lifecycle_status: EnvironmentLifecycleStatus,
1440
+ ) -> None:
1441
+ """Show lifecycle issues not already rendered by legacy status sections."""
1442
+ rendered_issue_ids = {
1443
+ "detached_head",
1444
+ "uncommitted_changes",
1445
+ "dependencies_not_synced",
1446
+ "missing_declared_nodes",
1447
+ "untracked_node_folder",
1448
+ "node_version_mismatch",
1449
+ "disabled_node",
1450
+ "new_workflow_added",
1451
+ "workflow_modified",
1452
+ "workflow_deleted",
1453
+ "workflow_changes",
1454
+ "workflow_download_intents",
1455
+ "workflow_unresolved_nodes",
1456
+ "workflow_uninstalled_nodes",
1457
+ "workflow_version_gated_nodes",
1458
+ "workflow_uninstallable_nodes",
1459
+ "missing_required_models",
1460
+ "missing_model_source",
1461
+ "model_path_mismatch",
1462
+ "model_category_mismatch",
1463
+ }
1464
+ issues = [
1465
+ issue
1466
+ for issue in lifecycle_status.issues
1467
+ if issue.id not in rendered_issue_ids
1468
+ ]
1469
+ if not issues:
1391
1470
  return
1392
1471
 
1393
- # Missing models only (no orphan nodes) - workflow resolve handles everything
1394
- if status.missing_models:
1395
- workflows_with_missing = {}
1396
- for missing_info in status.missing_models:
1397
- for wf_name in missing_info.workflow_names:
1398
- if wf_name not in workflows_with_missing:
1399
- workflows_with_missing[wf_name] = []
1400
- workflows_with_missing[wf_name].append(missing_info)
1401
-
1402
- if len(workflows_with_missing) == 1:
1403
- wf_name = list(workflows_with_missing.keys())[0]
1404
- suggestions.append(f"Resolve workflow: cg workflow resolve \"{wf_name}\"")
1472
+ print("\n⚠️ Environment needs attention:")
1473
+ for issue in issues:
1474
+ resources = ", ".join(issue.affected_resources)
1475
+ if resources:
1476
+ print(f" • {issue.message.rstrip('.')}: {resources}")
1405
1477
  else:
1406
- suggestions.append("Resolve workflows with missing models (pick one):")
1407
- for wf_name in list(workflows_with_missing.keys())[:3]:
1408
- suggestions.append(f" cg workflow resolve \"{wf_name}\"")
1409
- if len(workflows_with_missing) > 3:
1410
- suggestions.append(f" ... and {len(workflows_with_missing) - 3} more")
1478
+ print(f" • {issue.message}")
1411
1479
 
1412
- print("\n💡 Next:")
1413
- for s in suggestions:
1414
- print(f" {s}")
1415
- return
1416
-
1417
- # Environment drift only (no workflow issues)
1418
- if not status.comparison.is_synced:
1419
- # If only extra nodes, suggest tracking them as dev nodes
1420
- if status.comparison.extra_nodes and not status.comparison.missing_nodes and not status.comparison.version_mismatches and status.comparison.packages_in_sync:
1421
- if len(status.comparison.extra_nodes) == 1:
1422
- node_name = status.comparison.extra_nodes[0]
1423
- suggestions.append(f"Track as dev node: cg node add {node_name} --dev")
1424
- else:
1425
- suggestions.append("Track as dev nodes:")
1426
- for node_name in status.comparison.extra_nodes[:3]:
1427
- suggestions.append(f" cg node add {node_name} --dev")
1428
- if len(status.comparison.extra_nodes) > 3:
1429
- suggestions.append(f" ... and {len(status.comparison.extra_nodes) - 3} more")
1430
- suggestions.append("Or remove untracked: cg repair")
1480
+ def _format_lifecycle_suggestions(
1481
+ self,
1482
+ status: EnvironmentStatus,
1483
+ lifecycle_status: EnvironmentLifecycleStatus,
1484
+ ) -> list[str]:
1485
+ """Translate lifecycle actions into CLI commands and short guidance."""
1486
+ action = lifecycle_status.primary_action
1487
+ if action is None:
1488
+ return []
1489
+
1490
+ suggestions: list[str] = []
1491
+ action_id = action.id
1492
+
1493
+ if action_id == "create_branch":
1494
+ return ["Create a branch: cg checkout -b <branch-name>"]
1495
+
1496
+ if action_id in {"sync_missing_nodes", "sync_environment", "repair_environment"}:
1497
+ if action_id == "sync_missing_nodes":
1498
+ suggestions.append("Install missing nodes: cg sync")
1499
+ elif action_id == "sync_environment":
1500
+ suggestions.append("Sync environment: cg sync")
1431
1501
  else:
1432
1502
  suggestions.append("Run: cg repair")
1433
- print("\n💡 Next:")
1434
- for s in suggestions:
1435
- print(f" {s}")
1436
- return
1503
+ suggestions.extend(self._workflow_resolution_followups(status, prefix="Then resolve"))
1504
+ return suggestions
1505
+
1506
+ if action_id == "review_untracked_node":
1507
+ extra_nodes = list(status.comparison.extra_nodes)
1508
+ if len(extra_nodes) == 1:
1509
+ node_name = extra_nodes[0]
1510
+ suggestions.append(f"Review untracked node: {node_name}")
1511
+ suggestions.append(f"Track as dev node: cg node add {node_name} --dev")
1512
+ elif extra_nodes:
1513
+ suggestions.append("Review untracked nodes:")
1514
+ for node_name in extra_nodes[:3]:
1515
+ suggestions.append(f" cg node add {node_name} --dev")
1516
+ if len(extra_nodes) > 3:
1517
+ suggestions.append(f" ... and {len(extra_nodes) - 3} more")
1518
+ suggestions.append("Or remove untracked: cg repair")
1519
+ suggestions.extend(self._workflow_resolution_followups(status, prefix="Then resolve"))
1520
+ return suggestions
1521
+
1522
+ if action_id == "track_dev_node":
1523
+ dev_nodes = list(status.comparison.dev_nodes_untracked)
1524
+ if len(dev_nodes) == 1:
1525
+ suggestions.append(f"Track development node: cg node add {dev_nodes[0]} --dev")
1526
+ elif dev_nodes:
1527
+ suggestions.append("Track development nodes:")
1528
+ for node_name in dev_nodes[:3]:
1529
+ suggestions.append(f" cg node add {node_name} --dev")
1530
+ if len(dev_nodes) > 3:
1531
+ suggestions.append(f" ... and {len(dev_nodes) - 3} more")
1532
+ return suggestions
1533
+
1534
+ if action_id == "restore_or_relink_dev_node":
1535
+ missing_dev_nodes = list(status.comparison.dev_nodes_missing)
1536
+ if len(missing_dev_nodes) == 1:
1537
+ node_name = missing_dev_nodes[0]
1538
+ return [
1539
+ f"Restore checkout: ComfyUI/custom_nodes/{node_name}",
1540
+ f"Or untrack it: cg node remove {node_name} --dev --untrack",
1541
+ ]
1542
+ if missing_dev_nodes:
1543
+ suggestions.append("Restore missing development checkouts:")
1544
+ for node_name in missing_dev_nodes[:3]:
1545
+ suggestions.append(f" ComfyUI/custom_nodes/{node_name}")
1546
+ if len(missing_dev_nodes) > 3:
1547
+ suggestions.append(f" ... and {len(missing_dev_nodes) - 3} more")
1548
+ suggestions.append("Or untrack stale dev nodes: cg node remove <name> --dev --untrack")
1549
+ return suggestions
1550
+ return ["Restore missing development checkout or untrack it"]
1551
+
1552
+ if action_id == "review_workflow_changes":
1553
+ if status.workflow.sync_status.has_changes:
1554
+ if status.workflow.is_commit_safe:
1555
+ return ["Review workflow changes, then commit: cg commit -m \"<message>\""]
1556
+ return self._workflow_issue_suggestions(status, allow_commit_anyway=True)
1557
+ return ["Review workflow changes before committing"]
1558
+
1559
+ if action_id == "resolve_workflow_nodes":
1560
+ return self._workflow_issue_suggestions(status, allow_commit_anyway=True)
1561
+
1562
+ if action_id == "sync_model_paths":
1563
+ workflows = [
1564
+ workflow.name
1565
+ for workflow in status.workflow.analyzed_workflows
1566
+ if workflow.has_path_sync_issues
1567
+ ]
1568
+ if len(workflows) == 1:
1569
+ return [f"Sync model paths: cg workflow resolve \"{workflows[0]}\""]
1570
+ if workflows:
1571
+ return [f"Sync model paths in {len(workflows)} workflows: cg workflow resolve \"<name>\""]
1572
+ return [f"{action.label}: cg workflow resolve \"<workflow>\""]
1573
+
1574
+ if action_id == "download_required_models":
1575
+ workflows = self._workflows_with_downloads(status)
1576
+ if len(workflows) == 1:
1577
+ return [f"Complete downloads: cg workflow resolve \"{workflows[0]}\""]
1578
+ if workflows:
1579
+ suggestions.append("Complete downloads (pick one):")
1580
+ suggestions.extend(f" cg workflow resolve \"{name}\"" for name in workflows[:3])
1581
+ return suggestions
1582
+ return ["Download missing models: cg repair"]
1583
+
1584
+ if action_id == "resolve_missing_model":
1585
+ model_source_workflows = self._workflows_with_unresolved_models(status)
1586
+ if len(model_source_workflows) == 1:
1587
+ return [
1588
+ "Resolve missing models: "
1589
+ f"cg workflow resolve \"{model_source_workflows[0]}\""
1590
+ ]
1591
+ if model_source_workflows:
1592
+ suggestions.append("Resolve missing models (pick one):")
1593
+ suggestions.extend(
1594
+ f" cg workflow resolve \"{name}\""
1595
+ for name in model_source_workflows[:3]
1596
+ )
1597
+ if len(model_source_workflows) > 3:
1598
+ suggestions.append(f" ... and {len(model_source_workflows) - 3} more")
1599
+ return suggestions
1600
+ followups = self._workflow_resolution_followups(status, prefix="Resolve")
1601
+ if followups:
1602
+ return followups
1603
+ return ["Resolve missing models: cg workflow resolve \"<workflow>\""]
1437
1604
 
1438
- # Category mismatch (blocking - model in wrong directory for loader)
1439
- workflows_with_category_mismatch = [
1440
- w for w in status.workflow.analyzed_workflows
1441
- if w.has_category_mismatch_issues
1442
- ]
1605
+ if action_id == "add_model_source":
1606
+ return ["Add model source: cg model add-source <model> <url>"]
1443
1607
 
1444
- if workflows_with_category_mismatch:
1445
- suggestions.append("Models in wrong directory (move files manually):")
1446
- for wf in workflows_with_category_mismatch[:2]:
1447
- for m in wf.resolution.models_resolved:
1448
- if m.has_category_mismatch:
1449
- expected = m.expected_categories[0] if m.expected_categories else "unknown"
1450
- suggestions.append(f" {m.actual_category}/{m.name} → {expected}/")
1608
+ if action_id == "restart_comfyui":
1609
+ return ["Restart ComfyUI to load environment changes"]
1451
1610
 
1452
- print("\n💡 Next:")
1453
- for s in suggestions:
1454
- print(f" {s}")
1455
- return
1611
+ if action_id == "view_runtime_import_error":
1612
+ return ["View runtime import errors, then run: cg repair"]
1456
1613
 
1457
- # Path sync warnings (prioritize - quick fix!)
1458
- workflows_needing_sync = [
1459
- w for w in status.workflow.analyzed_workflows
1460
- if w.has_path_sync_issues
1461
- ]
1614
+ if action_id == "view_operation_logs":
1615
+ return ["View operation progress logs"]
1462
1616
 
1463
- if workflows_needing_sync:
1464
- workflow_names = [w.name for w in workflows_needing_sync]
1465
- if len(workflow_names) == 1:
1466
- suggestions.append(f"Sync model paths: cg workflow resolve \"{workflow_names[0]}\"")
1467
- else:
1468
- suggestions.append(f"Sync model paths in {len(workflow_names)} workflows: cg workflow resolve \"<name>\"")
1617
+ if action_id == "commit_snapshot":
1618
+ if status.workflow.sync_status.has_changes and status.workflow.is_commit_safe:
1619
+ return ["Commit workflows: cg commit -m \"<message>\""]
1620
+ return ["Commit changes: cg commit -m \"<message>\""]
1469
1621
 
1470
- # Check for workflows with download intents
1471
- workflows_with_downloads = []
1472
- for wf in status.workflow.analyzed_workflows:
1473
- download_intents = [m for m in wf.resolution.models_resolved if m.match_type == "download_intent"]
1474
- if download_intents:
1475
- workflows_with_downloads.append(wf.name)
1622
+ if action_id == "add_node_source_info":
1623
+ return ["Add node source info before export or deploy"]
1476
1624
 
1477
- # Workflows with issues (unresolved/ambiguous)
1478
- workflows_with_issues = [w.name for w in status.workflow.workflows_with_issues]
1479
- if workflows_with_issues:
1480
- if len(workflows_with_issues) == 1:
1481
- suggestions.append(f"Fix issues: cg workflow resolve \"{workflows_with_issues[0]}\"")
1482
- else:
1483
- suggestions.append("Fix workflows (pick one):")
1484
- for wf_name in workflows_with_issues[:3]:
1485
- suggestions.append(f" cg workflow resolve \"{wf_name}\"")
1486
- if len(workflows_with_issues) > 3:
1487
- suggestions.append(f" ... and {len(workflows_with_issues) - 3} more")
1488
-
1489
- # Only suggest committing if there are uncommitted changes
1490
- if status.git.has_changes:
1491
- suggestions.append("Or commit anyway: cg commit -m \"...\" --allow-issues")
1492
-
1493
- # Workflows with queued downloads (no other issues)
1494
- elif workflows_with_downloads:
1495
- if len(workflows_with_downloads) == 1:
1496
- suggestions.append(f"Complete downloads: cg workflow resolve \"{workflows_with_downloads[0]}\"")
1497
- else:
1498
- suggestions.append("Complete downloads (pick one):")
1499
- for wf_name in workflows_with_downloads[:3]:
1500
- suggestions.append(f" cg workflow resolve \"{wf_name}\"")
1625
+ if action_id == "fix_build_readiness":
1626
+ return ["Fix build readiness issues before export or deploy"]
1501
1627
 
1502
- # Ready to commit (workflow changes OR git changes)
1503
- elif status.workflow.sync_status.has_changes and status.workflow.is_commit_safe:
1504
- suggestions.append("Commit workflows: cg commit -m \"<message>\"")
1505
- elif status.git.has_changes:
1506
- # Uncommitted pyproject changes without workflow issues
1507
- suggestions.append("Commit changes: cg commit -m \"<message>\"")
1628
+ return [action.description]
1508
1629
 
1509
- # Show suggestions if any
1510
- if suggestions:
1511
- print("\n💡 Next:")
1512
- for s in suggestions:
1513
- print(f" {s}")
1630
+ def _workflow_resolution_followups(
1631
+ self,
1632
+ status: EnvironmentStatus,
1633
+ *,
1634
+ prefix: str,
1635
+ ) -> list[str]:
1636
+ workflows_with_missing: dict[str, list[object]] = {}
1637
+ for missing_info in status.missing_models:
1638
+ for workflow_name in missing_info.workflow_names:
1639
+ workflows_with_missing.setdefault(workflow_name, []).append(missing_info)
1640
+
1641
+ if workflows_with_missing:
1642
+ names = list(workflows_with_missing)
1643
+ if len(names) == 1:
1644
+ return [f"{prefix} workflow: cg workflow resolve \"{names[0]}\""]
1645
+ suggestions = [f"{prefix} workflow (pick one):"]
1646
+ suggestions.extend(f" cg workflow resolve \"{name}\"" for name in names[:3])
1647
+ if len(names) > 3:
1648
+ suggestions.append(f" ... and {len(names) - 3} more")
1649
+ return suggestions
1650
+
1651
+ issue_suggestions = self._workflow_issue_suggestions(status, allow_commit_anyway=False)
1652
+ if issue_suggestions:
1653
+ return issue_suggestions
1654
+ return []
1655
+
1656
+ def _workflow_issue_suggestions(
1657
+ self,
1658
+ status: EnvironmentStatus,
1659
+ *,
1660
+ allow_commit_anyway: bool,
1661
+ ) -> list[str]:
1662
+ workflows_with_issues = [workflow.name for workflow in status.workflow.workflows_with_issues]
1663
+ if not workflows_with_issues:
1664
+ return []
1665
+ suggestions: list[str] = []
1666
+ if len(workflows_with_issues) == 1:
1667
+ suggestions.append(f"Fix issues: cg workflow resolve \"{workflows_with_issues[0]}\"")
1668
+ else:
1669
+ suggestions.append("Fix workflows (pick one):")
1670
+ suggestions.extend(
1671
+ f" cg workflow resolve \"{name}\""
1672
+ for name in workflows_with_issues[:3]
1673
+ )
1674
+ if len(workflows_with_issues) > 3:
1675
+ suggestions.append(f" ... and {len(workflows_with_issues) - 3} more")
1676
+ if allow_commit_anyway and status.git.has_changes:
1677
+ suggestions.append("Or commit anyway: cg commit -m \"...\" --allow-issues")
1678
+ return suggestions
1679
+
1680
+ def _workflows_with_downloads(self, status: EnvironmentStatus) -> list[str]:
1681
+ workflows = []
1682
+ for workflow in status.workflow.analyzed_workflows:
1683
+ download_intents = [
1684
+ model
1685
+ for model in workflow.resolution.models_resolved
1686
+ if model.match_type == "download_intent"
1687
+ ]
1688
+ if download_intents:
1689
+ workflows.append(workflow.name)
1690
+ return workflows
1691
+
1692
+ def _workflows_with_unresolved_models(self, status: EnvironmentStatus) -> list[str]:
1693
+ workflows = []
1694
+ for workflow in status.workflow.analyzed_workflows:
1695
+ resolution = workflow.resolution
1696
+ unresolved = getattr(resolution, "models_unresolved", ())
1697
+ ambiguous = getattr(resolution, "models_ambiguous", ())
1698
+ has_unresolved = isinstance(unresolved, list | tuple) and bool(unresolved)
1699
+ has_ambiguous = isinstance(ambiguous, list | tuple) and bool(ambiguous)
1700
+ if has_unresolved or has_ambiguous:
1701
+ workflows.append(workflow.name)
1702
+ return workflows
1514
1703
 
1515
1704
  def _show_git_changes(self, status: EnvironmentStatus) -> None:
1516
1705
  """Helper method to show git changes in a structured way."""
@@ -1861,6 +2050,13 @@ class EnvironmentCommands:
1861
2050
  if result.filesystem_action == "deleted":
1862
2051
  print(" (cached globally, can reinstall)")
1863
2052
 
2053
+ if getattr(result, "sync_succeeded", True) is False:
2054
+ print("\n⚠️ Node was removed, but dependency sync still needs attention")
2055
+ sync_error = getattr(result, "sync_error", None)
2056
+ if sync_error:
2057
+ print(f" {sync_error}")
2058
+ print(f" Run 'cg -e {env.name} sync' after resolving the dependency issue")
2059
+
1864
2060
  print(f"\nRun 'cg -e {env.name} status' to review changes")
1865
2061
 
1866
2062
  @with_env_logging("node prune")
@@ -2312,12 +2508,12 @@ class EnvironmentCommands:
2312
2508
  # Get status first
2313
2509
  status = env.status()
2314
2510
 
2315
- if status.is_synced:
2511
+ if status.is_synced and not status.comparison.disabled_nodes:
2316
2512
  print("✓ No changes to apply")
2317
2513
  return
2318
2514
 
2319
2515
  # Get preview for display and later use
2320
- preview: dict[str, Any] = status.get_sync_preview()
2516
+ preview: dict[str, Any] = status.get_sync_preview(preserve_workflows=False)
2321
2517
 
2322
2518
  # Confirm unless --yes
2323
2519
  if not args.yes:
@@ -2325,6 +2521,7 @@ class EnvironmentCommands:
2325
2521
  # Check if there are actually any changes to show
2326
2522
  has_changes = (
2327
2523
  preview['nodes_to_install'] or
2524
+ preview.get('nodes_to_enable') or
2328
2525
  preview['nodes_to_remove'] or
2329
2526
  preview['nodes_to_update'] or
2330
2527
  preview['packages_to_sync'] or
@@ -2346,6 +2543,11 @@ class EnvironmentCommands:
2346
2543
  for node in preview['nodes_to_install']:
2347
2544
  print(f" - {node}")
2348
2545
 
2546
+ if preview.get('nodes_to_enable'):
2547
+ print(f" • Re-enable {len(preview['nodes_to_enable'])} disabled nodes:")
2548
+ for node in preview['nodes_to_enable']:
2549
+ print(f" - {node}")
2550
+
2349
2551
  if preview['nodes_to_remove']:
2350
2552
  print(f" • Remove {len(preview['nodes_to_remove'])} extra nodes:")
2351
2553
  for node in preview['nodes_to_remove']:
@@ -9,7 +9,7 @@ from __future__ import annotations
9
9
  import json
10
10
  import os
11
11
  from dataclasses import dataclass
12
- from datetime import UTC, datetime, timedelta
12
+ from datetime import datetime, timedelta, timezone
13
13
  from importlib.metadata import PackageNotFoundError
14
14
  from importlib.metadata import version as pkg_version
15
15
  from pathlib import Path
@@ -22,6 +22,7 @@ _PYPY_PROJECT = "comfygit"
22
22
  _PYPI_URL = f"https://pypi.org/pypi/{_PYPY_PROJECT}/json"
23
23
  _DEFAULT_TIMEOUT_S = 2.0
24
24
  _RECHECK_WINDOW = timedelta(hours=24)
25
+ UTC = timezone.utc
25
26
 
26
27
 
27
28
  def _is_truthy_env(value: str | None) -> bool:
@@ -1,14 +1,14 @@
1
1
  [project]
2
2
  name = "comfygit"
3
- version = "0.5.0"
3
+ version = "0.6.0"
4
4
  description = "ComfyGit - Git-based environment management for ComfyUI"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
7
7
  dependencies = [
8
- "comfygit-core==0.5.0",
9
- "comfygit-studio==0.5.0",
8
+ "comfygit-core==0.6.0",
9
+ "comfygit-studio==0.6.0",
10
10
  "argcomplete>=3.5.0",
11
- "requests>=2.33.0",
11
+ "requests>=2.32.4",
12
12
  ]
13
13
 
14
14
  [project.urls]
File without changes
File without changes
File without changes