comfygit 0.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,32 @@
1
+ """Utility functions for ComfyGit CLI."""
2
+
3
+ import sys
4
+ from typing import TYPE_CHECKING
5
+
6
+ from comfygit_core.factories.workspace_factory import WorkspaceFactory
7
+ from comfygit_core.models.exceptions import CDWorkspaceNotFoundError
8
+ from .logging.environment_logger import WorkspaceLogger
9
+
10
+ if TYPE_CHECKING:
11
+ from comfygit_core.core.workspace import Workspace
12
+
13
+ def get_workspace_or_exit() -> "Workspace":
14
+ """Get workspace or exit with error message."""
15
+ try:
16
+ workspace = WorkspaceFactory.find()
17
+ # Initialize workspace logging
18
+ WorkspaceLogger.set_workspace_path(workspace.path)
19
+ return workspace
20
+ except CDWorkspaceNotFoundError:
21
+ print("✗ No workspace initialized. Run 'cg init' first.")
22
+ sys.exit(1)
23
+
24
+ def get_workspace_optional() -> "Workspace | None":
25
+ """Get workspace if it exists."""
26
+ try:
27
+ workspace = WorkspaceFactory.find()
28
+ # Initialize workspace logging
29
+ WorkspaceLogger.set_workspace_path(workspace.path)
30
+ return workspace
31
+ except CDWorkspaceNotFoundError:
32
+ return None
@@ -0,0 +1,239 @@
1
+ """Custom argcomplete completers for ComfyGit CLI."""
2
+ import argparse
3
+ from typing import Any
4
+
5
+ from argcomplete.io import warn
6
+
7
+ from comfygit_core.core.environment import Environment
8
+ from comfygit_core.core.workspace import Workspace
9
+ from comfygit_core.factories.workspace_factory import WorkspaceFactory
10
+ from comfygit_core.models.exceptions import CDWorkspaceNotFoundError
11
+
12
+
13
+ # ============================================================================
14
+ # Shared Utilities
15
+ # ============================================================================
16
+
17
+ def get_workspace_safe() -> Workspace | None:
18
+ """Get workspace or return None if not initialized."""
19
+ try:
20
+ return WorkspaceFactory.find()
21
+ except CDWorkspaceNotFoundError:
22
+ return None
23
+ except Exception as e:
24
+ warn(f"Error loading workspace: {e}")
25
+ return None
26
+
27
+
28
+ def get_env_from_args(parsed_args: argparse.Namespace, workspace: Workspace) -> Environment | None:
29
+ """Get environment from -e flag or active environment.
30
+
31
+ Args:
32
+ parsed_args: Parsed arguments from argparse
33
+ workspace: Workspace instance
34
+
35
+ Returns:
36
+ Environment instance or None
37
+ """
38
+ try:
39
+ # Check for -e/--env flag
40
+ env_name = getattr(parsed_args, 'target_env', None)
41
+ if env_name:
42
+ return workspace.get_environment(env_name, auto_sync=False)
43
+
44
+ # Fall back to active environment
45
+ env = workspace.get_active_environment()
46
+ if not env:
47
+ warn("No active environment. Use -e or run 'cg use <env>'")
48
+ return env
49
+ except Exception as e:
50
+ warn(f"Error loading environment: {e}")
51
+ return None
52
+
53
+
54
+ def filter_by_prefix(items: list[str], prefix: str) -> list[str]:
55
+ """Filter items that start with the given prefix."""
56
+ return [item for item in items if item.startswith(prefix)]
57
+
58
+
59
+ # ============================================================================
60
+ # Completers
61
+ # ============================================================================
62
+
63
+ def environment_completer(prefix: str, parsed_args: argparse.Namespace, **kwargs: Any) -> list[str]:
64
+ """Complete environment names from workspace.
65
+
66
+ Used for:
67
+ - comfygit use <TAB>
68
+ - comfygit delete <TAB>
69
+ - comfygit -e <TAB>
70
+ """
71
+ workspace = get_workspace_safe()
72
+ if not workspace:
73
+ return []
74
+
75
+ try:
76
+ envs = workspace.list_environments()
77
+ names = [env.name for env in envs]
78
+ return filter_by_prefix(names, prefix)
79
+ except Exception as e:
80
+ warn(f"Error listing environments: {e}")
81
+ return []
82
+
83
+
84
+ def workflow_completer(prefix: str, parsed_args: argparse.Namespace, **kwargs: Any) -> list[str]:
85
+ """Complete workflow names, prioritizing unresolved workflows.
86
+
87
+ Smart ordering:
88
+ 1. New/modified workflows (likely need resolution)
89
+ 2. Synced workflows
90
+
91
+ Used for:
92
+ - comfygit workflow resolve <TAB>
93
+ """
94
+ workspace = get_workspace_safe()
95
+ if not workspace:
96
+ return []
97
+
98
+ env = get_env_from_args(parsed_args, workspace)
99
+ if not env:
100
+ return []
101
+
102
+ try:
103
+ workflows = env.list_workflows()
104
+
105
+ # Build candidates with smart ordering
106
+ candidates = []
107
+
108
+ # Priority 1: Unresolved workflows (new/modified)
109
+ candidates.extend(workflows.new)
110
+ candidates.extend(workflows.modified)
111
+
112
+ # Priority 2: Synced workflows
113
+ candidates.extend(workflows.synced)
114
+
115
+ # Remove .json extension and filter by prefix
116
+ names = [name.replace('.json', '') for name in candidates]
117
+ return filter_by_prefix(names, prefix)
118
+
119
+ except Exception as e:
120
+ warn(f"Error listing workflows: {e}")
121
+ return []
122
+
123
+
124
+ def installed_node_completer(prefix: str, parsed_args: argparse.Namespace, **kwargs: Any) -> list[str]:
125
+ """Complete installed node names.
126
+
127
+ Used for:
128
+ - comfygit node remove <TAB>
129
+ - comfygit node update <TAB>
130
+ """
131
+ workspace = get_workspace_safe()
132
+ if not workspace:
133
+ return []
134
+
135
+ env = get_env_from_args(parsed_args, workspace)
136
+ if not env:
137
+ return []
138
+
139
+ try:
140
+ nodes = env.list_nodes()
141
+ # Use registry_id if available, otherwise fall back to name
142
+ names = [node.registry_id or node.name for node in nodes]
143
+ return filter_by_prefix(names, prefix)
144
+ except Exception as e:
145
+ warn(f"Error listing nodes: {e}")
146
+ return []
147
+
148
+
149
+ def branch_completer(prefix: str, parsed_args: argparse.Namespace, **kwargs: Any) -> list[str]:
150
+ """Complete branch names from environment.
151
+
152
+ Returns only branch names for commands that accept branches.
153
+
154
+ Used for:
155
+ - cg switch <TAB>
156
+ """
157
+ workspace = get_workspace_safe()
158
+ if not workspace:
159
+ return []
160
+
161
+ env = get_env_from_args(parsed_args, workspace)
162
+ if not env:
163
+ return []
164
+
165
+ try:
166
+ # Get branches (returns list of (name, is_current) tuples)
167
+ branches = env.list_branches()
168
+ names = [name for name, _ in branches]
169
+ return filter_by_prefix(names, prefix)
170
+
171
+ except Exception as e:
172
+ warn(f"Error loading branches: {e}")
173
+ return []
174
+
175
+
176
+ def commit_hash_completer(prefix: str, parsed_args: argparse.Namespace, **kwargs: Any) -> list[str]:
177
+ """Complete commit hashes from environment history.
178
+
179
+ Returns only short commit hashes for clean tab completion.
180
+ Users can run 'cg log' to see commit messages.
181
+
182
+ Used for:
183
+ - cg reset <TAB>
184
+ """
185
+ workspace = get_workspace_safe()
186
+ if not workspace:
187
+ return []
188
+
189
+ env = get_env_from_args(parsed_args, workspace)
190
+ if not env:
191
+ return []
192
+
193
+ try:
194
+ # Get recent commits (50 should cover most use cases)
195
+ history = env.get_commit_history(limit=50)
196
+
197
+ # Return only hashes for clean completion
198
+ hashes = [commit['hash'] for commit in history]
199
+ return filter_by_prefix(hashes, prefix)
200
+
201
+ except Exception as e:
202
+ warn(f"Error loading commits: {e}")
203
+ return []
204
+
205
+
206
+ def ref_completer(prefix: str, parsed_args: argparse.Namespace, **kwargs: Any) -> list[str]:
207
+ """Complete git refs (branches and commits) from environment.
208
+
209
+ Returns branches first (most common use case), then recent commit hashes.
210
+ This provides comprehensive completion for commands like checkout that
211
+ accept both branches and commits.
212
+
213
+ Used for:
214
+ - cg checkout <TAB>
215
+ """
216
+ workspace = get_workspace_safe()
217
+ if not workspace:
218
+ return []
219
+
220
+ env = get_env_from_args(parsed_args, workspace)
221
+ if not env:
222
+ return []
223
+
224
+ try:
225
+ candidates = []
226
+
227
+ # Priority 1: Branches (most common for checkout)
228
+ branches = env.list_branches()
229
+ candidates.extend([name for name, _ in branches])
230
+
231
+ # Priority 2: Recent commits
232
+ history = env.get_commit_history(limit=50)
233
+ candidates.extend([commit['hash'] for commit in history])
234
+
235
+ return filter_by_prefix(candidates, prefix)
236
+
237
+ except Exception as e:
238
+ warn(f"Error loading refs: {e}")
239
+ return []
@@ -0,0 +1,246 @@
1
+ """Commands for managing shell completion setup."""
2
+ import os
3
+ import shutil
4
+ import subprocess
5
+ import sys
6
+ from pathlib import Path
7
+
8
+
9
+ class CompletionCommands:
10
+ """Handle shell completion installation and management."""
11
+
12
+ COMMANDS = ['comfygit', 'cg']
13
+ COMPLETION_COMMENT = "# ComfyGit tab completion"
14
+ ZSH_INIT_CHECK = 'if ! command -v compdef &> /dev/null; then'
15
+
16
+ @classmethod
17
+ def _completion_lines(cls):
18
+ """Generate completion lines for all command aliases."""
19
+ return [f'eval "$(register-python-argcomplete {cmd})"' for cmd in cls.COMMANDS]
20
+
21
+ @staticmethod
22
+ def _zsh_compinit_block():
23
+ """Generate zsh compinit initialization block."""
24
+ return [
25
+ "# Initialize zsh completion system if not already loaded",
26
+ "if ! command -v compdef &> /dev/null; then",
27
+ " autoload -Uz compinit",
28
+ " compinit",
29
+ "fi",
30
+ ]
31
+
32
+ @staticmethod
33
+ def _detect_shell():
34
+ """Detect the user's shell and return shell name and config file path."""
35
+ shell = os.environ.get('SHELL', '')
36
+
37
+ if 'bash' in shell:
38
+ config_file = Path.home() / '.bashrc'
39
+ return 'bash', config_file
40
+ elif 'zsh' in shell:
41
+ config_file = Path.home() / '.zshrc'
42
+ return 'zsh', config_file
43
+ else:
44
+ return None, None
45
+
46
+ @staticmethod
47
+ def _check_argcomplete_available():
48
+ """Check if register-python-argcomplete is available in PATH."""
49
+ return shutil.which('register-python-argcomplete') is not None
50
+
51
+ @staticmethod
52
+ def _install_argcomplete():
53
+ """Install argcomplete globally using uv tool."""
54
+ try:
55
+ print("📦 Installing argcomplete globally...")
56
+ result = subprocess.run(
57
+ ['uv', 'tool', 'install', 'argcomplete'],
58
+ capture_output=True,
59
+ text=True,
60
+ check=True
61
+ )
62
+ return True
63
+ except subprocess.CalledProcessError as e:
64
+ print(f"✗ Failed to install argcomplete: {e.stderr}")
65
+ return False
66
+ except FileNotFoundError:
67
+ print("✗ 'uv' command not found. Please install uv first.")
68
+ return False
69
+
70
+ @classmethod
71
+ def _is_completion_installed(cls, config_file):
72
+ """Check if completion is already installed in config file."""
73
+ if not config_file.exists():
74
+ return False
75
+
76
+ content = config_file.read_text()
77
+ return cls.COMPLETION_COMMENT in content and all(line in content for line in cls._completion_lines())
78
+
79
+ @classmethod
80
+ def _add_completion_to_config(cls, shell, config_file):
81
+ """Add completion lines to shell config file."""
82
+ # Ensure file exists
83
+ config_file.parent.mkdir(parents=True, exist_ok=True)
84
+ config_file.touch(exist_ok=True)
85
+
86
+ # Read current content
87
+ content = config_file.read_text()
88
+
89
+ # Add completion lines at the end
90
+ if content and not content.endswith('\n'):
91
+ content += '\n'
92
+
93
+ content += f'\n{cls.COMPLETION_COMMENT}\n'
94
+
95
+ # Add zsh compinit initialization if needed
96
+ if shell == 'zsh':
97
+ for line in cls._zsh_compinit_block():
98
+ content += f'{line}\n'
99
+ content += '\n'
100
+
101
+ for line in cls._completion_lines():
102
+ content += f'{line}\n'
103
+
104
+ # Write back
105
+ config_file.write_text(content)
106
+
107
+ @classmethod
108
+ def _remove_completion_from_config(cls, config_file):
109
+ """Remove completion lines from shell config file."""
110
+ if not config_file.exists():
111
+ return False
112
+
113
+ lines = config_file.read_text().splitlines(keepends=True)
114
+ new_lines = []
115
+ in_block = False
116
+
117
+ for line in lines:
118
+ # Start of completion block
119
+ if cls.COMPLETION_COMMENT in line:
120
+ in_block = True
121
+ continue
122
+
123
+ # Inside block - skip all lines until we find a non-completion line
124
+ if in_block:
125
+ # Check if this is part of our block (init, completion, or empty lines)
126
+ stripped = line.strip()
127
+ is_our_line = (
128
+ not stripped # empty line
129
+ or '# Initialize zsh completion system' in line # our specific comment
130
+ or cls.ZSH_INIT_CHECK in line # zsh init check
131
+ or 'autoload -Uz compinit' in line
132
+ or stripped == 'compinit'
133
+ or stripped == 'fi'
134
+ or any(comp in line for comp in cls._completion_lines())
135
+ )
136
+ if is_our_line:
137
+ continue
138
+ else:
139
+ # Non-completion line found, exit block
140
+ in_block = False
141
+
142
+ new_lines.append(line)
143
+
144
+ config_file.write_text(''.join(new_lines))
145
+ return True
146
+
147
+ def install(self, args):
148
+ """Install shell completion for the current user."""
149
+ shell, config_file = self._detect_shell()
150
+
151
+ if not shell:
152
+ print("✗ Could not detect shell (bash or zsh)")
153
+ print(" Your SHELL environment variable is:", os.environ.get('SHELL', 'not set'))
154
+ print("\nManual setup:")
155
+ print(" Add these lines to your shell config file:")
156
+ for line in self._completion_lines():
157
+ print(f" {line}")
158
+ sys.exit(1)
159
+
160
+ # Check if already installed
161
+ if self._is_completion_installed(config_file):
162
+ print(f"✓ Tab completion is already installed in {config_file}")
163
+ print(f"\nTo activate in current shell, run:")
164
+ print(f" source {config_file}")
165
+ return
166
+
167
+ # Check if argcomplete is available
168
+ if not self._check_argcomplete_available():
169
+ print("⚠️ argcomplete not found in PATH")
170
+ print(" Installing argcomplete as a uv tool...")
171
+ if not self._install_argcomplete():
172
+ print("\n✗ Could not install argcomplete automatically")
173
+ print("\nManual installation:")
174
+ print(" uv tool install argcomplete")
175
+ print("\nThen run:")
176
+ print(" cg completion install")
177
+ sys.exit(1)
178
+ print("✓ argcomplete installed")
179
+
180
+ # Install completion
181
+ try:
182
+ self._add_completion_to_config(shell, config_file)
183
+ print(f"\n✓ Tab completion installed successfully!")
184
+ print(f"\nAdded to: {config_file}")
185
+ print(f"\nTo activate in current shell, run:")
186
+ print(f" source {config_file}")
187
+ print(f"\nOr start a new terminal session.")
188
+ print(f"\nTry it out:")
189
+ print(f" cg stat<TAB>")
190
+ print(f" cg use <TAB>")
191
+ print(f" cg workflow resolve <TAB>")
192
+ except Exception as e:
193
+ print(f"✗ Failed to install completion: {e}")
194
+ sys.exit(1)
195
+
196
+ def uninstall(self, args):
197
+ """Remove shell completion from config."""
198
+ shell, config_file = self._detect_shell()
199
+
200
+ if not shell:
201
+ print("✗ Could not detect shell (bash or zsh)")
202
+ sys.exit(1)
203
+
204
+ if not self._is_completion_installed(config_file):
205
+ print(f"✓ Tab completion is not installed")
206
+ return
207
+
208
+ try:
209
+ self._remove_completion_from_config(config_file)
210
+ print(f"✓ Tab completion uninstalled")
211
+ print(f"\nRemoved from: {config_file}")
212
+ print(f"\nRestart your shell for changes to take effect.")
213
+ except Exception as e:
214
+ print(f"✗ Failed to uninstall completion: {e}")
215
+ sys.exit(1)
216
+
217
+ def status(self, args):
218
+ """Show completion installation status."""
219
+ shell, config_file = self._detect_shell()
220
+
221
+ print("Shell Completion Status")
222
+ print("=" * 40)
223
+
224
+ if not shell:
225
+ print("Shell: Unknown")
226
+ print("Status: ✗ Not supported")
227
+ print(f"\nYour SHELL: {os.environ.get('SHELL', 'not set')}")
228
+ print("Supported shells: bash, zsh")
229
+ return
230
+
231
+ print(f"Shell: {shell}")
232
+ print(f"Config: {config_file}")
233
+
234
+ # Check argcomplete availability
235
+ argcomplete_available = self._check_argcomplete_available()
236
+ print(f"Argcomplete: {'✓ Available' if argcomplete_available else '✗ Not found'}")
237
+
238
+ if self._is_completion_installed(config_file):
239
+ print("Status: ✓ Installed")
240
+ if not argcomplete_available:
241
+ print("\n⚠️ Warning: Completion is configured but argcomplete is not in PATH")
242
+ print(" Install with: uv tool install argcomplete")
243
+ print(f"\nTo uninstall: cg completion uninstall")
244
+ else:
245
+ print("Status: ✗ Not installed")
246
+ print(f"\nTo install: cg completion install")