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.
comfygit_cli/cli.py ADDED
@@ -0,0 +1,704 @@
1
+ """ComfyGit MVP CLI - Workspace and Environment Management."""
2
+ # PYTHON_ARGCOMPLETE_OK
3
+
4
+ import argparse
5
+ import sys
6
+ from importlib.metadata import version, PackageNotFoundError
7
+ from pathlib import Path
8
+
9
+ import argcomplete
10
+
11
+ from .completion_commands import CompletionCommands
12
+ from .completers import (
13
+ branch_completer,
14
+ commit_hash_completer,
15
+ environment_completer,
16
+ installed_node_completer,
17
+ ref_completer,
18
+ workflow_completer,
19
+ )
20
+ from .env_commands import EnvironmentCommands
21
+ from .global_commands import GlobalCommands
22
+ from .logging.logging_config import setup_logging
23
+
24
+ try:
25
+ __version__ = version("comfygit")
26
+ except PackageNotFoundError:
27
+ __version__ = "unknown"
28
+
29
+
30
+ def _get_comfygit_config_dir() -> Path:
31
+ """Get ComfyGit config directory (creates if needed)."""
32
+ config_dir = Path.home() / ".config" / "comfygit"
33
+ config_dir.mkdir(parents=True, exist_ok=True)
34
+ return config_dir
35
+
36
+
37
+ def _check_for_old_docker_installation() -> None:
38
+ """Warn once about old Docker-based ComfyDock installation."""
39
+ old_config = Path.home() / ".comfydock" / "environments.json"
40
+ if not old_config.exists():
41
+ return # No old Docker installation detected
42
+
43
+ # Check if we've already shown this warning
44
+ warning_flag = _get_comfygit_config_dir() / ".docker_warning_shown"
45
+ if warning_flag.exists():
46
+ return # Already warned user
47
+
48
+ # Show warning (compact, informative)
49
+ print("\n" + "="*70)
50
+ print("ℹ️ OLD DOCKER-BASED COMFYDOCK DETECTED")
51
+ print("="*70)
52
+ print("\nYou have an old Docker-based ComfyDock (v0.3.x) at ~/.comfydock")
53
+ print("This is the NEW ComfyGit v1.0+ (UV-based).")
54
+ print("\nKey differences:")
55
+ print(" • Old version: Docker containers, 'comfydock' command")
56
+ print(" • New version: UV packages, 'comfygit' command")
57
+ print("\nBoth versions can coexist. Your old environments are unchanged.")
58
+ print("\nTo use old version: pip install comfydock==0.1.6")
59
+ print("To use new version: comfygit init")
60
+ print("\nMigration guide: https://github.com/comfyhub-org/comfygit/blob/main/MIGRATION.md")
61
+ print("="*70 + "\n")
62
+
63
+ # Mark warning as shown
64
+ warning_flag.touch()
65
+
66
+
67
+ def main() -> None:
68
+ """Main entry point for ComfyGit CLI."""
69
+ # Enable readline for input() line editing (arrow keys, history)
70
+ # Unix/Linux/macOS: provides full editing capability
71
+ # Windows: gracefully falls back to native console editing
72
+ try:
73
+ import readline # noqa: F401
74
+ except ImportError:
75
+ pass
76
+
77
+ # Check for old Docker installation (show warning once)
78
+ _check_for_old_docker_installation()
79
+
80
+ # Initialize logging system with minimal console output
81
+ # Environment commands will add file handlers as needed
82
+ setup_logging(level="INFO", simple_format=True, console_level="CRITICAL")
83
+
84
+ # Special handling for 'run' command to pass through ComfyUI args
85
+ parser = create_parser()
86
+ if 'run' in sys.argv:
87
+ # Parse known args, pass unknown to ComfyUI
88
+ args, unknown = parser.parse_known_args()
89
+ if getattr(args, 'command', None) == 'run':
90
+ args.args = unknown
91
+ else:
92
+ # Not actually the run command, do normal parsing
93
+ args = parser.parse_args()
94
+ else:
95
+ # Normal parsing for all other commands
96
+ args = parser.parse_args()
97
+
98
+ if not hasattr(args, 'func'):
99
+ parser.print_help()
100
+ sys.exit(1)
101
+
102
+ try:
103
+ # Execute the command
104
+ args.func(args)
105
+ except KeyboardInterrupt:
106
+ print("\n✗ Interrupted")
107
+ sys.exit(130)
108
+ except Exception as e:
109
+ print(f"✗ Error: {e}", file=sys.stderr)
110
+ sys.exit(1)
111
+
112
+
113
+ def create_parser() -> argparse.ArgumentParser:
114
+ """Create the argument parser with hierarchical command structure."""
115
+ parser = argparse.ArgumentParser(
116
+ description="ComfyGit - Manage ComfyUI workspaces and environments",
117
+ prog="cg"
118
+ )
119
+
120
+ # Global options
121
+ parser.add_argument(
122
+ '--version',
123
+ action='version',
124
+ version=f'ComfyGit CLI v{__version__}',
125
+ help='Show version and exit'
126
+ )
127
+ parser.add_argument(
128
+ '-e', '--env',
129
+ help='Target environment (uses active if not specified)',
130
+ dest='target_env'
131
+ ).completer = environment_completer # type: ignore[attr-defined]
132
+ parser.add_argument(
133
+ '-v', '--verbose',
134
+ action='store_true',
135
+ help='Verbose output'
136
+ )
137
+
138
+ subparsers = parser.add_subparsers(dest="command", help="Available commands")
139
+
140
+ # Add all commands (workspace and environment)
141
+ _add_global_commands(subparsers)
142
+ _add_env_commands(subparsers)
143
+
144
+ # Enable argcomplete for tab completion
145
+ argcomplete.autocomplete(parser)
146
+
147
+ return parser
148
+
149
+
150
+ def _add_global_commands(subparsers: argparse._SubParsersAction) -> None:
151
+ """Add global workspace-level commands."""
152
+ global_cmds = GlobalCommands()
153
+
154
+ # init - Initialize workspace
155
+ init_parser = subparsers.add_parser("init", help="Initialize ComfyGit workspace")
156
+ init_parser.add_argument("path", type=Path, nargs="?", help="Workspace directory (default: ~/comfygit)")
157
+ init_parser.add_argument("--models-dir", type=Path, help="Path to existing models directory to index")
158
+ init_parser.add_argument("--yes", "-y", action="store_true", help="Use all defaults, no interactive prompts")
159
+ init_parser.add_argument("--bare", action="store_true", help="Create workspace without system nodes (comfygit-manager)")
160
+ init_parser.set_defaults(func=global_cmds.init)
161
+
162
+ # list - List all environments
163
+ list_parser = subparsers.add_parser("list", help="List all environments")
164
+ list_parser.set_defaults(func=global_cmds.list_envs)
165
+
166
+ # migrate - Import existing ComfyUI
167
+ # migrate_parser = subparsers.add_parser("migrate", help="Scan and import existing ComfyUI instance")
168
+ # migrate_parser.add_argument("source_path", type=Path, help="Path to existing ComfyUI")
169
+ # migrate_parser.add_argument("env_name", help="New environment name")
170
+ # migrate_parser.add_argument("--scan-only", action="store_true", help="Only scan, don't import")
171
+ # migrate_parser.set_defaults(func=global_cmds.migrate)
172
+
173
+ # import - Import ComfyGit environment
174
+ import_parser = subparsers.add_parser("import", help="Import ComfyGit environment from tarball or git repository")
175
+ import_parser.add_argument("path", type=str, nargs="?", help="Path to .tar.gz file or git repository URL (use #subdirectory for subdirectory imports)")
176
+ import_parser.add_argument("--name", type=str, help="Name for imported environment (skip prompt)")
177
+ import_parser.add_argument("--branch", "-b", type=str, help="Git branch, tag, or commit to import (git imports only)")
178
+ import_parser.add_argument(
179
+ "--torch-backend",
180
+ default="auto",
181
+ metavar="BACKEND",
182
+ help=(
183
+ "PyTorch backend. Examples: auto (detect GPU), cpu, "
184
+ "cu128 (CUDA 12.8), cu126, cu124, rocm6.3 (AMD), xpu (Intel). "
185
+ "Default: auto"
186
+ ),
187
+ )
188
+ import_parser.add_argument("--use", action="store_true", help="Set imported environment as active")
189
+ import_parser.add_argument("-y", "--yes", action="store_true", help="Skip confirmation prompts, use defaults for workspace initialization")
190
+ import_parser.set_defaults(func=global_cmds.import_env)
191
+
192
+ # export - Export ComfyGit environment
193
+ export_parser = subparsers.add_parser("export", help="Export ComfyGit environment (include relevant files from .cec)")
194
+ export_parser.add_argument("path", type=Path, nargs="?", help="Path to output file")
195
+ export_parser.add_argument("--allow-issues", action="store_true", help="Skip confirmation if models are missing source URLs")
196
+ export_parser.set_defaults(func=global_cmds.export_env)
197
+
198
+ # Model management subcommands
199
+ model_parser = subparsers.add_parser("model", help="Manage model index")
200
+ model_subparsers = model_parser.add_subparsers(dest="model_command", help="Model commands")
201
+
202
+ # model index subcommands
203
+ model_index_parser = model_subparsers.add_parser("index", help="Model index operations")
204
+ model_index_subparsers = model_index_parser.add_subparsers(dest="model_index_command", help="Model index commands")
205
+
206
+ # model index find
207
+ model_index_find_parser = model_index_subparsers.add_parser("find", help="Find models by hash or filename")
208
+ model_index_find_parser.add_argument("query", help="Search query (hash prefix or filename)")
209
+ model_index_find_parser.set_defaults(func=global_cmds.model_index_find)
210
+
211
+ # model index list
212
+ model_index_list_parser = model_index_subparsers.add_parser("list", help="List all indexed models")
213
+ model_index_list_parser.add_argument("--duplicates", action="store_true", help="Show only models with multiple locations")
214
+ model_index_list_parser.set_defaults(func=global_cmds.model_index_list)
215
+
216
+ # model index show
217
+ model_index_show_parser = model_index_subparsers.add_parser("show", help="Show detailed model information")
218
+ model_index_show_parser.add_argument("identifier", help="Model hash, hash prefix, filename, or path")
219
+ model_index_show_parser.set_defaults(func=global_cmds.model_index_show)
220
+
221
+ # model index status
222
+ model_index_status_parser = model_index_subparsers.add_parser("status", help="Show models directory and index status")
223
+ model_index_status_parser.set_defaults(func=global_cmds.model_index_status)
224
+
225
+ # model index sync
226
+ model_index_sync_parser = model_index_subparsers.add_parser("sync", help="Scan models directory and update index")
227
+ model_index_sync_parser.set_defaults(func=global_cmds.model_index_sync)
228
+
229
+ # model index dir
230
+ model_index_dir_parser = model_index_subparsers.add_parser("dir", help="Set global models directory to index")
231
+ model_index_dir_parser.add_argument("path", type=Path, help="Path to models directory")
232
+ model_index_dir_parser.set_defaults(func=global_cmds.model_dir_add)
233
+
234
+ # model download
235
+ model_download_parser = model_subparsers.add_parser("download", help="Download model from URL")
236
+ model_download_parser.add_argument("url", help="Model download URL (Civitai, HuggingFace, or direct)")
237
+ model_download_parser.add_argument("--path", type=str, help="Target path relative to models directory (e.g., checkpoints/model.safetensors)")
238
+ model_download_parser.add_argument("-c", "--category", type=str, help="Model category for auto-path (e.g., checkpoints, loras, vae)")
239
+ model_download_parser.add_argument("-y", "--yes", action="store_true", help="Skip path confirmation prompt")
240
+ model_download_parser.set_defaults(func=global_cmds.model_download)
241
+
242
+ # model add-source
243
+ model_add_source_parser = model_subparsers.add_parser("add-source", help="Add download source URL to model(s)")
244
+ model_add_source_parser.add_argument("model", nargs="?", help="Model filename or hash (omit for interactive mode)")
245
+ model_add_source_parser.add_argument("url", nargs="?", help="Download URL")
246
+ model_add_source_parser.set_defaults(func=global_cmds.model_add_source)
247
+
248
+ # Registry management subcommands
249
+ registry_parser = subparsers.add_parser("registry", help="Manage node registry cache")
250
+ registry_subparsers = registry_parser.add_subparsers(dest="registry_command", help="Registry commands")
251
+
252
+ # registry status
253
+ registry_status_parser = registry_subparsers.add_parser("status", help="Show registry cache status")
254
+ registry_status_parser.set_defaults(func=global_cmds.registry_status)
255
+
256
+ # registry update
257
+ registry_update_parser = registry_subparsers.add_parser("update", help="Update registry data from GitHub")
258
+ registry_update_parser.set_defaults(func=global_cmds.registry_update)
259
+
260
+ # Config management
261
+ config_parser = subparsers.add_parser("config", help="Manage configuration settings")
262
+ config_parser.add_argument("--civitai-key", type=str, help="Set Civitai API key (use empty string to clear)")
263
+ config_parser.add_argument("--show", action="store_true", help="Show current configuration")
264
+ config_parser.set_defaults(func=global_cmds.config)
265
+
266
+ # debug - Show application logs for debugging
267
+ debug_parser = subparsers.add_parser("debug", help="Show application debug logs")
268
+ debug_parser.add_argument("-n", "--lines", type=int, default=200, help="Number of lines to show (default: 200)")
269
+ debug_parser.add_argument("--level", choices=["DEBUG", "INFO", "WARNING", "ERROR"], help="Filter by log level")
270
+ debug_parser.add_argument("--full", action="store_true", help="Show all logs (no line limit)")
271
+ debug_parser.add_argument("--workspace", action="store_true", help="Show workspace logs instead of environment logs")
272
+ debug_parser.set_defaults(func=global_cmds.debug)
273
+
274
+ # Shell completion management
275
+ completion_cmds = CompletionCommands()
276
+ completion_parser = subparsers.add_parser("completion", help="Manage shell tab completion")
277
+ completion_subparsers = completion_parser.add_subparsers(dest="completion_command", help="Completion commands")
278
+
279
+ # completion install
280
+ completion_install_parser = completion_subparsers.add_parser("install", help="Install tab completion for your shell")
281
+ completion_install_parser.set_defaults(func=completion_cmds.install)
282
+
283
+ # completion uninstall
284
+ completion_uninstall_parser = completion_subparsers.add_parser("uninstall", help="Remove tab completion from your shell")
285
+ completion_uninstall_parser.set_defaults(func=completion_cmds.uninstall)
286
+
287
+ # completion status
288
+ completion_status_parser = completion_subparsers.add_parser("status", help="Show tab completion installation status")
289
+ completion_status_parser.set_defaults(func=completion_cmds.status)
290
+
291
+ # Orchestrator management subcommands
292
+ orch_parser = subparsers.add_parser(
293
+ "orch",
294
+ aliases=["orchestrator"],
295
+ help="Monitor and control orchestrator"
296
+ )
297
+ orch_subparsers = orch_parser.add_subparsers(
298
+ dest="orch_command",
299
+ help="Orchestrator commands"
300
+ )
301
+
302
+ # orch status
303
+ orch_status_parser = orch_subparsers.add_parser("status", help="Show orchestrator status")
304
+ orch_status_parser.add_argument("--json", action="store_true", help="Output as JSON")
305
+ orch_status_parser.set_defaults(func=global_cmds.orch_status)
306
+
307
+ # orch restart
308
+ orch_restart_parser = orch_subparsers.add_parser("restart", help="Restart ComfyUI")
309
+ orch_restart_parser.add_argument("--wait", action="store_true", help="Wait for restart to complete")
310
+ orch_restart_parser.set_defaults(func=global_cmds.orch_restart)
311
+
312
+ # orch kill
313
+ orch_kill_parser = orch_subparsers.add_parser("kill", help="Shutdown orchestrator")
314
+ orch_kill_parser.add_argument("--force", action="store_true", help="Force kill (bypass command queue)")
315
+ orch_kill_parser.set_defaults(func=global_cmds.orch_kill)
316
+
317
+ # orch clean
318
+ orch_clean_parser = orch_subparsers.add_parser("clean", help="Clean orchestrator state")
319
+ orch_clean_parser.add_argument("--dry-run", action="store_true", help="Show what would be deleted")
320
+ orch_clean_parser.add_argument("--force", action="store_true", help="Skip confirmation")
321
+ orch_clean_parser.add_argument("--kill", action="store_true", help="Also kill orchestrator process")
322
+ orch_clean_parser.set_defaults(func=global_cmds.orch_clean)
323
+
324
+ # orch logs
325
+ orch_logs_parser = orch_subparsers.add_parser("logs", help="Show orchestrator logs")
326
+ orch_logs_parser.add_argument("-f", "--follow", action="store_true", help="Follow logs in real-time")
327
+ orch_logs_parser.add_argument("-n", "--lines", type=int, default=50, help="Number of lines to show (default: 50)")
328
+ orch_logs_parser.set_defaults(func=global_cmds.orch_logs)
329
+
330
+
331
+ def _add_env_commands(subparsers: argparse._SubParsersAction) -> None:
332
+ """Add environment-specific commands."""
333
+ env_cmds = EnvironmentCommands()
334
+
335
+ # Environment Management Commands (operate ON environments)
336
+
337
+ # create - Create new environment
338
+ create_parser = subparsers.add_parser("create", help="Create new environment")
339
+ create_parser.add_argument("name", help="Environment name")
340
+ create_parser.add_argument("--template", type=Path, help="Template manifest")
341
+ create_parser.add_argument("--python", default="3.11", help="Python version")
342
+ create_parser.add_argument("--comfyui", help="ComfyUI version")
343
+ create_parser.add_argument(
344
+ "--torch-backend",
345
+ default="auto",
346
+ metavar="BACKEND",
347
+ help=(
348
+ "PyTorch backend. Examples: auto (detect GPU), cpu, "
349
+ "cu128 (CUDA 12.8), cu126, cu124, rocm6.3 (AMD), xpu (Intel). "
350
+ "Default: auto"
351
+ ),
352
+ )
353
+ create_parser.add_argument("--use", action="store_true", help="Set active environment after creation")
354
+ create_parser.add_argument("-y", "--yes", action="store_true", help="Skip confirmation prompts, use defaults for workspace initialization")
355
+ create_parser.set_defaults(func=env_cmds.create)
356
+
357
+ # use - Set active environment
358
+ use_parser = subparsers.add_parser("use", help="Set active environment")
359
+ use_parser.add_argument("name", help="Environment name").completer = environment_completer # type: ignore[attr-defined]
360
+ use_parser.set_defaults(func=env_cmds.use)
361
+
362
+ # delete - Delete environment
363
+ delete_parser = subparsers.add_parser("delete", help="Delete environment")
364
+ delete_parser.add_argument("name", help="Environment name").completer = environment_completer # type: ignore[attr-defined]
365
+ delete_parser.add_argument("-y", "--yes", action="store_true", help="Skip confirmation")
366
+ delete_parser.set_defaults(func=env_cmds.delete)
367
+
368
+ # Environment Operation Commands (operate IN environments, require -e or active)
369
+
370
+ # run - Run ComfyUI (special handling for ComfyUI args)
371
+ run_parser = subparsers.add_parser("run", help="Run ComfyUI")
372
+ run_parser.add_argument("--no-sync", action="store_true", help="Skip environment sync before running")
373
+ run_parser.set_defaults(func=env_cmds.run, args=[])
374
+
375
+ # status - Show environment status
376
+ status_parser = subparsers.add_parser("status", help="Show status (both sync and git status)")
377
+ status_parser.add_argument("-v", "--verbose", action="store_true", help="Show full details")
378
+ status_parser.set_defaults(func=env_cmds.status)
379
+
380
+ # manifest - Show environment manifest
381
+ manifest_parser = subparsers.add_parser("manifest", help="Show environment manifest (pyproject.toml)")
382
+ manifest_parser.add_argument("--pretty", action="store_true", help="Output as YAML instead of TOML")
383
+ manifest_parser.add_argument("--section", type=str, help="Show specific section (e.g., tool.comfygit.nodes)")
384
+ manifest_parser.add_argument("--ide", nargs="?", const="auto", metavar="CMD", help="Open in editor (uses $EDITOR if no command given)")
385
+ manifest_parser.set_defaults(func=env_cmds.manifest)
386
+
387
+ # repair - Repair environment drift (manual edits or git operations)
388
+ repair_parser = subparsers.add_parser("repair", help="Repair environment to match pyproject.toml")
389
+ repair_parser.add_argument("-y", "--yes", action="store_true", help="Skip confirmation")
390
+ repair_parser.add_argument(
391
+ "--models",
392
+ choices=["all", "required", "skip"],
393
+ default="all",
394
+ help="Model download strategy: all (default), required only, or skip"
395
+ )
396
+ repair_parser.set_defaults(func=env_cmds.repair)
397
+
398
+ # log - Show commit history
399
+ log_parser = subparsers.add_parser("log", help="Show commit history")
400
+ log_parser.add_argument("-n", "--limit", type=int, default=20, metavar="N", help="Number of commits to show (default: 20)")
401
+ log_parser.add_argument("-v", "--verbose", action="store_true", help="Show full details")
402
+ log_parser.set_defaults(func=env_cmds.log)
403
+
404
+ # commit - Save environment changes
405
+ commit_parser = subparsers.add_parser("commit", help="Commit environment changes")
406
+ commit_parser.add_argument("-m", "--message", help="Commit message (auto-generated if not provided)")
407
+ commit_parser.add_argument("--auto", action="store_true", help="Auto-resolve issues without interaction")
408
+ commit_parser.add_argument("--allow-issues", action="store_true", help="Allow committing workflows with unresolved issues")
409
+ commit_parser.add_argument("-y", "--yes", action="store_true", help="Skip detached HEAD warning (allow commit anyway)")
410
+ commit_parser.set_defaults(func=env_cmds.commit)
411
+
412
+ # checkout - Move HEAD without committing
413
+ checkout_parser = subparsers.add_parser("checkout", help="Checkout commits, branches, or files")
414
+ checkout_parser.add_argument("ref", nargs="?", help="Commit, branch, or tag to checkout (defaults to HEAD when using -b)").completer = ref_completer # type: ignore[attr-defined]
415
+ checkout_parser.add_argument("-b", "--branch", help="Create new branch and switch to it")
416
+ checkout_parser.add_argument("-y", "--yes", action="store_true", help="Skip confirmation for uncommitted changes")
417
+ checkout_parser.add_argument("--force", action="store_true", help="Force checkout, discarding uncommitted changes")
418
+ checkout_parser.set_defaults(func=env_cmds.checkout)
419
+
420
+ # branch - Manage branches
421
+ branch_parser = subparsers.add_parser("branch", help="List, create, or delete branches")
422
+ branch_parser.add_argument("name", nargs="?", help="Branch name (list all if omitted)").completer = branch_completer # type: ignore[attr-defined]
423
+ branch_parser.add_argument("-d", "--delete", action="store_true", help="Delete branch")
424
+ branch_parser.add_argument("-D", "--force-delete", action="store_true", help="Force delete branch (even if unmerged)")
425
+ branch_parser.set_defaults(func=env_cmds.branch)
426
+
427
+ # switch - Switch branches
428
+ switch_parser = subparsers.add_parser("switch", help="Switch to a branch")
429
+ switch_parser.add_argument("branch", help="Branch name to switch to").completer = branch_completer # type: ignore[attr-defined]
430
+ switch_parser.add_argument("-c", "--create", action="store_true", help="Create branch if it doesn't exist")
431
+ switch_parser.set_defaults(func=env_cmds.switch)
432
+
433
+ # reset - Reset current HEAD to ref
434
+ reset_parser = subparsers.add_parser("reset", help="Reset current HEAD to specified state")
435
+ reset_parser.add_argument("ref", nargs="?", default="HEAD", help="Commit to reset to (default: HEAD)").completer = commit_hash_completer # type: ignore[attr-defined]
436
+ reset_parser.add_argument("--hard", action="store_true", help="Discard all changes (hard reset)")
437
+ reset_parser.add_argument("--mixed", action="store_true", help="Keep changes in working tree, unstage (default)")
438
+ reset_parser.add_argument("--soft", action="store_true", help="Keep changes staged")
439
+ reset_parser.add_argument("-y", "--yes", action="store_true", help="Skip confirmation")
440
+ reset_parser.set_defaults(func=env_cmds.reset_git)
441
+
442
+ # merge - Merge branches
443
+ merge_parser = subparsers.add_parser("merge", help="Merge branch into current")
444
+ merge_parser.add_argument("branch", help="Branch to merge")
445
+ merge_parser.add_argument("-m", "--message", help="Merge commit message")
446
+ merge_parser.add_argument(
447
+ "--preview",
448
+ action="store_true",
449
+ help="Preview changes without applying (read-only diff with conflict detection)"
450
+ )
451
+ merge_parser.add_argument(
452
+ "--auto-resolve",
453
+ choices=["mine", "theirs"],
454
+ help="Auto-resolve conflicts: 'mine' keeps local, 'theirs' takes incoming"
455
+ )
456
+ merge_parser.set_defaults(func=env_cmds.merge)
457
+
458
+ # revert - Revert commits
459
+ revert_parser = subparsers.add_parser("revert", help="Create new commit that undoes previous commit")
460
+ revert_parser.add_argument("commit", help="Commit to revert")
461
+ revert_parser.set_defaults(func=env_cmds.revert)
462
+
463
+ # pull - Pull from remote and sync
464
+ pull_parser = subparsers.add_parser(
465
+ "pull",
466
+ help="Pull changes from remote and repair environment"
467
+ )
468
+ pull_parser.add_argument(
469
+ "-r", "--remote",
470
+ default="origin",
471
+ help="Git remote name (default: origin)"
472
+ )
473
+ pull_parser.add_argument(
474
+ "--models",
475
+ choices=["all", "required", "skip"],
476
+ default="all",
477
+ help="Model download strategy (default: all)"
478
+ )
479
+ pull_parser.add_argument(
480
+ "--force",
481
+ action="store_true",
482
+ help="Discard uncommitted changes and force pull"
483
+ )
484
+ pull_parser.add_argument(
485
+ "--preview",
486
+ action="store_true",
487
+ help="Preview changes without applying (read-only fetch and diff)"
488
+ )
489
+ pull_parser.add_argument(
490
+ "--auto-resolve",
491
+ choices=["mine", "theirs"],
492
+ help="Auto-resolve conflicts: 'mine' keeps local, 'theirs' takes incoming"
493
+ )
494
+ pull_parser.set_defaults(func=env_cmds.pull)
495
+
496
+ # push - Push commits to remote
497
+ push_parser = subparsers.add_parser(
498
+ "push",
499
+ help="Push committed changes to remote"
500
+ )
501
+ push_parser.add_argument(
502
+ "-r", "--remote",
503
+ default="origin",
504
+ help="Git remote name (default: origin)"
505
+ )
506
+ push_parser.add_argument(
507
+ "--force",
508
+ action="store_true",
509
+ help="Force push using --force-with-lease (overwrite remote)"
510
+ )
511
+ push_parser.set_defaults(func=env_cmds.push)
512
+
513
+ # remote - Manage git remotes
514
+ remote_parser = subparsers.add_parser(
515
+ "remote",
516
+ help="Manage git remotes"
517
+ )
518
+ remote_subparsers = remote_parser.add_subparsers(
519
+ dest="remote_command",
520
+ required=True
521
+ )
522
+
523
+ # remote add
524
+ remote_add_parser = remote_subparsers.add_parser(
525
+ "add",
526
+ help="Add a git remote"
527
+ )
528
+ remote_add_parser.add_argument(
529
+ "name",
530
+ help="Remote name (e.g., origin)"
531
+ )
532
+ remote_add_parser.add_argument(
533
+ "url",
534
+ help="Remote URL"
535
+ )
536
+
537
+ # remote remove
538
+ remote_remove_parser = remote_subparsers.add_parser(
539
+ "remove",
540
+ help="Remove a git remote"
541
+ )
542
+ remote_remove_parser.add_argument(
543
+ "name",
544
+ help="Remote name to remove"
545
+ )
546
+
547
+ # remote list
548
+ remote_list_parser = remote_subparsers.add_parser(
549
+ "list",
550
+ help="List all git remotes"
551
+ )
552
+
553
+ remote_parser.set_defaults(func=env_cmds.remote)
554
+
555
+ # Node management subcommands
556
+ node_parser = subparsers.add_parser("node", help="Manage custom nodes")
557
+ node_subparsers = node_parser.add_subparsers(dest="node_command", help="Node commands")
558
+
559
+ # node add
560
+ node_add_parser = node_subparsers.add_parser("add", help="Add custom node(s)")
561
+ node_add_parser.add_argument("node_names", nargs="+", help="Node identifier(s): registry-id[@version], github-url[@ref], or directory name")
562
+ node_add_parser.add_argument("--dev", action="store_true", help="Track existing local development node")
563
+ node_add_parser.add_argument("--no-test", action="store_true", help="Don't test resolution")
564
+ node_add_parser.add_argument("--force", action="store_true", help="Force overwrite existing directory")
565
+ node_add_parser.add_argument("--verbose", "-v", action="store_true", help="Show full UV error output for dependency conflicts")
566
+ node_add_parser.set_defaults(func=env_cmds.node_add)
567
+
568
+ # node remove
569
+ node_remove_parser = node_subparsers.add_parser("remove", help="Remove custom node(s)")
570
+ node_remove_parser.add_argument("node_names", nargs="+", help="Node registry ID(s) or name(s)").completer = installed_node_completer # type: ignore[attr-defined]
571
+ node_remove_parser.add_argument("--dev", action="store_true", help="Remove development node specifically")
572
+ node_remove_parser.add_argument("--untrack", action="store_true", help="Only remove from tracking, leave filesystem unchanged")
573
+ node_remove_parser.set_defaults(func=env_cmds.node_remove)
574
+
575
+ # node prune
576
+ node_prune_parser = node_subparsers.add_parser("prune", help="Remove unused custom nodes")
577
+ node_prune_parser.add_argument("--exclude", nargs="+", metavar="PACKAGE", help="Package IDs to keep even if unused")
578
+ node_prune_parser.add_argument("-y", "--yes", action="store_true", help="Skip confirmation prompt")
579
+ node_prune_parser.set_defaults(func=env_cmds.node_prune)
580
+
581
+ # node list
582
+ node_list_parser = node_subparsers.add_parser("list", help="List custom nodes")
583
+ node_list_parser.set_defaults(func=env_cmds.node_list)
584
+
585
+ # node update
586
+ node_update_parser = node_subparsers.add_parser("update", help="Update custom node")
587
+ node_update_parser.add_argument("node_name", help="Node identifier or name to update").completer = installed_node_completer # type: ignore[attr-defined]
588
+ node_update_parser.add_argument("-y", "--yes", action="store_true", help="Auto-confirm updates (skip prompts)")
589
+ node_update_parser.add_argument("--no-test", action="store_true", help="Don't test resolution")
590
+ node_update_parser.set_defaults(func=env_cmds.node_update)
591
+
592
+ # Workflow management subcommands
593
+ workflow_parser = subparsers.add_parser("workflow", help="Manage workflows")
594
+ workflow_subparsers = workflow_parser.add_subparsers(dest="workflow_command", help="Workflow commands")
595
+
596
+ # workflow list
597
+ workflow_list_parser = workflow_subparsers.add_parser("list", help="List all workflows with sync status")
598
+ workflow_list_parser.set_defaults(func=env_cmds.workflow_list)
599
+
600
+ # workflow resolve
601
+ workflow_resolve_parser = workflow_subparsers.add_parser("resolve", help="Resolve workflow dependencies (nodes & models)")
602
+ workflow_resolve_parser.add_argument("name", help="Workflow name to resolve").completer = workflow_completer # type: ignore[attr-defined]
603
+ workflow_resolve_parser.add_argument("--auto", action="store_true", help="Auto-resolve without interaction")
604
+ workflow_resolve_parser.add_argument("--install", action="store_true", help="Auto-install missing nodes without prompting")
605
+ workflow_resolve_parser.add_argument("--no-install", action="store_true", help="Skip node installation prompt")
606
+ workflow_resolve_parser.set_defaults(func=env_cmds.workflow_resolve)
607
+
608
+ # workflow model importance
609
+ workflow_importance_parser = workflow_subparsers.add_parser(
610
+ "model",
611
+ help="Manage workflow models"
612
+ )
613
+ workflow_model_subparsers = workflow_importance_parser.add_subparsers(
614
+ dest="model_command",
615
+ help="Model management commands"
616
+ )
617
+
618
+ importance_parser = workflow_model_subparsers.add_parser(
619
+ "importance",
620
+ help="Set model importance (required/flexible/optional)"
621
+ )
622
+ importance_parser.add_argument(
623
+ "workflow_name",
624
+ nargs="?",
625
+ help="Workflow name (interactive if omitted)"
626
+ ).completer = workflow_completer # type: ignore[attr-defined]
627
+ importance_parser.add_argument(
628
+ "model_identifier",
629
+ nargs="?",
630
+ help="Model filename or hash (interactive if omitted)"
631
+ )
632
+ importance_parser.add_argument(
633
+ "importance",
634
+ nargs="?",
635
+ choices=["required", "flexible", "optional"],
636
+ help="Importance level"
637
+ )
638
+ importance_parser.set_defaults(func=env_cmds.workflow_model_importance)
639
+
640
+ # Constraint management subcommands
641
+ constraint_parser = subparsers.add_parser("constraint", help="Manage UV constraint dependencies")
642
+ constraint_subparsers = constraint_parser.add_subparsers(dest="constraint_command", help="Constraint commands")
643
+
644
+ # constraint add
645
+ constraint_add_parser = constraint_subparsers.add_parser("add", help="Add constraint dependencies")
646
+ constraint_add_parser.add_argument("packages", nargs="+", help="Package specifications (e.g., torch==2.4.1)")
647
+ constraint_add_parser.set_defaults(func=env_cmds.constraint_add)
648
+
649
+ # constraint list
650
+ constraint_list_parser = constraint_subparsers.add_parser("list", help="List constraint dependencies")
651
+ constraint_list_parser.set_defaults(func=env_cmds.constraint_list)
652
+
653
+ # constraint remove
654
+ constraint_remove_parser = constraint_subparsers.add_parser("remove", help="Remove constraint dependencies")
655
+ constraint_remove_parser.add_argument("packages", nargs="+", help="Package names to remove")
656
+ constraint_remove_parser.set_defaults(func=env_cmds.constraint_remove)
657
+
658
+ # Python dependency management subcommands
659
+ py_parser = subparsers.add_parser("py", help="Manage Python dependencies")
660
+ py_subparsers = py_parser.add_subparsers(dest="py_command", help="Python dependency commands")
661
+
662
+ # py add
663
+ py_add_parser = py_subparsers.add_parser("add", help="Add Python dependencies")
664
+ py_add_parser.add_argument("packages", nargs="*", help="Package specifications (e.g., requests>=2.0.0)")
665
+ py_add_parser.add_argument("-r", "--requirements", type=Path, help="Add packages from requirements.txt file")
666
+ py_add_parser.add_argument("--upgrade", action="store_true", help="Upgrade existing packages")
667
+ # Tier 2: Power-user flags
668
+ py_add_parser.add_argument("--group", help="Add to dependency group (e.g., optional-cuda)")
669
+ py_add_parser.add_argument("--dev", action="store_true", help="Add to dev dependencies")
670
+ py_add_parser.add_argument("--editable", action="store_true", help="Install as editable (for local development)")
671
+ py_add_parser.add_argument("--bounds", choices=["lower", "major", "minor", "exact"], help="Version specifier style")
672
+ py_add_parser.set_defaults(func=env_cmds.py_add)
673
+
674
+ # py remove
675
+ py_remove_parser = py_subparsers.add_parser("remove", help="Remove Python dependencies")
676
+ py_remove_parser.add_argument("packages", nargs="+", help="Package names to remove")
677
+ py_remove_parser.add_argument("--group", help="Remove packages from dependency group instead of main dependencies")
678
+ py_remove_parser.set_defaults(func=env_cmds.py_remove)
679
+
680
+ # py remove-group
681
+ py_remove_group_parser = py_subparsers.add_parser("remove-group", help="Remove entire dependency group")
682
+ py_remove_group_parser.add_argument("group", help="Dependency group name to remove")
683
+ py_remove_group_parser.set_defaults(func=env_cmds.py_remove_group)
684
+
685
+ # py list
686
+ py_list_parser = py_subparsers.add_parser("list", help="List project dependencies")
687
+ py_list_parser.add_argument("--all", action="store_true", help="Show all dependencies including dependency groups")
688
+ py_list_parser.set_defaults(func=env_cmds.py_list)
689
+
690
+ # py uv - Direct UV passthrough for advanced users
691
+ py_uv_parser = py_subparsers.add_parser(
692
+ "uv",
693
+ help="Direct UV passthrough (advanced)",
694
+ add_help=False # Don't interfere with UV's --help
695
+ )
696
+ py_uv_parser.add_argument(
697
+ "uv_args",
698
+ nargs=argparse.REMAINDER, # Capture everything after 'uv'
699
+ help="UV command and arguments (e.g., 'add --group optional-cuda sageattention')"
700
+ )
701
+ py_uv_parser.set_defaults(func=env_cmds.py_uv)
702
+
703
+ if __name__ == "__main__":
704
+ main()