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.
- {comfygit-0.5.0 → comfygit-0.6.0}/PKG-INFO +4 -4
- {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/cli.py +18 -0
- {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/env_commands.py +387 -185
- {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/utils/update_checker.py +2 -1
- {comfygit-0.5.0 → comfygit-0.6.0}/pyproject.toml +4 -4
- {comfygit-0.5.0 → comfygit-0.6.0}/.gitignore +0 -0
- {comfygit-0.5.0 → comfygit-0.6.0}/LICENSE.txt +0 -0
- {comfygit-0.5.0 → comfygit-0.6.0}/README.md +0 -0
- {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/__init__.py +0 -0
- {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/__main__.py +0 -0
- {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/cli_utils.py +0 -0
- {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/completers.py +0 -0
- {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/completion_commands.py +0 -0
- {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/formatters/__init__.py +0 -0
- {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/formatters/error_formatter.py +0 -0
- {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/global_commands.py +0 -0
- {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/interactive/__init__.py +0 -0
- {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/logging/compressed_handler.py +0 -0
- {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/logging/environment_logger.py +0 -0
- {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/logging/log_compressor.py +0 -0
- {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/logging/logging_config.py +0 -0
- {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/resolution_strategies.py +0 -0
- {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/strategies/__init__.py +0 -0
- {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/strategies/confirmation.py +0 -0
- {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/strategies/conflict_resolver.py +0 -0
- {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/strategies/interactive.py +0 -0
- {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/strategies/rollback.py +0 -0
- {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/update_commands.py +0 -0
- {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/utils/__init__.py +0 -0
- {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/utils/civitai_errors.py +0 -0
- {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/utils/formatting.py +0 -0
- {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/utils/orchestrator.py +0 -0
- {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/utils/pagination.py +0 -0
- {comfygit-0.5.0 → comfygit-0.6.0}/comfygit_cli/utils/progress.py +0 -0
- {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.
|
|
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.
|
|
12
|
-
Requires-Dist: comfygit-studio==0.
|
|
13
|
-
Requires-Dist: requests>=2.
|
|
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
|
|
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
|
-
|
|
579
|
-
if
|
|
580
|
-
|
|
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(
|
|
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
|
-
|
|
679
|
-
|
|
680
|
-
return None
|
|
695
|
+
endpoint = resolve_comfyui_endpoint(comfyui_args)
|
|
696
|
+
explicit_port = port_text is not None
|
|
681
697
|
|
|
682
|
-
|
|
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
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
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
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
suggestions.append("
|
|
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
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
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
|
-
|
|
1439
|
-
|
|
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
|
|
1445
|
-
|
|
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
|
-
|
|
1453
|
-
|
|
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
|
-
|
|
1458
|
-
|
|
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
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
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
|
-
|
|
1471
|
-
|
|
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
|
-
|
|
1478
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
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
|
|
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.
|
|
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.
|
|
9
|
-
"comfygit-studio==0.
|
|
8
|
+
"comfygit-core==0.6.0",
|
|
9
|
+
"comfygit-studio==0.6.0",
|
|
10
10
|
"argcomplete>=3.5.0",
|
|
11
|
-
"requests>=2.
|
|
11
|
+
"requests>=2.32.4",
|
|
12
12
|
]
|
|
13
13
|
|
|
14
14
|
[project.urls]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|