comfygit 0.3.8.dev1__tar.gz → 0.3.10__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 (72) hide show
  1. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/PKG-INFO +2 -2
  2. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/comfygit_cli/cli.py +36 -1
  3. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/comfygit_cli/cli_utils.py +1 -0
  4. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/comfygit_cli/env_commands.py +104 -16
  5. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/comfygit_cli/global_commands.py +23 -48
  6. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/pyproject.toml +2 -2
  7. comfygit-0.3.10/tests/test_manager_commands.py +319 -0
  8. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/tests/test_torch_backend_cli.py +18 -18
  9. comfygit-0.3.8.dev1/tests/test_init_system_nodes.py +0 -126
  10. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/.github/workflows/publish_pypi.yml +0 -0
  11. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/.gitignore +0 -0
  12. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/.python-version +0 -0
  13. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/CLAUDE.md +0 -0
  14. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/LICENSE.txt +0 -0
  15. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/README.md +0 -0
  16. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/comfygit_cli/__init__.py +0 -0
  17. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/comfygit_cli/__main__.py +0 -0
  18. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/comfygit_cli/completers.py +0 -0
  19. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/comfygit_cli/completion_commands.py +0 -0
  20. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/comfygit_cli/formatters/__init__.py +0 -0
  21. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/comfygit_cli/formatters/error_formatter.py +0 -0
  22. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/comfygit_cli/interactive/__init__.py +0 -0
  23. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/comfygit_cli/logging/compressed_handler.py +0 -0
  24. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/comfygit_cli/logging/environment_logger.py +0 -0
  25. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/comfygit_cli/logging/log_compressor.py +0 -0
  26. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/comfygit_cli/logging/logging_config.py +0 -0
  27. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/comfygit_cli/resolution_strategies.py +0 -0
  28. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/comfygit_cli/strategies/__init__.py +0 -0
  29. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/comfygit_cli/strategies/conflict_resolver.py +0 -0
  30. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/comfygit_cli/strategies/interactive.py +0 -0
  31. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/comfygit_cli/strategies/rollback.py +0 -0
  32. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/comfygit_cli/utils/__init__.py +0 -0
  33. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/comfygit_cli/utils/civitai_errors.py +0 -0
  34. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/comfygit_cli/utils/orchestrator.py +0 -0
  35. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/comfygit_cli/utils/pagination.py +0 -0
  36. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/comfygit_cli/utils/progress.py +0 -0
  37. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/docs/architecture.md +0 -0
  38. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/scripts/README_REGISTRY.md +0 -0
  39. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/scripts/augment_mappings.py +0 -0
  40. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/scripts/build_global_mappings.py +0 -0
  41. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/scripts/build_registry_cache.py +0 -0
  42. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/scripts/extract_builtin_nodes.py +0 -0
  43. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/scripts/extract_node_modules.py +0 -0
  44. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/scripts/get_hash.py +0 -0
  45. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/scripts/global-node-mappings.md +0 -0
  46. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/scripts/registry.py +0 -0
  47. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/scripts/registry_client.py +0 -0
  48. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/scripts/test_concurrent_api.py +0 -0
  49. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/tests/conftest.py +0 -0
  50. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/tests/test_batch_node_add.py +0 -0
  51. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/tests/test_batch_node_remove.py +0 -0
  52. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/tests/test_branch_commands.py +0 -0
  53. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/tests/test_completers.py +0 -0
  54. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/tests/test_completion_commands.py +0 -0
  55. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/tests/test_conflict_resolver.py +0 -0
  56. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/tests/test_detached_head_display.py +0 -0
  57. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/tests/test_error_formatter.py +0 -0
  58. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/tests/test_log_command.py +0 -0
  59. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/tests/test_logging_structure.py +0 -0
  60. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/tests/test_manifest_command.py +0 -0
  61. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/tests/test_pagination.py +0 -0
  62. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/tests/test_preview_display.py +0 -0
  63. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/tests/test_py_commands.py +0 -0
  64. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/tests/test_py_remove_group_commands.py +0 -0
  65. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/tests/test_status_disabled_nodes_display.py +0 -0
  66. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/tests/test_status_displays_uninstalled_nodes.py +0 -0
  67. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/tests/test_status_path_sync_display.py +0 -0
  68. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/tests/test_status_real_bug_scenario.py +0 -0
  69. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/tests/test_status_suggestions.py +0 -0
  70. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/tests/test_status_uninstalled_reporting.py +0 -0
  71. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/tests/test_update_workflow_model_paths.py +0 -0
  72. {comfygit-0.3.8.dev1 → comfygit-0.3.10}/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.dev1
3
+ Version: 0.3.10
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.dev1
8
+ Requires-Dist: comfygit-core==0.3.10
9
9
  Description-Content-Type: text/markdown
10
10
 
11
11
  # ComfyGit CLI
@@ -165,7 +165,6 @@ def _add_global_commands(subparsers: argparse._SubParsersAction) -> None:
165
165
  init_parser.add_argument("path", type=Path, nargs="?", help="Workspace directory (default: ~/comfygit)")
166
166
  init_parser.add_argument("--models-dir", type=Path, help="Path to existing models directory to index")
167
167
  init_parser.add_argument("--yes", "-y", action="store_true", help="Use all defaults, no interactive prompts")
168
- init_parser.add_argument("--bare", action="store_true", help="Create workspace without system nodes (comfygit-manager)")
169
168
  init_parser.set_defaults(func=global_cmds.init)
170
169
 
171
170
  # list - List all environments
@@ -349,6 +348,26 @@ def _add_global_commands(subparsers: argparse._SubParsersAction) -> None:
349
348
  orch_logs_parser.add_argument("-n", "--lines", type=int, default=50, help="Number of lines to show (default: 50)")
350
349
  orch_logs_parser.set_defaults(func=global_cmds.orch_logs)
351
350
 
351
+ # Workspace management subcommands
352
+ workspace_parser = subparsers.add_parser("workspace", help="Workspace operations")
353
+ workspace_subparsers = workspace_parser.add_subparsers(
354
+ dest="workspace_command",
355
+ help="Workspace commands"
356
+ )
357
+ workspace_parser.set_defaults(func=_make_help_func(workspace_parser))
358
+
359
+ # workspace cleanup
360
+ workspace_cleanup_parser = workspace_subparsers.add_parser(
361
+ "cleanup",
362
+ help="Remove legacy workspace artifacts"
363
+ )
364
+ workspace_cleanup_parser.add_argument(
365
+ "--force",
366
+ action="store_true",
367
+ help="Skip verification and force cleanup"
368
+ )
369
+ workspace_cleanup_parser.set_defaults(func=global_cmds.workspace_cleanup)
370
+
352
371
 
353
372
  def _add_env_commands(subparsers: argparse._SubParsersAction) -> None:
354
373
  """Add environment-specific commands."""
@@ -796,5 +815,21 @@ def _add_env_commands(subparsers: argparse._SubParsersAction) -> None:
796
815
  )
797
816
  py_uv_parser.set_defaults(func=env_cmds.py_uv)
798
817
 
818
+ # Manager subcommands (per-environment comfygit-manager)
819
+ manager_parser = subparsers.add_parser("manager", help="Manage comfygit-manager installation")
820
+ manager_subparsers = manager_parser.add_subparsers(dest="manager_command", help="Manager commands")
821
+ manager_parser.set_defaults(func=_make_help_func(manager_parser))
822
+
823
+ # manager status
824
+ manager_status_parser = manager_subparsers.add_parser("status", help="Show manager version and update availability")
825
+ manager_status_parser.set_defaults(func=env_cmds.manager_status)
826
+
827
+ # manager update
828
+ manager_update_parser = manager_subparsers.add_parser("update", help="Update or migrate comfygit-manager")
829
+ manager_update_parser.add_argument("--version", help="Target version (default: latest)")
830
+ manager_update_parser.add_argument("-y", "--yes", action="store_true", help="Skip confirmation prompts")
831
+ manager_update_parser.set_defaults(func=env_cmds.manager_update)
832
+
833
+
799
834
  if __name__ == "__main__":
800
835
  main()
@@ -10,6 +10,7 @@ from .logging.environment_logger import WorkspaceLogger
10
10
  if TYPE_CHECKING:
11
11
  from comfygit_core.core.workspace import Workspace
12
12
 
13
+
13
14
  def get_workspace_or_exit() -> "Workspace":
14
15
  """Get workspace or exit with error message."""
15
16
  try:
@@ -88,6 +88,13 @@ class EnvironmentCommands:
88
88
  sys.exit(1)
89
89
  return active
90
90
 
91
+ def _get_python_version(self, env: Environment) -> str:
92
+ """Get Python version from environment."""
93
+ python_version_file = env.cec_path / ".python-version"
94
+ if python_version_file.exists():
95
+ return python_version_file.read_text(encoding="utf-8").strip()
96
+ return "3.12"
97
+
91
98
  def _get_or_probe_backend(
92
99
  self, env: Environment, override: str | None = None
93
100
  ) -> tuple[str, bool]:
@@ -104,29 +111,34 @@ class EnvironmentCommands:
104
111
  if override:
105
112
  return override, False
106
113
 
107
- if env.pytorch_manager.has_backend():
108
- return env.pytorch_manager.get_backend(), False
114
+ had_backend = env.pytorch_manager.has_backend()
109
115
 
110
- # No backend configured - probe and set it
111
- print("⚠️ No PyTorch backend configured. Auto-detecting...")
112
116
  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
117
+ python_version = self._get_python_version(env)
118
+ backend = env.pytorch_manager.ensure_backend(python_version)
119
+
120
+ was_probed = not had_backend
121
+ if was_probed:
122
+ print("⚠️ No PyTorch backend configured. Auto-detecting...")
123
+ print(f"✓ Backend detected and saved: {backend}")
124
+ print(" To change: cg env-config torch-backend set <backend>")
125
+
126
+ return backend, was_probed
125
127
  except Exception as e:
126
128
  print(f"✗ Error probing PyTorch backend: {e}")
127
129
  print(" Try setting it explicitly: cg env-config torch-backend set <backend>")
128
130
  sys.exit(1)
129
131
 
132
+ def _show_legacy_manager_notice(self, env: Environment) -> None:
133
+ """Show legacy manager notice if environment uses symlinked manager."""
134
+ try:
135
+ status = env.get_manager_status()
136
+ if status.is_legacy:
137
+ print("")
138
+ print("Legacy manager detected. Run 'cg manager update' to migrate.")
139
+ except Exception:
140
+ pass # Silently fail - notice is informational only
141
+
130
142
  def _format_size(self, size_bytes: int) -> str:
131
143
  """Format bytes as human-readable size."""
132
144
  for unit in ("B", "KB", "MB", "GB"):
@@ -611,6 +623,9 @@ class EnvironmentCommands:
611
623
 
612
624
  print("\n✓ No workflows")
613
625
  print("✓ No uncommitted changes")
626
+
627
+ # Show legacy manager notice even in clean state
628
+ self._show_legacy_manager_notice(env)
614
629
  return
615
630
 
616
631
  # Show environment name with branch
@@ -761,6 +776,9 @@ class EnvironmentCommands:
761
776
  # Suggested actions - smart and contextual
762
777
  self._show_smart_suggestions(status)
763
778
 
779
+ # Show legacy manager notice if applicable
780
+ self._show_legacy_manager_notice(env)
781
+
764
782
  # Removed: _has_uninstalled_packages - this logic is now in core's WorkflowAnalysisStatus
765
783
 
766
784
  def _print_workflow_issues(self, wf_analysis: WorkflowAnalysisStatus, verbose: bool = False) -> None:
@@ -2950,3 +2968,73 @@ class EnvironmentCommands:
2950
2968
  print(f" {m.actual_category}/{m.name} → {expected}/")
2951
2969
  else:
2952
2970
  print("✓ No changes needed - all dependencies already resolved")
2971
+
2972
+ # ================================================================================
2973
+ # Manager Commands - Per-environment comfygit-manager management
2974
+ # ================================================================================
2975
+
2976
+ @with_env_logging("manager status")
2977
+ def manager_status(self, args: argparse.Namespace, logger: Any = None) -> None:
2978
+ """Show manager version and update availability."""
2979
+ env = self._get_env(args)
2980
+
2981
+ status = env.get_manager_status()
2982
+
2983
+ print("comfygit-manager")
2984
+ print(f" Current: {status.current_version or 'not installed'}")
2985
+ print(f" Latest: {status.latest_version or 'unknown'}")
2986
+
2987
+ if status.is_legacy:
2988
+ print(" Legacy installation (symlinked)")
2989
+ print(f" Run 'cg -e {env.name} manager update' to migrate")
2990
+ elif not status.is_tracked:
2991
+ print(" Not installed")
2992
+ print(f" Run 'cg -e {env.name} manager update' to install")
2993
+ elif status.update_available:
2994
+ print(" Update available!")
2995
+ else:
2996
+ print(" Up to date")
2997
+
2998
+ @with_env_logging("manager update")
2999
+ def manager_update(self, args: argparse.Namespace, logger: Any = None) -> None:
3000
+ """Update or migrate comfygit-manager."""
3001
+ from comfygit_core.strategies.confirmation import AutoConfirmStrategy, InteractiveConfirmStrategy
3002
+
3003
+ env = self._get_env(args)
3004
+ version = getattr(args, 'version', None) or "latest"
3005
+ use_yes = getattr(args, 'yes', False)
3006
+
3007
+ # Ensure backend is configured (same as sync/run commands)
3008
+ had_backend = env.pytorch_manager.has_backend()
3009
+ if not had_backend:
3010
+ print("⚠️ No PyTorch backend configured. Auto-detecting...")
3011
+ python_version = self._get_python_version(env)
3012
+ backend = env.pytorch_manager.ensure_backend(python_version)
3013
+ print(f"✓ Backend detected and saved: {backend}")
3014
+ print(" To change: cg env-config torch-backend set <backend>\n")
3015
+
3016
+ status = env.get_manager_status()
3017
+
3018
+ if status.is_legacy:
3019
+ print("Migrating comfygit-manager to per-environment installation...")
3020
+ elif not status.is_tracked:
3021
+ print("Installing comfygit-manager...")
3022
+ else:
3023
+ print("Updating comfygit-manager...")
3024
+
3025
+ strategy = AutoConfirmStrategy() if use_yes else InteractiveConfirmStrategy()
3026
+
3027
+ try:
3028
+ result = env.update_manager(version=version, confirmation_strategy=strategy)
3029
+
3030
+ if result.changed:
3031
+ print(f" {result.message}")
3032
+ print("\n Restart this environment to use the new version")
3033
+ else:
3034
+ print(f" {result.message}")
3035
+
3036
+ except Exception as e:
3037
+ print(f" Failed to update manager: {e}", file=sys.stderr)
3038
+ if logger:
3039
+ logger.error(f"Manager update failed: {e}", exc_info=True)
3040
+ sys.exit(1)
@@ -16,15 +16,6 @@ from .utils import create_progress_callback, paginate, show_civitai_auth_help, s
16
16
 
17
17
  logger = get_logger(__name__)
18
18
 
19
- # Default system nodes to install with new workspaces.
20
- # These are infrastructure custom nodes that provide management capabilities.
21
- # Use `cg init --bare` to skip installation.
22
- DEFAULT_SYSTEM_NODES = {
23
- "comfygit-manager": {
24
- "url": "https://github.com/comfyhub-org/comfygit-manager.git",
25
- "description": "ComfyGit management panel for ComfyUI",
26
- },
27
- }
28
19
 
29
20
 
30
21
  class GlobalCommands:
@@ -141,12 +132,6 @@ class GlobalCommands:
141
132
 
142
133
  print(f"✓ Workspace initialized at {workspace.path}")
143
134
 
144
- # Install default system nodes (unless --bare)
145
- if not getattr(args, 'bare', False):
146
- self._install_system_nodes(workspace)
147
- else:
148
- print("📦 Skipping system node installation (--bare flag)")
149
-
150
135
  # Handle models directory setup
151
136
  self._setup_models_directory(workspace, args)
152
137
 
@@ -162,39 +147,6 @@ class GlobalCommands:
162
147
  print(f"✗ Failed to initialize workspace: {e}", file=sys.stderr)
163
148
  sys.exit(1)
164
149
 
165
- def _install_system_nodes(self, workspace: Workspace) -> None:
166
- """Install default system nodes (comfygit-manager) into workspace.
167
-
168
- System nodes are infrastructure custom nodes that:
169
- - Live at workspace level (.metadata/system_nodes/)
170
- - Are symlinked into every environment
171
- - Are never tracked in pyproject.toml
172
- """
173
- from comfygit_core.utils.git import git_clone
174
-
175
- system_nodes_path = workspace.paths.system_nodes
176
-
177
- for node_name, node_config in DEFAULT_SYSTEM_NODES.items():
178
- target_path = system_nodes_path / node_name
179
-
180
- if target_path.exists():
181
- logger.debug(f"System node '{node_name}' already exists, skipping")
182
- continue
183
-
184
- print(f"📦 Installing system node: {node_name}")
185
- try:
186
- git_clone(
187
- url=node_config["url"],
188
- target_path=target_path,
189
- depth=1 # Shallow clone for speed
190
- )
191
- print(f" ✓ Installed {node_name}")
192
- logger.info(f"Installed system node: {node_name}")
193
- except Exception as e:
194
- print(f" ⚠️ Failed to install {node_name}: {e}")
195
- print(" You can install it manually later")
196
- logger.warning(f"Failed to install system node {node_name}: {e}")
197
-
198
150
  def _show_workspace_env_setup(self, workspace_path: Path) -> None:
199
151
  """Show instructions for setting COMFYGIT_HOME for custom workspace location."""
200
152
  import os
@@ -1811,3 +1763,26 @@ class GlobalCommands:
1811
1763
  else:
1812
1764
  print("(empty log file)")
1813
1765
 
1766
+ # === Workspace Management ===
1767
+
1768
+ @with_workspace_logging("workspace cleanup")
1769
+ def workspace_cleanup(self, args: argparse.Namespace) -> None:
1770
+ """Clean up legacy workspace artifacts.
1771
+
1772
+ Removes .metadata/system_nodes/ directory if no environments
1773
+ still use legacy symlinked manager.
1774
+ """
1775
+ force = getattr(args, 'force', False)
1776
+
1777
+ result = self.workspace.cleanup_legacy_system_nodes(force=force)
1778
+
1779
+ if result.success:
1780
+ print(f"Removed {result.removed_path}")
1781
+ else:
1782
+ if result.legacy_environments:
1783
+ print("Cannot cleanup: Some environments still use legacy manager")
1784
+ for env in result.legacy_environments:
1785
+ print(f" {env}")
1786
+ print("\nRun 'cg -e <ENV> manager update' to migrate, then retry.")
1787
+ else:
1788
+ print(f"{result.message}")
@@ -1,11 +1,11 @@
1
1
  [project]
2
2
  name = "comfygit"
3
- version = "0.3.8.dev1"
3
+ version = "0.3.10"
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.3.8.dev1",
8
+ "comfygit-core==0.3.10",
9
9
  "argcomplete>=3.5.0",
10
10
  ]
11
11
 
@@ -0,0 +1,319 @@
1
+ """Tests for manager commands (cg manager status/update)."""
2
+ import argparse
3
+ from unittest.mock import MagicMock, patch
4
+
5
+ import pytest
6
+
7
+ from comfygit_core.models.shared import ManagerStatus, ManagerUpdateResult
8
+
9
+
10
+ class TestManagerCommands:
11
+ """Tests for manager status and update CLI commands."""
12
+
13
+ def test_manager_status_shows_not_installed(self, capsys):
14
+ """manager status shows 'not installed' when manager is missing."""
15
+ from comfygit_cli.env_commands import EnvironmentCommands
16
+
17
+ env_cmds = EnvironmentCommands()
18
+
19
+ # Mock environment
20
+ mock_env = MagicMock()
21
+ mock_env.name = "test-env"
22
+ mock_env.get_manager_status.return_value = ManagerStatus(
23
+ current_version=None,
24
+ latest_version="0.3.0",
25
+ update_available=False,
26
+ is_legacy=False,
27
+ is_tracked=False,
28
+ )
29
+
30
+ with patch.object(env_cmds, "_get_env", return_value=mock_env):
31
+ args = argparse.Namespace(target_env="test-env")
32
+ env_cmds.manager_status(args)
33
+
34
+ captured = capsys.readouterr()
35
+ assert "not installed" in captured.out
36
+ assert "manager update" in captured.out
37
+
38
+ def test_manager_status_shows_legacy(self, capsys):
39
+ """manager status shows legacy notice for symlinked installations."""
40
+ from comfygit_cli.env_commands import EnvironmentCommands
41
+
42
+ env_cmds = EnvironmentCommands()
43
+
44
+ mock_env = MagicMock()
45
+ mock_env.name = "test-env"
46
+ mock_env.get_manager_status.return_value = ManagerStatus(
47
+ current_version="0.2.0",
48
+ latest_version="0.3.0",
49
+ update_available=True,
50
+ is_legacy=True,
51
+ is_tracked=False,
52
+ )
53
+
54
+ with patch.object(env_cmds, "_get_env", return_value=mock_env):
55
+ args = argparse.Namespace(target_env="test-env")
56
+ env_cmds.manager_status(args)
57
+
58
+ captured = capsys.readouterr()
59
+ assert "Legacy" in captured.out
60
+ assert "manager update" in captured.out
61
+
62
+ def test_manager_status_shows_update_available(self, capsys):
63
+ """manager status shows update available for tracked installations."""
64
+ from comfygit_cli.env_commands import EnvironmentCommands
65
+
66
+ env_cmds = EnvironmentCommands()
67
+
68
+ mock_env = MagicMock()
69
+ mock_env.name = "test-env"
70
+ mock_env.get_manager_status.return_value = ManagerStatus(
71
+ current_version="0.2.0",
72
+ latest_version="0.3.0",
73
+ update_available=True,
74
+ is_legacy=False,
75
+ is_tracked=True,
76
+ )
77
+
78
+ with patch.object(env_cmds, "_get_env", return_value=mock_env):
79
+ args = argparse.Namespace(target_env="test-env")
80
+ env_cmds.manager_status(args)
81
+
82
+ captured = capsys.readouterr()
83
+ assert "Update available" in captured.out
84
+ assert "0.2.0" in captured.out
85
+ assert "0.3.0" in captured.out
86
+
87
+ def test_manager_status_shows_up_to_date(self, capsys):
88
+ """manager status shows up to date when no update needed."""
89
+ from comfygit_cli.env_commands import EnvironmentCommands
90
+
91
+ env_cmds = EnvironmentCommands()
92
+
93
+ mock_env = MagicMock()
94
+ mock_env.name = "test-env"
95
+ mock_env.get_manager_status.return_value = ManagerStatus(
96
+ current_version="0.3.0",
97
+ latest_version="0.3.0",
98
+ update_available=False,
99
+ is_legacy=False,
100
+ is_tracked=True,
101
+ )
102
+
103
+ with patch.object(env_cmds, "_get_env", return_value=mock_env):
104
+ args = argparse.Namespace(target_env="test-env")
105
+ env_cmds.manager_status(args)
106
+
107
+ captured = capsys.readouterr()
108
+ assert "Up to date" in captured.out
109
+
110
+ def test_manager_update_calls_env_update_manager(self, capsys):
111
+ """manager update calls environment update_manager method."""
112
+ from comfygit_cli.env_commands import EnvironmentCommands
113
+
114
+ env_cmds = EnvironmentCommands()
115
+
116
+ mock_env = MagicMock()
117
+ mock_env.name = "test-env"
118
+ mock_env.get_manager_status.return_value = ManagerStatus(
119
+ current_version="0.2.0",
120
+ latest_version="0.3.0",
121
+ update_available=True,
122
+ is_legacy=False,
123
+ is_tracked=True,
124
+ )
125
+ mock_env.update_manager.return_value = ManagerUpdateResult(
126
+ changed=True,
127
+ message="Updated from 0.2.0 to 0.3.0",
128
+ old_version="0.2.0",
129
+ new_version="0.3.0",
130
+ )
131
+
132
+ with patch.object(env_cmds, "_get_env", return_value=mock_env):
133
+ args = argparse.Namespace(target_env="test-env", version=None, yes=True)
134
+ env_cmds.manager_update(args)
135
+
136
+ mock_env.update_manager.assert_called_once()
137
+ captured = capsys.readouterr()
138
+ assert "Updated" in captured.out or "Updating" in captured.out
139
+
140
+ def test_manager_update_shows_migration_message_for_legacy(self, capsys):
141
+ """manager update shows migration message for legacy installations."""
142
+ from comfygit_cli.env_commands import EnvironmentCommands
143
+
144
+ env_cmds = EnvironmentCommands()
145
+
146
+ mock_env = MagicMock()
147
+ mock_env.name = "test-env"
148
+ mock_env.get_manager_status.return_value = ManagerStatus(
149
+ current_version="0.2.0",
150
+ latest_version="0.3.0",
151
+ update_available=True,
152
+ is_legacy=True,
153
+ is_tracked=False,
154
+ )
155
+ mock_env.update_manager.return_value = ManagerUpdateResult(
156
+ changed=True,
157
+ was_migration=True,
158
+ message="Migrated and updated to 0.3.0",
159
+ old_version="0.2.0",
160
+ new_version="0.3.0",
161
+ )
162
+
163
+ with patch.object(env_cmds, "_get_env", return_value=mock_env):
164
+ args = argparse.Namespace(target_env="test-env", version=None, yes=True)
165
+ env_cmds.manager_update(args)
166
+
167
+ captured = capsys.readouterr()
168
+ assert "Migrating" in captured.out
169
+
170
+
171
+ class TestInitNoSystemNodes:
172
+ """Tests that init no longer installs system nodes."""
173
+
174
+ def test_init_does_not_install_system_nodes(self, tmp_path):
175
+ """init should not call _install_system_nodes (removed)."""
176
+ from comfygit_cli.global_commands import GlobalCommands
177
+
178
+ global_cmds = GlobalCommands()
179
+
180
+ # Mock workspace factory and creation
181
+ mock_workspace = MagicMock()
182
+ mock_workspace.paths.root = tmp_path
183
+ mock_workspace.path = tmp_path
184
+ mock_workspace.update_registry_data.return_value = True
185
+ mock_workspace.get_models_directory.return_value = tmp_path / "models"
186
+
187
+ with patch("comfygit_cli.global_commands.WorkspaceFactory") as mock_factory:
188
+ mock_factory.get_paths.return_value = mock_workspace.paths
189
+ mock_factory.create.return_value = mock_workspace
190
+
191
+ with patch.object(global_cmds, "_setup_models_directory"):
192
+ args = argparse.Namespace(
193
+ path=None,
194
+ models_dir=None,
195
+ yes=True,
196
+ )
197
+
198
+ global_cmds.init(args)
199
+
200
+ # Verify workspace was created (no system nodes installation)
201
+ mock_factory.create.assert_called_once()
202
+
203
+ def test_bare_flag_no_longer_exists(self):
204
+ """--bare flag should not exist in init parser."""
205
+ from comfygit_cli.cli import create_parser
206
+
207
+ parser = create_parser()
208
+
209
+ # Find the init subparser through the subparsers action
210
+ init_action = None
211
+ for action in parser._subparsers._actions:
212
+ if hasattr(action, "choices") and action.choices is not None:
213
+ if "init" in action.choices:
214
+ init_action = action.choices["init"]
215
+ break
216
+
217
+ assert init_action is not None, "init subparser should exist"
218
+
219
+ # Check that --bare is not in the init parser
220
+ option_strings = []
221
+ for action in init_action._actions:
222
+ option_strings.extend(action.option_strings)
223
+
224
+ assert "--bare" not in option_strings
225
+
226
+
227
+ class TestStatusLegacyManagerNotice:
228
+ """Tests that status command shows legacy manager notice."""
229
+
230
+ def test_status_shows_legacy_notice_even_when_clean(self, capsys):
231
+ """status should show legacy manager notice even when environment is clean.
232
+
233
+ Bug: Previously the status command returned early for clean environments,
234
+ skipping the legacy manager notice check.
235
+ """
236
+ from comfygit_cli.env_commands import EnvironmentCommands
237
+ from comfygit_core.models.environment import (
238
+ EnvironmentComparison,
239
+ EnvironmentStatus,
240
+ GitStatus,
241
+ )
242
+ from comfygit_core.models.shared import ManagerStatus
243
+ from comfygit_core.models.workflow import DetailedWorkflowStatus, WorkflowSyncStatus
244
+
245
+ env_cmds = EnvironmentCommands()
246
+
247
+ # Mock a clean environment with legacy manager
248
+ mock_env = MagicMock()
249
+ mock_env.name = "test-env"
250
+
251
+ # Create clean status (no workflows, no changes, synced)
252
+ mock_env.status.return_value = EnvironmentStatus(
253
+ git=GitStatus(
254
+ current_branch="main",
255
+ has_changes=False,
256
+ ),
257
+ workflow=DetailedWorkflowStatus(
258
+ sync_status=WorkflowSyncStatus(
259
+ synced=[],
260
+ new=[],
261
+ modified=[],
262
+ deleted=[],
263
+ ),
264
+ analyzed_workflows=[],
265
+ ),
266
+ comparison=EnvironmentComparison(
267
+ missing_nodes=[],
268
+ extra_nodes=[],
269
+ version_mismatches=[],
270
+ packages_in_sync=True,
271
+ ),
272
+ missing_models=[],
273
+ )
274
+
275
+ # Legacy manager detected
276
+ mock_env.get_manager_status.return_value = ManagerStatus(
277
+ current_version="0.2.0",
278
+ latest_version="0.3.0",
279
+ update_available=True,
280
+ is_legacy=True,
281
+ is_tracked=False,
282
+ )
283
+
284
+ with patch.object(env_cmds, "_get_env", return_value=mock_env):
285
+ args = argparse.Namespace(target_env="test-env", verbose=False)
286
+ env_cmds.status(args)
287
+
288
+ captured = capsys.readouterr()
289
+ # Should show clean state
290
+ assert "No workflows" in captured.out
291
+ assert "No uncommitted changes" in captured.out
292
+ # AND also show legacy notice
293
+ assert "Legacy manager detected" in captured.out
294
+
295
+
296
+ class TestLegacyWorkspaceNotice:
297
+ """Tests for legacy notice in get_workspace_or_exit.
298
+
299
+ Note: Legacy notices have been moved to per-environment level.
300
+ The workspace-level notice has been removed. These tests verify
301
+ that get_workspace_or_exit() does NOT show legacy notices.
302
+ """
303
+
304
+ def test_no_legacy_notice_at_workspace_level(self, capsys):
305
+ """Workspace-level legacy notice has been removed."""
306
+ from comfygit_cli import cli_utils
307
+
308
+ mock_workspace = MagicMock()
309
+ mock_workspace.has_legacy_system_nodes.return_value = True
310
+
311
+ with patch.object(cli_utils.WorkspaceFactory, "find", return_value=mock_workspace):
312
+ with patch.object(cli_utils.WorkspaceLogger, "set_workspace_path"):
313
+ result = cli_utils.get_workspace_or_exit()
314
+
315
+ assert result == mock_workspace
316
+ captured = capsys.readouterr()
317
+ # Legacy notices are now per-environment, not workspace-level
318
+ assert "Legacy workspace" not in captured.out
319
+ assert "Legacy" not in captured.out
@@ -152,15 +152,17 @@ class TestSyncBehavior:
152
152
  """Test that sync reads from file and doesn't overwrite user settings."""
153
153
 
154
154
  @patch('comfygit_cli.env_commands.get_workspace_or_exit')
155
- def test_sync_reads_from_backend_file(self, mock_get_workspace):
156
- """Sync should read backend from .pytorch-backend file."""
155
+ def test_sync_uses_ensure_backend(self, mock_get_workspace):
156
+ """Sync should use ensure_backend() which handles both existing and missing backends."""
157
157
  from comfygit_cli.env_commands import EnvironmentCommands
158
158
 
159
159
  # Setup mocks
160
160
  mock_env = MagicMock()
161
161
  mock_env.name = "test-env"
162
- mock_env.pytorch_manager.backend_file.exists.return_value = True
163
- mock_env.pytorch_manager.get_backend.return_value = "cu128"
162
+ mock_env.cec_path = MagicMock()
163
+ mock_env.cec_path.__truediv__ = MagicMock(return_value=MagicMock(exists=MagicMock(return_value=True)))
164
+ mock_env.pytorch_manager.has_backend.return_value = True
165
+ mock_env.pytorch_manager.ensure_backend.return_value = "cu128"
164
166
  mock_env.sync.return_value = MagicMock(success=True, packages_synced=0, dependency_groups_installed=[], errors=[])
165
167
 
166
168
  mock_workspace = MagicMock()
@@ -175,17 +177,14 @@ class TestSyncBehavior:
175
177
 
176
178
  args = argparse.Namespace(
177
179
  target_env=None,
178
- torch_backend=None, # No override - should read from file
180
+ torch_backend=None, # No override - should use ensure_backend
179
181
  verbose=False
180
182
  )
181
183
 
182
184
  cmd.sync(args)
183
185
 
184
- # Should have read from file, NOT called detect_backend
185
- mock_env.pytorch_manager.get_backend.assert_called()
186
- mock_env.pytorch_manager.detect_backend.assert_not_called()
187
- # Should NOT have called set_backend (no file write)
188
- mock_env.pytorch_manager.set_backend.assert_not_called()
186
+ # Should have called ensure_backend which reads from file or probes
187
+ mock_env.pytorch_manager.ensure_backend.assert_called()
189
188
 
190
189
  @patch('comfygit_cli.env_commands.get_workspace_or_exit')
191
190
  def test_sync_warns_when_no_backend_file(self, mock_get_workspace, capsys, tmp_path):
@@ -257,15 +256,17 @@ class TestRunBehavior:
257
256
  """Test that run reads from file like sync does."""
258
257
 
259
258
  @patch('comfygit_cli.env_commands.get_workspace_or_exit')
260
- def test_run_uses_backend_from_file(self, mock_get_workspace):
261
- """Run should use backend from .pytorch-backend file."""
259
+ def test_run_uses_ensure_backend(self, mock_get_workspace):
260
+ """Run should use ensure_backend() which handles both existing and missing backends."""
262
261
  from comfygit_cli.env_commands import EnvironmentCommands
263
262
 
264
263
  mock_env = MagicMock()
265
264
  mock_env.name = "test-env"
266
265
  mock_env.get_current_branch.return_value = "main"
267
- mock_env.pytorch_manager.backend_file.exists.return_value = True
268
- mock_env.pytorch_manager.get_backend.return_value = "cu128"
266
+ mock_env.cec_path = MagicMock()
267
+ mock_env.cec_path.__truediv__ = MagicMock(return_value=MagicMock(exists=MagicMock(return_value=True)))
268
+ mock_env.pytorch_manager.has_backend.return_value = True
269
+ mock_env.pytorch_manager.ensure_backend.return_value = "cu128"
269
270
  mock_env.sync.return_value = MagicMock(success=True)
270
271
  mock_env.run.return_value = MagicMock(returncode=0)
271
272
 
@@ -279,7 +280,7 @@ class TestRunBehavior:
279
280
 
280
281
  args = argparse.Namespace(
281
282
  target_env=None,
282
- torch_backend=None, # Should read from file
283
+ torch_backend=None, # Should use ensure_backend
283
284
  no_sync=False,
284
285
  args=[]
285
286
  )
@@ -287,6 +288,5 @@ class TestRunBehavior:
287
288
  with pytest.raises(SystemExit) as exc_info:
288
289
  cmd.run(args)
289
290
 
290
- # Should have read from file, not auto-detected
291
- mock_env.pytorch_manager.get_backend.assert_called()
292
- mock_env.pytorch_manager.detect_backend.assert_not_called()
291
+ # Should have called ensure_backend which reads from file or probes
292
+ mock_env.pytorch_manager.ensure_backend.assert_called()
@@ -1,126 +0,0 @@
1
- """Tests for system node installation during workspace init."""
2
- import argparse
3
- from unittest.mock import MagicMock, patch
4
-
5
- import pytest
6
-
7
-
8
- class TestInitSystemNodes:
9
- """Tests for system node installation in init command."""
10
-
11
- def test_default_system_nodes_constant_exists(self):
12
- """DEFAULT_SYSTEM_NODES constant should exist and include comfygit-manager."""
13
- from comfygit_cli.global_commands import DEFAULT_SYSTEM_NODES
14
-
15
- assert DEFAULT_SYSTEM_NODES is not None
16
- assert "comfygit-manager" in DEFAULT_SYSTEM_NODES
17
- assert "url" in DEFAULT_SYSTEM_NODES["comfygit-manager"]
18
-
19
- def test_install_system_nodes_clones_comfygit_manager(self, tmp_path):
20
- """_install_system_nodes should clone comfygit-manager to system_nodes."""
21
- from comfygit_cli.global_commands import GlobalCommands
22
-
23
- # Create mock workspace
24
- mock_workspace = MagicMock()
25
- mock_workspace.paths.system_nodes = tmp_path / "system_nodes"
26
- mock_workspace.paths.system_nodes.mkdir(parents=True)
27
-
28
- global_cmds = GlobalCommands()
29
-
30
- # Patch at the source module where git_clone is defined
31
- with patch("comfygit_core.utils.git.git_clone") as mock_clone:
32
- global_cmds._install_system_nodes(mock_workspace)
33
-
34
- # Verify git_clone was called
35
- mock_clone.assert_called_once()
36
- call_args = mock_clone.call_args
37
-
38
- # Check URL
39
- assert "comfygit-manager" in call_args.kwargs["url"]
40
- # Check target path
41
- assert str(call_args.kwargs["target_path"]).endswith("comfygit-manager")
42
- # Check shallow clone
43
- assert call_args.kwargs["depth"] == 1
44
-
45
- def test_install_system_nodes_skips_existing(self, tmp_path):
46
- """_install_system_nodes should skip if node already exists."""
47
- from comfygit_cli.global_commands import GlobalCommands
48
-
49
- # Create mock workspace with existing comfygit-manager
50
- mock_workspace = MagicMock()
51
- mock_workspace.paths.system_nodes = tmp_path / "system_nodes"
52
- mock_workspace.paths.system_nodes.mkdir(parents=True)
53
- (mock_workspace.paths.system_nodes / "comfygit-manager").mkdir()
54
-
55
- global_cmds = GlobalCommands()
56
-
57
- # Patch at the source module where git_clone is defined
58
- with patch("comfygit_core.utils.git.git_clone") as mock_clone:
59
- global_cmds._install_system_nodes(mock_workspace)
60
-
61
- # git_clone should NOT be called since directory exists
62
- mock_clone.assert_not_called()
63
-
64
- def test_bare_flag_skips_system_nodes(self, tmp_path, monkeypatch):
65
- """init with --bare flag should skip system node installation."""
66
- from comfygit_cli.global_commands import GlobalCommands
67
-
68
- global_cmds = GlobalCommands()
69
-
70
- # Mock workspace factory and creation
71
- mock_workspace = MagicMock()
72
- mock_workspace.paths.root = tmp_path
73
- mock_workspace.paths.system_nodes = tmp_path / "system_nodes"
74
- mock_workspace.path = tmp_path
75
- mock_workspace.update_registry_data.return_value = True
76
- mock_workspace.get_models_directory.return_value = tmp_path / "models"
77
-
78
- with patch("comfygit_cli.global_commands.WorkspaceFactory") as mock_factory:
79
- mock_factory.get_paths.return_value = mock_workspace.paths
80
- mock_factory.create.return_value = mock_workspace
81
-
82
- with patch.object(global_cmds, "_install_system_nodes") as mock_install:
83
- with patch.object(global_cmds, "_setup_models_directory"):
84
- args = argparse.Namespace(
85
- path=None,
86
- models_dir=None,
87
- yes=True,
88
- bare=True
89
- )
90
-
91
- global_cmds.init(args)
92
-
93
- # _install_system_nodes should NOT be called
94
- mock_install.assert_not_called()
95
-
96
- def test_init_calls_install_system_nodes_by_default(self, tmp_path, monkeypatch):
97
- """init without --bare should call _install_system_nodes."""
98
- from comfygit_cli.global_commands import GlobalCommands
99
-
100
- global_cmds = GlobalCommands()
101
-
102
- # Mock workspace factory and creation
103
- mock_workspace = MagicMock()
104
- mock_workspace.paths.root = tmp_path
105
- mock_workspace.paths.system_nodes = tmp_path / "system_nodes"
106
- mock_workspace.path = tmp_path
107
- mock_workspace.update_registry_data.return_value = True
108
- mock_workspace.get_models_directory.return_value = tmp_path / "models"
109
-
110
- with patch("comfygit_cli.global_commands.WorkspaceFactory") as mock_factory:
111
- mock_factory.get_paths.return_value = mock_workspace.paths
112
- mock_factory.create.return_value = mock_workspace
113
-
114
- with patch.object(global_cmds, "_install_system_nodes") as mock_install:
115
- with patch.object(global_cmds, "_setup_models_directory"):
116
- args = argparse.Namespace(
117
- path=None,
118
- models_dir=None,
119
- yes=True,
120
- bare=False
121
- )
122
-
123
- global_cmds.init(args)
124
-
125
- # _install_system_nodes SHOULD be called
126
- mock_install.assert_called_once_with(mock_workspace)
File without changes
File without changes
File without changes
File without changes
File without changes