comfygit-deploy 0.3.4__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,3 @@
1
+ """ComfyGit Deploy - Remote deployment and worker management CLI."""
2
+
3
+ __version__ = "0.3.3"
comfygit_deploy/cli.py ADDED
@@ -0,0 +1,374 @@
1
+ """CLI entry point for comfygit-deploy.
2
+
3
+ Provides command-line interface for deploying ComfyUI environments
4
+ to cloud providers (RunPod) and self-hosted workers.
5
+ """
6
+
7
+ import argparse
8
+ import sys
9
+
10
+ from . import __version__
11
+
12
+
13
+ def create_parser() -> argparse.ArgumentParser:
14
+ """Create the argument parser with all subcommands."""
15
+ parser = argparse.ArgumentParser(
16
+ prog="cg-deploy",
17
+ description="ComfyGit Deploy - Remote deployment and worker management",
18
+ )
19
+ parser.add_argument(
20
+ "--version",
21
+ action="version",
22
+ version=f"%(prog)s {__version__}",
23
+ )
24
+
25
+ subparsers = parser.add_subparsers(dest="command", help="Available commands")
26
+
27
+ # =========================================================================
28
+ # RunPod commands
29
+ # =========================================================================
30
+ runpod_parser = subparsers.add_parser("runpod", help="RunPod operations")
31
+ runpod_subparsers = runpod_parser.add_subparsers(
32
+ dest="runpod_command", help="RunPod subcommands"
33
+ )
34
+
35
+ # runpod config
36
+ config_parser = runpod_subparsers.add_parser("config", help="Configure RunPod API key")
37
+ config_parser.add_argument("--api-key", help="Set RunPod API key")
38
+ config_parser.add_argument("--show", action="store_true", help="Show current config")
39
+ config_parser.add_argument("--clear", action="store_true", help="Clear stored API key")
40
+
41
+ # runpod gpus
42
+ gpus_parser = runpod_subparsers.add_parser("gpus", help="List available GPUs with pricing")
43
+ gpus_parser.add_argument("--region", help="Filter by region/data center")
44
+
45
+ # runpod volumes
46
+ runpod_subparsers.add_parser("volumes", help="List network volumes")
47
+
48
+ # runpod regions
49
+ runpod_subparsers.add_parser("regions", help="List data centers")
50
+
51
+ # runpod deploy
52
+ deploy_parser = runpod_subparsers.add_parser("deploy", help="Deploy environment to RunPod")
53
+ deploy_parser.add_argument("import_source", help="Git URL or local path to import")
54
+ deploy_parser.add_argument("--gpu", required=True, help="GPU type (e.g., 'RTX 4090')")
55
+ deploy_parser.add_argument("--volume", help="Network volume ID to attach")
56
+ deploy_parser.add_argument("--name", help="Deployment name")
57
+ deploy_parser.add_argument("--branch", "-b", help="Git branch/tag to use")
58
+ deploy_parser.add_argument(
59
+ "--cloud-type",
60
+ choices=["SECURE", "COMMUNITY"],
61
+ default="SECURE",
62
+ help="Cloud type (default: SECURE)",
63
+ )
64
+ deploy_parser.add_argument(
65
+ "--pricing-type",
66
+ choices=["ON_DEMAND", "SPOT"],
67
+ default="ON_DEMAND",
68
+ help="Pricing type (default: ON_DEMAND)",
69
+ )
70
+ deploy_parser.add_argument("--spot-bid", type=float, help="Spot bid price (for SPOT pricing)")
71
+
72
+ # =========================================================================
73
+ # Instance commands
74
+ # =========================================================================
75
+ instances_parser = subparsers.add_parser("instances", help="List all instances")
76
+ instances_parser.add_argument(
77
+ "--provider",
78
+ choices=["runpod", "custom"],
79
+ help="Filter by provider",
80
+ )
81
+ instances_parser.add_argument(
82
+ "--status",
83
+ choices=["running", "stopped"],
84
+ help="Filter by status",
85
+ )
86
+ instances_parser.add_argument("--json", action="store_true", help="Output as JSON")
87
+
88
+ # start
89
+ start_parser = subparsers.add_parser("start", help="Start a stopped instance")
90
+ start_parser.add_argument("instance_id", help="Instance ID to start")
91
+
92
+ # stop
93
+ stop_parser = subparsers.add_parser("stop", help="Stop a running instance")
94
+ stop_parser.add_argument("instance_id", help="Instance ID to stop")
95
+
96
+ # terminate
97
+ terminate_parser = subparsers.add_parser("terminate", help="Terminate an instance")
98
+ terminate_parser.add_argument("instance_id", help="Instance ID to terminate")
99
+ terminate_parser.add_argument("--force", action="store_true", help="Force termination")
100
+ terminate_parser.add_argument("--keep-env", action="store_true", help="Keep environment directory")
101
+
102
+ # logs
103
+ logs_parser = subparsers.add_parser("logs", help="View instance logs")
104
+ logs_parser.add_argument("instance_id", help="Instance ID")
105
+ logs_parser.add_argument("--follow", "-f", action="store_true", help="Follow log output")
106
+ logs_parser.add_argument("--lines", "-n", type=int, default=100, help="Number of lines")
107
+
108
+ # open
109
+ open_parser = subparsers.add_parser("open", help="Open ComfyUI URL in browser")
110
+ open_parser.add_argument("instance_id", help="Instance ID")
111
+
112
+ # wait
113
+ wait_parser = subparsers.add_parser("wait", help="Wait for instance to be ready")
114
+ wait_parser.add_argument("instance_id", help="Instance ID")
115
+ wait_parser.add_argument("--timeout", type=int, default=300, help="Timeout in seconds")
116
+
117
+ # =========================================================================
118
+ # Custom worker commands (Phase 2)
119
+ # =========================================================================
120
+ custom_parser = subparsers.add_parser("custom", help="Custom worker operations")
121
+ custom_subparsers = custom_parser.add_subparsers(
122
+ dest="custom_command", help="Custom worker subcommands"
123
+ )
124
+
125
+ # custom scan
126
+ scan_parser = custom_subparsers.add_parser("scan", help="Scan for workers via mDNS")
127
+ scan_parser.add_argument("--timeout", type=int, default=5, help="Scan timeout")
128
+
129
+ # custom add
130
+ add_parser = custom_subparsers.add_parser("add", help="Add a custom worker")
131
+ add_parser.add_argument("name", help="Worker name")
132
+ add_parser.add_argument("--host", help="Worker host/IP")
133
+ add_parser.add_argument("--port", type=int, default=9090, help="Worker port")
134
+ add_parser.add_argument("--api-key", help="Worker API key")
135
+ add_parser.add_argument(
136
+ "--discovered", action="store_true", help="Add from last scan results"
137
+ )
138
+
139
+ # custom remove
140
+ remove_parser = custom_subparsers.add_parser("remove", help="Remove a custom worker")
141
+ remove_parser.add_argument("name", help="Worker name")
142
+
143
+ # custom list
144
+ custom_subparsers.add_parser("list", help="List registered workers")
145
+
146
+ # custom test
147
+ test_parser = custom_subparsers.add_parser("test", help="Test worker connection")
148
+ test_parser.add_argument("name", help="Worker name")
149
+
150
+ # custom deploy
151
+ custom_deploy_parser = custom_subparsers.add_parser(
152
+ "deploy", help="Deploy to custom worker"
153
+ )
154
+ custom_deploy_parser.add_argument("worker_name", help="Worker to deploy to")
155
+ custom_deploy_parser.add_argument("import_source", help="Git URL or local path")
156
+ custom_deploy_parser.add_argument("--branch", "-b", help="Git branch/tag")
157
+ custom_deploy_parser.add_argument(
158
+ "--mode",
159
+ choices=["docker", "native"],
160
+ default="docker",
161
+ help="Deployment mode",
162
+ )
163
+ custom_deploy_parser.add_argument("--name", help="Environment name")
164
+
165
+ # =========================================================================
166
+ # Worker commands (runs on GPU machine, Phase 2)
167
+ # =========================================================================
168
+ worker_parser = subparsers.add_parser("worker", help="Worker server management")
169
+ worker_subparsers = worker_parser.add_subparsers(
170
+ dest="worker_command", help="Worker subcommands"
171
+ )
172
+
173
+ # worker setup
174
+ setup_parser = worker_subparsers.add_parser("setup", help="One-time worker setup")
175
+ setup_parser.add_argument("--api-key", help="Set worker API key")
176
+ setup_parser.add_argument("--workspace", help="Workspace path")
177
+
178
+ # worker up
179
+ up_parser = worker_subparsers.add_parser("up", help="Start worker server")
180
+ up_parser.add_argument("--port", type=int, default=9090, help="Server port")
181
+ up_parser.add_argument("--host", default="0.0.0.0", help="Bind host")
182
+ up_parser.add_argument(
183
+ "--mode",
184
+ choices=["docker", "native"],
185
+ default="docker",
186
+ help="Instance mode",
187
+ )
188
+ up_parser.add_argument("--broadcast", action="store_true", help="Enable mDNS broadcast")
189
+ up_parser.add_argument("--port-range", default="8200:8210", help="Instance port range")
190
+ up_parser.add_argument("--dev", action="store_true", help="Use saved dev config (from 'dev setup')")
191
+ up_parser.add_argument("--dev-core", metavar="PATH", help="Use local comfygit-core (editable)")
192
+ up_parser.add_argument("--dev-manager", metavar="PATH", help="Use local comfygit-manager")
193
+
194
+ # worker down
195
+ worker_subparsers.add_parser("down", help="Stop worker server")
196
+
197
+ # worker status
198
+ worker_subparsers.add_parser("status", help="Show worker status")
199
+
200
+ # worker regenerate-key
201
+ worker_subparsers.add_parser("regenerate-key", help="Regenerate API key")
202
+
203
+ # =========================================================================
204
+ # Dev commands (development mode setup)
205
+ # =========================================================================
206
+ dev_parser = subparsers.add_parser("dev", help="Development mode setup")
207
+ dev_subparsers = dev_parser.add_subparsers(dest="dev_command", help="Dev subcommands")
208
+
209
+ # dev setup
210
+ dev_setup_parser = dev_subparsers.add_parser(
211
+ "setup", help="Configure local dev paths for core/manager"
212
+ )
213
+ dev_setup_parser.add_argument("--core", metavar="PATH", help="Path to comfygit-core package")
214
+ dev_setup_parser.add_argument("--manager", metavar="PATH", help="Path to comfygit-manager")
215
+ dev_setup_parser.add_argument("--show", action="store_true", help="Show current dev config")
216
+ dev_setup_parser.add_argument("--clear", action="store_true", help="Clear dev config")
217
+
218
+ # dev patch
219
+ dev_patch_parser = dev_subparsers.add_parser(
220
+ "patch", help="Patch existing environments with dev config"
221
+ )
222
+ dev_patch_parser.add_argument("--env", help="Specific environment to patch (default: all)")
223
+
224
+ # dev add-node
225
+ dev_add_node_parser = dev_subparsers.add_parser(
226
+ "add-node", help="Add a dev node to be symlinked into environments"
227
+ )
228
+ dev_add_node_parser.add_argument("name", help="Node directory name (e.g., ComfyUI-Async-API)")
229
+ dev_add_node_parser.add_argument("path", help="Path to the node source directory")
230
+
231
+ # dev remove-node
232
+ dev_remove_node_parser = dev_subparsers.add_parser(
233
+ "remove-node", help="Remove a dev node from config"
234
+ )
235
+ dev_remove_node_parser.add_argument("name", help="Node name to remove")
236
+
237
+ # dev list-nodes
238
+ dev_subparsers.add_parser("list-nodes", help="List configured dev nodes")
239
+
240
+ return parser
241
+
242
+
243
+ def main(args: list[str] | None = None) -> int:
244
+ """Main entry point for cg-deploy CLI.
245
+
246
+ Args:
247
+ args: Command-line arguments (uses sys.argv if None)
248
+
249
+ Returns:
250
+ Exit code (0 for success, non-zero for errors)
251
+ """
252
+ parser = create_parser()
253
+ parsed = parser.parse_args(args)
254
+
255
+ if not parsed.command:
256
+ parser.print_help()
257
+ return 0
258
+
259
+ # Import command handlers
260
+ from .commands import instances as instance_commands
261
+ from .commands import runpod as runpod_commands
262
+
263
+ # Dispatch to command handlers
264
+ try:
265
+ if parsed.command == "runpod":
266
+ if not parsed.runpod_command:
267
+ parser.parse_args(["runpod", "--help"])
268
+ return 0
269
+ handler_map = {
270
+ "config": runpod_commands.handle_config,
271
+ "gpus": runpod_commands.handle_gpus,
272
+ "volumes": runpod_commands.handle_volumes,
273
+ "regions": runpod_commands.handle_regions,
274
+ "deploy": runpod_commands.handle_deploy,
275
+ }
276
+ handler = handler_map.get(parsed.runpod_command)
277
+ if handler:
278
+ return handler(parsed)
279
+ print(f"Unknown runpod command: {parsed.runpod_command}")
280
+ return 1
281
+
282
+ elif parsed.command == "instances":
283
+ return instance_commands.handle_instances(parsed)
284
+
285
+ elif parsed.command == "start":
286
+ return instance_commands.handle_start(parsed)
287
+
288
+ elif parsed.command == "stop":
289
+ return instance_commands.handle_stop(parsed)
290
+
291
+ elif parsed.command == "terminate":
292
+ return instance_commands.handle_terminate(parsed)
293
+
294
+ elif parsed.command == "open":
295
+ return instance_commands.handle_open(parsed)
296
+
297
+ elif parsed.command == "wait":
298
+ return instance_commands.handle_wait(parsed)
299
+
300
+ elif parsed.command == "logs":
301
+ return instance_commands.handle_logs(parsed)
302
+
303
+ elif parsed.command == "worker":
304
+ from .commands import worker as worker_commands
305
+
306
+ if not parsed.worker_command:
307
+ parser.parse_args(["worker", "--help"])
308
+ return 0
309
+ handler_map = {
310
+ "setup": worker_commands.handle_setup,
311
+ "up": worker_commands.handle_up,
312
+ "down": worker_commands.handle_down,
313
+ "status": worker_commands.handle_status,
314
+ "regenerate-key": worker_commands.handle_regenerate_key,
315
+ }
316
+ handler = handler_map.get(parsed.worker_command)
317
+ if handler:
318
+ return handler(parsed)
319
+ print(f"Unknown worker command: {parsed.worker_command}")
320
+ return 1
321
+
322
+ elif parsed.command == "custom":
323
+ from .commands import custom as custom_commands
324
+
325
+ if not parsed.custom_command:
326
+ parser.parse_args(["custom", "--help"])
327
+ return 0
328
+ handler_map = {
329
+ "scan": custom_commands.handle_scan,
330
+ "add": custom_commands.handle_add,
331
+ "remove": custom_commands.handle_remove,
332
+ "list": custom_commands.handle_list,
333
+ "test": custom_commands.handle_test,
334
+ "deploy": custom_commands.handle_deploy,
335
+ }
336
+ handler = handler_map.get(parsed.custom_command)
337
+ if handler:
338
+ return handler(parsed)
339
+ print(f"Unknown custom command: {parsed.custom_command}")
340
+ return 1
341
+
342
+ elif parsed.command == "dev":
343
+ from .commands import dev as dev_commands
344
+
345
+ if not parsed.dev_command:
346
+ parser.parse_args(["dev", "--help"])
347
+ return 0
348
+ handler_map = {
349
+ "setup": dev_commands.handle_setup,
350
+ "patch": dev_commands.handle_patch,
351
+ "add-node": dev_commands.handle_add_node,
352
+ "remove-node": dev_commands.handle_remove_node,
353
+ "list-nodes": dev_commands.handle_list_nodes,
354
+ }
355
+ handler = handler_map.get(parsed.dev_command)
356
+ if handler:
357
+ return handler(parsed)
358
+ print(f"Unknown dev command: {parsed.dev_command}")
359
+ return 1
360
+
361
+ else:
362
+ print(f"Unknown command: {parsed.command}")
363
+ return 1
364
+
365
+ except KeyboardInterrupt:
366
+ print("\nCancelled.")
367
+ return 130
368
+ except Exception as e:
369
+ print(f"Error: {e}")
370
+ return 1
371
+
372
+
373
+ if __name__ == "__main__":
374
+ sys.exit(main())
@@ -0,0 +1,5 @@
1
+ """CLI command implementations."""
2
+
3
+ from . import custom, instances, runpod, worker
4
+
5
+ __all__ = ["custom", "instances", "runpod", "worker"]
@@ -0,0 +1,218 @@
1
+ """Custom worker CLI command handlers.
2
+
3
+ Commands for managing connections to self-hosted workers.
4
+ """
5
+
6
+ import argparse
7
+ import asyncio
8
+ import json
9
+ from dataclasses import asdict
10
+ from typing import Any
11
+
12
+ from ..config import DeployConfig
13
+ from ..providers.custom import CustomWorkerClient
14
+ from ..worker.mdns import MDNSScanner
15
+
16
+
17
+ def test_worker_connection(host: str, port: int, api_key: str) -> dict[str, Any]:
18
+ """Test connection to a worker (sync wrapper)."""
19
+ async def _test():
20
+ client = CustomWorkerClient(host=host, port=port, api_key=api_key)
21
+ return await client.test_connection()
22
+
23
+ return asyncio.run(_test())
24
+
25
+
26
+ def deploy_to_worker(
27
+ host: str,
28
+ port: int,
29
+ api_key: str,
30
+ import_source: str,
31
+ name: str | None = None,
32
+ branch: str | None = None,
33
+ mode: str | None = None,
34
+ ) -> dict[str, Any]:
35
+ """Deploy to a worker (sync wrapper)."""
36
+ async def _deploy():
37
+ client = CustomWorkerClient(host=host, port=port, api_key=api_key)
38
+ return await client.create_instance(
39
+ import_source=import_source,
40
+ name=name,
41
+ branch=branch,
42
+ mode=mode,
43
+ )
44
+
45
+ return asyncio.run(_deploy())
46
+
47
+
48
+ def handle_add(args: argparse.Namespace) -> int:
49
+ """Handle 'custom add' command."""
50
+ config = DeployConfig()
51
+ host = args.host
52
+ port = args.port
53
+ mode = "docker" # Default mode
54
+
55
+ if getattr(args, "discovered", False):
56
+ # Load from last scan results
57
+ discovered_file = config.path.parent / "discovered_workers.json"
58
+ if not discovered_file.exists():
59
+ print("Error: No scan results found. Run 'cg-deploy custom scan' first.")
60
+ return 1
61
+
62
+ discovered = json.loads(discovered_file.read_text())
63
+ worker = next((w for w in discovered if w["name"] == args.name), None)
64
+ if not worker:
65
+ print(f"Error: Worker '{args.name}' not found in scan results.")
66
+ return 1
67
+
68
+ host = worker["host"]
69
+ port = worker["port"]
70
+ mode = worker.get("mode", "docker")
71
+
72
+ if not host:
73
+ print("Error: --host is required (or use --discovered)")
74
+ return 1
75
+
76
+ if not args.api_key:
77
+ print("Error: --api-key is required")
78
+ return 1
79
+
80
+ config.add_worker(args.name, host, port, args.api_key, mode=mode)
81
+ config.save()
82
+
83
+ print(f"Added worker '{args.name}'")
84
+ print(f" Host: {host}:{port}")
85
+ print(f" Mode: {mode}")
86
+
87
+ return 0
88
+
89
+
90
+ def handle_remove(args: argparse.Namespace) -> int:
91
+ """Handle 'custom remove' command."""
92
+ config = DeployConfig()
93
+
94
+ if not config.remove_worker(args.name):
95
+ print(f"Error: Worker '{args.name}' not found")
96
+ return 1
97
+
98
+ config.save()
99
+ print(f"Removed worker '{args.name}'")
100
+ return 0
101
+
102
+
103
+ def handle_list(args: argparse.Namespace) -> int:
104
+ """Handle 'custom list' command."""
105
+ config = DeployConfig()
106
+ workers = config.workers
107
+
108
+ if not workers:
109
+ print("No workers registered.")
110
+ print("Use 'cg-deploy custom add' to register a worker.")
111
+ return 0
112
+
113
+ print("Registered workers:")
114
+ for name, info in workers.items():
115
+ host = info.get("host", "?")
116
+ port = info.get("port", "?")
117
+ mode = info.get("mode", "docker")
118
+ print(f" {name}: {host}:{port} ({mode})")
119
+
120
+ return 0
121
+
122
+
123
+ def handle_test(args: argparse.Namespace) -> int:
124
+ """Handle 'custom test' command."""
125
+ config = DeployConfig()
126
+ worker = config.get_worker(args.name)
127
+
128
+ if not worker:
129
+ print(f"Error: Worker '{args.name}' not found")
130
+ return 1
131
+
132
+ print(f"Testing connection to '{args.name}'...")
133
+
134
+ result = test_worker_connection(
135
+ host=worker["host"],
136
+ port=worker["port"],
137
+ api_key=worker["api_key"],
138
+ )
139
+
140
+ if result.get("success"):
141
+ print(" Status: OK")
142
+ print(f" Version: {result.get('worker_version', 'unknown')}")
143
+ return 0
144
+ else:
145
+ print(" Status: FAILED")
146
+ print(f" Error: {result.get('error', 'Unknown error')}")
147
+ return 1
148
+
149
+
150
+ def handle_deploy(args: argparse.Namespace) -> int:
151
+ """Handle 'custom deploy' command."""
152
+ config = DeployConfig()
153
+ worker = config.get_worker(args.worker_name)
154
+
155
+ if not worker:
156
+ print(f"Error: Worker '{args.worker_name}' not found")
157
+ return 1
158
+
159
+ print(f"Deploying to '{args.worker_name}'...")
160
+ print(f" Source: {args.import_source}")
161
+ if args.branch:
162
+ print(f" Branch: {args.branch}")
163
+ print(f" Mode: {args.mode}")
164
+
165
+ try:
166
+ result = deploy_to_worker(
167
+ host=worker["host"],
168
+ port=worker["port"],
169
+ api_key=worker["api_key"],
170
+ import_source=args.import_source,
171
+ name=args.name,
172
+ branch=args.branch,
173
+ mode=args.mode,
174
+ )
175
+
176
+ print()
177
+ print("Deployment started!")
178
+ print(f" Instance ID: {result.get('id')}")
179
+ print(f" Name: {result.get('name')}")
180
+ print(f" Port: {result.get('assigned_port')}")
181
+ print(f" Status: {result.get('status')}")
182
+
183
+ return 0
184
+
185
+ except Exception as e:
186
+ print(f"Error: {e}")
187
+ return 1
188
+
189
+
190
+ def handle_scan(args: argparse.Namespace) -> int:
191
+ """Handle 'custom scan' command (mDNS discovery)."""
192
+ timeout = getattr(args, "timeout", 5)
193
+ print(f"Scanning for workers (timeout: {timeout}s)...")
194
+
195
+ scanner = MDNSScanner(timeout=float(timeout))
196
+ workers = scanner.scan()
197
+
198
+ if not workers:
199
+ print("\nNo workers found on the network.")
200
+ print("Make sure workers are running with mDNS enabled.")
201
+ return 0
202
+
203
+ print(f"\nFound {len(workers)} worker(s):\n")
204
+ for w in workers:
205
+ print(f" {w.name}")
206
+ print(f" Host: {w.host}:{w.port}")
207
+ print(f" Mode: {w.mode}")
208
+ print(f" Version: {w.version}")
209
+ print()
210
+
211
+ # Save results for --discovered flag
212
+ config = DeployConfig()
213
+ discovered_file = config.path.parent / "discovered_workers.json"
214
+ discovered_file.parent.mkdir(parents=True, exist_ok=True)
215
+ discovered_file.write_text(json.dumps([asdict(w) for w in workers], indent=2))
216
+
217
+ print("Use 'cg-deploy custom add <name> --discovered --api-key <key>' to register.")
218
+ return 0