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,506 @@
1
+ """Instance management CLI command implementations.
2
+
3
+ Provides unified instance listing and management across RunPod and custom workers.
4
+ Instance IDs use namespacing: worker_name:instance_id for custom, plain id for RunPod.
5
+ """
6
+
7
+ import asyncio
8
+ import json
9
+ import time
10
+ import webbrowser
11
+ from argparse import Namespace
12
+
13
+ from ..config import DeployConfig
14
+ from ..providers.custom import CustomWorkerClient, CustomWorkerError
15
+ from ..providers.runpod import RunPodAPIError, RunPodClient
16
+
17
+
18
+ def parse_instance_id(instance_id: str) -> tuple[str | None, str]:
19
+ """Parse instance ID into (worker_name, local_id).
20
+
21
+ Format: "worker_name:local_id" for custom workers, plain id for RunPod.
22
+
23
+ Returns:
24
+ (worker_name, local_id) - worker_name is None for RunPod instances
25
+ """
26
+ if ":" in instance_id:
27
+ worker_name, local_id = instance_id.split(":", 1)
28
+ return worker_name, local_id
29
+ return None, instance_id
30
+
31
+
32
+ def _convert_runpod_to_unified(pod: dict) -> dict:
33
+ """Convert RunPod pod to unified instance format."""
34
+ status_map = {"RUNNING": "running", "EXITED": "stopped", "PENDING": "deploying"}
35
+ return {
36
+ "id": pod.get("id"),
37
+ "provider": "runpod",
38
+ "worker_name": None,
39
+ "name": pod.get("name"),
40
+ "status": status_map.get(pod.get("desiredStatus", ""), pod.get("desiredStatus")),
41
+ "gpu": pod.get("machine", {}).get("gpuDisplayName") if pod.get("machine") else None,
42
+ "cost_per_hour": pod.get("costPerHr", 0),
43
+ "comfyui_url": RunPodClient.get_comfyui_url(pod),
44
+ }
45
+
46
+
47
+ def _convert_worker_to_unified(worker_name: str, instance: dict) -> dict:
48
+ """Convert custom worker instance to unified format with namespaced ID."""
49
+ return {
50
+ "id": f"{worker_name}:{instance.get('id')}",
51
+ "provider": "custom",
52
+ "worker_name": worker_name,
53
+ "name": instance.get("name"),
54
+ "status": instance.get("status"),
55
+ "gpu": None, # Could be fetched from worker system info if needed
56
+ "cost_per_hour": 0, # Self-hosted
57
+ "comfyui_url": instance.get("comfyui_url"),
58
+ }
59
+
60
+
61
+ async def _fetch_all_instances(
62
+ config: DeployConfig, provider_filter: str | None
63
+ ) -> list[dict]:
64
+ """Fetch instances from all configured providers.
65
+
66
+ Args:
67
+ config: Deploy configuration
68
+ provider_filter: Optional filter ('runpod' or 'custom')
69
+
70
+ Returns:
71
+ List of unified instance dicts
72
+ """
73
+ instances = []
74
+
75
+ # Fetch RunPod instances
76
+ if provider_filter in (None, "runpod"):
77
+ api_key = config.runpod_api_key
78
+ if api_key:
79
+ try:
80
+ client = RunPodClient(api_key)
81
+ pods = await client.list_pods()
82
+ instances.extend(_convert_runpod_to_unified(pod) for pod in pods)
83
+ except RunPodAPIError as e:
84
+ print(f"Warning: RunPod error: {e}")
85
+
86
+ # Fetch custom worker instances
87
+ if provider_filter in (None, "custom"):
88
+ for worker_name, worker_config in config.workers.items():
89
+ try:
90
+ client = CustomWorkerClient(
91
+ worker_config["host"],
92
+ worker_config["port"],
93
+ worker_config["api_key"],
94
+ )
95
+ worker_instances = await client.list_instances()
96
+ instances.extend(
97
+ _convert_worker_to_unified(worker_name, inst)
98
+ for inst in worker_instances
99
+ )
100
+ except Exception:
101
+ # Worker offline or unreachable - skip silently
102
+ pass
103
+
104
+ return instances
105
+
106
+
107
+ def handle_instances(args: Namespace) -> int:
108
+ """Handle 'instances' command - list all instances from all providers."""
109
+ config = DeployConfig()
110
+ provider_filter = getattr(args, "provider", None)
111
+
112
+ # Check if we have any providers configured
113
+ has_runpod = config.runpod_api_key is not None
114
+ has_workers = len(config.workers) > 0
115
+
116
+ if not has_runpod and not has_workers:
117
+ print("No providers configured.")
118
+ print("Run: cg-deploy runpod config --api-key <key>")
119
+ print(" or: cg-deploy custom add <name> --host <ip> --api-key <key>")
120
+ return 1
121
+
122
+ # Require RunPod key only if specifically filtering for runpod
123
+ if provider_filter == "runpod" and not has_runpod:
124
+ print("Error: RunPod API key not configured.")
125
+ print("Run: cg-deploy runpod config --api-key <your-key>")
126
+ return 1
127
+
128
+ # Fetch all instances
129
+ instances = asyncio.run(_fetch_all_instances(config, provider_filter))
130
+
131
+ # Filter by status if requested
132
+ status_filter = getattr(args, "status", None)
133
+ if status_filter:
134
+ instances = [i for i in instances if i.get("status") == status_filter]
135
+
136
+ # JSON output
137
+ if getattr(args, "json", False):
138
+ print(json.dumps(instances, indent=2))
139
+ return 0
140
+
141
+ if not instances:
142
+ print("No instances found.")
143
+ return 0
144
+
145
+ # Table output
146
+ print(f"{'ID':<25} {'Name':<25} {'Provider':<10} {'Status':<10} {'$/hr':>8}")
147
+ print("-" * 80)
148
+
149
+ for inst in instances:
150
+ inst_id = inst.get("id", "?")[:25]
151
+ name = (inst.get("name") or "?")[:25]
152
+ provider = inst.get("provider", "?")
153
+ if inst.get("worker_name"):
154
+ provider = inst["worker_name"][:10]
155
+ status = inst.get("status", "?")
156
+ cost = inst.get("cost_per_hour", 0)
157
+
158
+ print(f"{inst_id:<25} {name:<25} {provider:<10} {status:<10} ${cost:>7.2f}")
159
+
160
+ # Show URL for running instances
161
+ if status == "running" and inst.get("comfyui_url"):
162
+ print(f" -> {inst['comfyui_url']}")
163
+
164
+ return 0
165
+
166
+
167
+ def _get_custom_client(config: DeployConfig, worker_name: str) -> CustomWorkerClient | None:
168
+ """Get CustomWorkerClient for a registered worker."""
169
+ worker = config.get_worker(worker_name)
170
+ if not worker:
171
+ print(f"Error: Worker '{worker_name}' not found.")
172
+ print("Run: cg-deploy custom list")
173
+ return None
174
+ return CustomWorkerClient(worker["host"], worker["port"], worker["api_key"])
175
+
176
+
177
+ def handle_start(args: Namespace) -> int:
178
+ """Handle 'start' command - start a stopped instance."""
179
+ config = DeployConfig()
180
+ worker_name, local_id = parse_instance_id(args.instance_id)
181
+
182
+ if worker_name:
183
+ # Custom worker instance
184
+ client = _get_custom_client(config, worker_name)
185
+ if not client:
186
+ return 1
187
+
188
+ print(f"Starting instance {local_id} on {worker_name}...")
189
+ try:
190
+ result = asyncio.run(client.start_instance(local_id))
191
+ print(f"Status: {result.get('status')}")
192
+ if result.get("comfyui_url"):
193
+ print(f"URL: {result['comfyui_url']}")
194
+ return 0
195
+ except CustomWorkerError as e:
196
+ print(f"Error: {e}")
197
+ return 1
198
+ else:
199
+ # RunPod instance
200
+ api_key = config.runpod_api_key
201
+ if not api_key:
202
+ print("Error: RunPod API key not configured.")
203
+ return 1
204
+
205
+ client = RunPodClient(api_key)
206
+ print(f"Starting instance {local_id}...")
207
+
208
+ try:
209
+ result = asyncio.run(client.start_pod(local_id))
210
+ print(f"Status: {result.get('desiredStatus')}")
211
+ if result.get("costPerHr"):
212
+ print(f"Cost: ${result['costPerHr']:.2f}/hr")
213
+ return 0
214
+ except RunPodAPIError as e:
215
+ print(f"Error: {e}")
216
+ return 1
217
+
218
+
219
+ def handle_stop(args: Namespace) -> int:
220
+ """Handle 'stop' command - stop a running instance."""
221
+ config = DeployConfig()
222
+ worker_name, local_id = parse_instance_id(args.instance_id)
223
+
224
+ if worker_name:
225
+ # Custom worker instance
226
+ client = _get_custom_client(config, worker_name)
227
+ if not client:
228
+ return 1
229
+
230
+ print(f"Stopping instance {local_id} on {worker_name}...")
231
+ try:
232
+ result = asyncio.run(client.stop_instance(local_id))
233
+ print(f"Status: {result.get('status')}")
234
+ return 0
235
+ except CustomWorkerError as e:
236
+ print(f"Error: {e}")
237
+ return 1
238
+ else:
239
+ # RunPod instance
240
+ api_key = config.runpod_api_key
241
+ if not api_key:
242
+ print("Error: RunPod API key not configured.")
243
+ return 1
244
+
245
+ client = RunPodClient(api_key)
246
+ print(f"Stopping instance {local_id}...")
247
+
248
+ try:
249
+ result = asyncio.run(client.stop_pod(local_id))
250
+ print(f"Status: {result.get('desiredStatus')}")
251
+ return 0
252
+ except RunPodAPIError as e:
253
+ print(f"Error: {e}")
254
+ return 1
255
+
256
+
257
+ def handle_terminate(args: Namespace) -> int:
258
+ """Handle 'terminate' command - terminate and remove an instance."""
259
+ config = DeployConfig()
260
+ worker_name, local_id = parse_instance_id(args.instance_id)
261
+ keep_env = getattr(args, "keep_env", False)
262
+
263
+ # Confirm unless --force
264
+ if not getattr(args, "force", False):
265
+ confirm = input(f"Terminate instance {args.instance_id}? This cannot be undone. [y/N]: ")
266
+ if confirm.lower() != "y":
267
+ print("Cancelled.")
268
+ return 0
269
+
270
+ if worker_name:
271
+ # Custom worker instance
272
+ client = _get_custom_client(config, worker_name)
273
+ if not client:
274
+ return 1
275
+
276
+ print(f"Terminating instance {local_id} on {worker_name}...")
277
+ try:
278
+ result = asyncio.run(client.terminate_instance(local_id, keep_env=keep_env))
279
+ print(result.get("message", "Instance terminated."))
280
+ return 0
281
+ except CustomWorkerError as e:
282
+ print(f"Error: {e}")
283
+ return 1
284
+ else:
285
+ # RunPod instance
286
+ api_key = config.runpod_api_key
287
+ if not api_key:
288
+ print("Error: RunPod API key not configured.")
289
+ return 1
290
+
291
+ client = RunPodClient(api_key)
292
+ print(f"Terminating instance {local_id}...")
293
+
294
+ try:
295
+ asyncio.run(client.delete_pod(local_id))
296
+ print("Instance terminated.")
297
+ return 0
298
+ except RunPodAPIError as e:
299
+ print(f"Error: {e}")
300
+ return 1
301
+
302
+
303
+ def handle_open(args: Namespace) -> int:
304
+ """Handle 'open' command - open ComfyUI URL in browser."""
305
+ config = DeployConfig()
306
+ worker_name, local_id = parse_instance_id(args.instance_id)
307
+
308
+ if worker_name:
309
+ # Custom worker instance
310
+ client = _get_custom_client(config, worker_name)
311
+ if not client:
312
+ return 1
313
+
314
+ try:
315
+ instance = asyncio.run(client.get_instance(local_id))
316
+ url = instance.get("comfyui_url")
317
+ if not url:
318
+ print(f"Instance {local_id} is not running or URL not available.")
319
+ return 1
320
+ print(f"Opening: {url}")
321
+ webbrowser.open(url)
322
+ return 0
323
+ except CustomWorkerError as e:
324
+ print(f"Error: {e}")
325
+ return 1
326
+ else:
327
+ # RunPod instance
328
+ api_key = config.runpod_api_key
329
+ if not api_key:
330
+ print("Error: RunPod API key not configured.")
331
+ return 1
332
+
333
+ client = RunPodClient(api_key)
334
+ try:
335
+ pod = asyncio.run(client.get_pod(local_id))
336
+ except RunPodAPIError as e:
337
+ print(f"Error: {e}")
338
+ return 1
339
+
340
+ url = RunPodClient.get_comfyui_url(pod)
341
+ if not url:
342
+ print(f"Instance {local_id} is not running or URL not available.")
343
+ return 1
344
+
345
+ print(f"Opening: {url}")
346
+ webbrowser.open(url)
347
+ return 0
348
+
349
+
350
+ def handle_wait(args: Namespace) -> int:
351
+ """Handle 'wait' command - wait for instance to be ready."""
352
+ config = DeployConfig()
353
+ worker_name, local_id = parse_instance_id(args.instance_id)
354
+ timeout = getattr(args, "timeout", 300)
355
+ start_time = time.time()
356
+
357
+ print(f"Waiting for instance {args.instance_id} to be ready (timeout: {timeout}s)...")
358
+
359
+ if worker_name:
360
+ # Custom worker instance
361
+ client = _get_custom_client(config, worker_name)
362
+ if not client:
363
+ return 1
364
+
365
+ while time.time() - start_time < timeout:
366
+ try:
367
+ instance = asyncio.run(client.get_instance(local_id))
368
+ status = instance.get("status")
369
+
370
+ if status == "running":
371
+ url = instance.get("comfyui_url")
372
+ if url:
373
+ print("\nInstance ready!")
374
+ print(f"ComfyUI URL: {url}")
375
+ return 0
376
+
377
+ elapsed = int(time.time() - start_time)
378
+ print(f"\r Status: {status} ({elapsed}s)", end="", flush=True)
379
+
380
+ except CustomWorkerError as e:
381
+ print(f"\nWarning: {e}")
382
+
383
+ time.sleep(5)
384
+ else:
385
+ # RunPod instance
386
+ api_key = config.runpod_api_key
387
+ if not api_key:
388
+ print("Error: RunPod API key not configured.")
389
+ return 1
390
+
391
+ client = RunPodClient(api_key)
392
+ while time.time() - start_time < timeout:
393
+ try:
394
+ pod = asyncio.run(client.get_pod(local_id))
395
+ status = pod.get("desiredStatus")
396
+
397
+ if status == "RUNNING":
398
+ url = RunPodClient.get_comfyui_url(pod)
399
+ if url:
400
+ print("\nInstance ready!")
401
+ print(f"ComfyUI URL: {url}")
402
+ return 0
403
+
404
+ elapsed = int(time.time() - start_time)
405
+ print(f"\r Status: {status} ({elapsed}s)", end="", flush=True)
406
+
407
+ except RunPodAPIError as e:
408
+ print(f"\nWarning: {e}")
409
+
410
+ time.sleep(5)
411
+
412
+ print(f"\nTimeout: Instance not ready after {timeout}s")
413
+ return 1
414
+
415
+
416
+ def stream_worker_logs(
417
+ host: str, port: int, api_key: str, instance_id: str, follow: bool
418
+ ) -> None:
419
+ """Stream logs from a custom worker instance.
420
+
421
+ Args:
422
+ host: Worker host
423
+ port: Worker port
424
+ api_key: API key
425
+ instance_id: Instance ID (local, not namespaced)
426
+ follow: If True, stream continuously; if False, just print and exit
427
+ """
428
+ from ..providers.custom import CustomWorkerClient
429
+
430
+ async def _stream():
431
+ client = CustomWorkerClient(host=host, port=port, api_key=api_key)
432
+ try:
433
+ async for entry in client.stream_logs(instance_id):
434
+ print(f"[{entry.level}] {entry.message}")
435
+ except KeyboardInterrupt:
436
+ pass
437
+
438
+ asyncio.run(_stream())
439
+
440
+
441
+ def fetch_worker_logs(
442
+ host: str, port: int, api_key: str, instance_id: str, lines: int
443
+ ) -> list[dict]:
444
+ """Fetch recent logs from a custom worker instance.
445
+
446
+ Args:
447
+ host: Worker host
448
+ port: Worker port
449
+ api_key: API key
450
+ instance_id: Instance ID (local, not namespaced)
451
+ lines: Number of lines to fetch
452
+
453
+ Returns:
454
+ List of log entries
455
+ """
456
+ from ..providers.custom import CustomWorkerClient
457
+
458
+ async def _fetch():
459
+ client = CustomWorkerClient(host=host, port=port, api_key=api_key)
460
+ return await client.get_logs(instance_id, lines=lines)
461
+
462
+ return asyncio.run(_fetch())
463
+
464
+
465
+ def handle_logs(args: Namespace) -> int:
466
+ """Handle 'logs' command."""
467
+ config = DeployConfig()
468
+ worker_name, local_id = parse_instance_id(args.instance_id)
469
+ follow = getattr(args, "follow", False)
470
+ lines = getattr(args, "lines", 100)
471
+
472
+ if worker_name:
473
+ # Custom worker logs
474
+ worker = config.get_worker(worker_name)
475
+ if not worker:
476
+ print(f"Error: Worker '{worker_name}' not found.")
477
+ return 1
478
+
479
+ if follow:
480
+ # Stream logs via WebSocket
481
+ stream_worker_logs(
482
+ host=worker["host"],
483
+ port=worker["port"],
484
+ api_key=worker["api_key"],
485
+ instance_id=local_id,
486
+ follow=True,
487
+ )
488
+ else:
489
+ # Fetch recent logs
490
+ logs = fetch_worker_logs(
491
+ host=worker["host"],
492
+ port=worker["port"],
493
+ api_key=worker["api_key"],
494
+ instance_id=local_id,
495
+ lines=lines,
496
+ )
497
+ for entry in logs:
498
+ level = entry.get("level", "INFO")
499
+ message = entry.get("message", "")
500
+ print(f"[{level}] {message}")
501
+ else:
502
+ # RunPod doesn't have a direct logs API - users need to use the console
503
+ print("Log streaming not available via API.")
504
+ print(f"View logs in RunPod console: https://www.runpod.io/console/pods/{local_id}")
505
+
506
+ return 0
@@ -0,0 +1,203 @@
1
+ """RunPod CLI command implementations."""
2
+
3
+ import asyncio
4
+ import sys
5
+ from argparse import Namespace
6
+
7
+ from ..config import DeployConfig
8
+ from ..providers.runpod import RunPodAPIError, RunPodClient
9
+ from ..startup.scripts import generate_deployment_id, generate_startup_script
10
+
11
+
12
+ def _get_client() -> RunPodClient:
13
+ """Get RunPod client with configured API key."""
14
+ config = DeployConfig()
15
+ api_key = config.runpod_api_key
16
+ if not api_key:
17
+ print("Error: RunPod API key not configured.")
18
+ print("Run: cg-deploy runpod config --api-key <your-key>")
19
+ sys.exit(1)
20
+ return RunPodClient(api_key)
21
+
22
+
23
+ def handle_config(args: Namespace) -> int:
24
+ """Handle 'runpod config' command."""
25
+ config = DeployConfig()
26
+
27
+ if args.api_key:
28
+ config.runpod_api_key = args.api_key
29
+ config.save()
30
+ # Test the connection
31
+ client = RunPodClient(args.api_key)
32
+ result = asyncio.run(client.test_connection())
33
+ if result["success"]:
34
+ print(f"API key saved. Credit balance: ${result['credit_balance']:.2f}")
35
+ else:
36
+ print(f"Warning: API key saved but connection test failed: {result['error']}")
37
+ return 0
38
+
39
+ if args.clear:
40
+ config.runpod_api_key = None
41
+ config.save()
42
+ print("API key cleared.")
43
+ return 0
44
+
45
+ if args.show or (not args.api_key and not args.clear):
46
+ api_key = config.runpod_api_key
47
+ if api_key:
48
+ # Mask the key for display
49
+ masked = api_key[:8] + "..." + api_key[-4:] if len(api_key) > 12 else "***"
50
+ print(f"API Key: {masked}")
51
+ # Test connection
52
+ client = RunPodClient(api_key)
53
+ result = asyncio.run(client.test_connection())
54
+ if result["success"]:
55
+ print("Status: Connected")
56
+ print(f"Credit Balance: ${result['credit_balance']:.2f}")
57
+ else:
58
+ print(f"Status: Error - {result['error']}")
59
+ else:
60
+ print("No API key configured.")
61
+ print("Run: cg-deploy runpod config --api-key <your-key>")
62
+ return 0
63
+
64
+ return 0
65
+
66
+
67
+ def handle_gpus(args: Namespace) -> int:
68
+ """Handle 'runpod gpus' command."""
69
+ client = _get_client()
70
+ region = getattr(args, "region", None)
71
+
72
+ try:
73
+ gpus = asyncio.run(client.get_gpu_types_with_pricing(region))
74
+ except RunPodAPIError as e:
75
+ print(f"Error: {e}")
76
+ return 1
77
+
78
+ # Sort by price (cheapest first)
79
+ gpus = sorted(gpus, key=lambda g: g.get("securePrice") or 999)
80
+
81
+ print(f"{'GPU Type':<35} {'VRAM':>6} {'Secure':>8} {'Spot':>8} {'Stock':>10}")
82
+ print("-" * 75)
83
+
84
+ for gpu in gpus:
85
+ name = gpu.get("displayName", gpu.get("id", "Unknown"))[:35]
86
+ vram = gpu.get("memoryInGb", "?")
87
+ secure_price = gpu.get("securePrice")
88
+ spot_price = gpu.get("secureSpotPrice")
89
+ stock = gpu.get("lowestPrice", {}).get("stockStatus", "?")
90
+
91
+ secure_str = f"${secure_price:.2f}/hr" if secure_price else "N/A"
92
+ spot_str = f"${spot_price:.2f}/hr" if spot_price else "N/A"
93
+
94
+ print(f"{name:<35} {vram:>4}GB {secure_str:>8} {spot_str:>8} {stock:>10}")
95
+
96
+ return 0
97
+
98
+
99
+ def handle_volumes(args: Namespace) -> int:
100
+ """Handle 'runpod volumes' command."""
101
+ client = _get_client()
102
+
103
+ try:
104
+ volumes = asyncio.run(client.list_network_volumes())
105
+ except RunPodAPIError as e:
106
+ print(f"Error: {e}")
107
+ return 1
108
+
109
+ if not volumes:
110
+ print("No network volumes found.")
111
+ return 0
112
+
113
+ print(f"{'ID':<15} {'Name':<25} {'Size':>8} {'Region':<15}")
114
+ print("-" * 65)
115
+
116
+ for vol in volumes:
117
+ vol_id = vol.get("id", "?")
118
+ name = vol.get("name", "?")[:25]
119
+ size = vol.get("size", "?")
120
+ region = vol.get("dataCenterId", vol.get("dataCenter", "?"))
121
+
122
+ print(f"{vol_id:<15} {name:<25} {size:>6}GB {region:<15}")
123
+
124
+ return 0
125
+
126
+
127
+ def handle_regions(args: Namespace) -> int:
128
+ """Handle 'runpod regions' command."""
129
+ client = _get_client()
130
+
131
+ try:
132
+ regions = asyncio.run(client.get_data_centers())
133
+ except RunPodAPIError as e:
134
+ print(f"Error: {e}")
135
+ return 1
136
+
137
+ print(f"{'ID':<15} {'Name':<35} {'Available':>10}")
138
+ print("-" * 62)
139
+
140
+ for dc in regions:
141
+ dc_id = dc.get("id", "?")
142
+ name = dc.get("name", "?")[:35]
143
+ available = "Yes" if dc.get("available", True) else "No"
144
+
145
+ print(f"{dc_id:<15} {name:<35} {available:>10}")
146
+
147
+ return 0
148
+
149
+
150
+ def handle_deploy(args: Namespace) -> int:
151
+ """Handle 'runpod deploy' command."""
152
+ client = _get_client()
153
+
154
+ # Generate deployment ID
155
+ name = getattr(args, "name", None) or args.import_source.split("/")[-1].replace(".git", "")
156
+ deployment_id = generate_deployment_id(name)
157
+
158
+ # Generate startup script
159
+ startup_script = generate_startup_script(
160
+ deployment_id=deployment_id,
161
+ import_source=args.import_source,
162
+ branch=getattr(args, "branch", None),
163
+ )
164
+
165
+ # Create pod
166
+ print(f"Creating deployment: {deployment_id}")
167
+ print(f"GPU: {args.gpu}")
168
+ print(f"Source: {args.import_source}")
169
+
170
+ try:
171
+ # Use spot pricing if requested
172
+ pricing_type = getattr(args, "pricing_type", "ON_DEMAND")
173
+ cloud_type = getattr(args, "cloud_type", "SECURE")
174
+ volume_id = getattr(args, "volume", None)
175
+
176
+ pod = asyncio.run(
177
+ client.create_pod(
178
+ name=deployment_id,
179
+ image_name="runpod/pytorch:2.1.0-py3.10-cuda11.8.0-devel-ubuntu22.04",
180
+ gpu_type_id=args.gpu,
181
+ cloud_type=cloud_type,
182
+ ports=["8188/http", "22/tcp"],
183
+ docker_start_cmd=["/bin/bash", "-c", startup_script],
184
+ network_volume_id=volume_id,
185
+ interruptible=(pricing_type == "SPOT"),
186
+ )
187
+ )
188
+ except RunPodAPIError as e:
189
+ print(f"Error creating pod: {e}")
190
+ return 1
191
+
192
+ pod_id = pod.get("id")
193
+ print(f"\nPod created: {pod_id}")
194
+ print(f"Status: {pod.get('desiredStatus')}")
195
+
196
+ url = RunPodClient.get_comfyui_url(pod)
197
+ if url:
198
+ print(f"ComfyUI URL: {url}")
199
+
200
+ print(f"\nConsole: https://www.runpod.io/console/pods/{pod_id}")
201
+ print("\nUse 'cg-deploy wait {pod_id}' to wait for deployment to complete.")
202
+
203
+ return 0