tetra-rp 0.6.0__py3-none-any.whl → 0.24.0__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.
Files changed (97) hide show
  1. tetra_rp/__init__.py +109 -19
  2. tetra_rp/cli/commands/__init__.py +1 -0
  3. tetra_rp/cli/commands/apps.py +143 -0
  4. tetra_rp/cli/commands/build.py +1082 -0
  5. tetra_rp/cli/commands/build_utils/__init__.py +1 -0
  6. tetra_rp/cli/commands/build_utils/handler_generator.py +176 -0
  7. tetra_rp/cli/commands/build_utils/lb_handler_generator.py +309 -0
  8. tetra_rp/cli/commands/build_utils/manifest.py +430 -0
  9. tetra_rp/cli/commands/build_utils/mothership_handler_generator.py +75 -0
  10. tetra_rp/cli/commands/build_utils/scanner.py +596 -0
  11. tetra_rp/cli/commands/deploy.py +580 -0
  12. tetra_rp/cli/commands/init.py +123 -0
  13. tetra_rp/cli/commands/resource.py +108 -0
  14. tetra_rp/cli/commands/run.py +296 -0
  15. tetra_rp/cli/commands/test_mothership.py +458 -0
  16. tetra_rp/cli/commands/undeploy.py +533 -0
  17. tetra_rp/cli/main.py +97 -0
  18. tetra_rp/cli/utils/__init__.py +1 -0
  19. tetra_rp/cli/utils/app.py +15 -0
  20. tetra_rp/cli/utils/conda.py +127 -0
  21. tetra_rp/cli/utils/deployment.py +530 -0
  22. tetra_rp/cli/utils/ignore.py +143 -0
  23. tetra_rp/cli/utils/skeleton.py +184 -0
  24. tetra_rp/cli/utils/skeleton_template/.env.example +4 -0
  25. tetra_rp/cli/utils/skeleton_template/.flashignore +40 -0
  26. tetra_rp/cli/utils/skeleton_template/.gitignore +44 -0
  27. tetra_rp/cli/utils/skeleton_template/README.md +263 -0
  28. tetra_rp/cli/utils/skeleton_template/main.py +44 -0
  29. tetra_rp/cli/utils/skeleton_template/mothership.py +55 -0
  30. tetra_rp/cli/utils/skeleton_template/pyproject.toml +58 -0
  31. tetra_rp/cli/utils/skeleton_template/requirements.txt +1 -0
  32. tetra_rp/cli/utils/skeleton_template/workers/__init__.py +0 -0
  33. tetra_rp/cli/utils/skeleton_template/workers/cpu/__init__.py +19 -0
  34. tetra_rp/cli/utils/skeleton_template/workers/cpu/endpoint.py +36 -0
  35. tetra_rp/cli/utils/skeleton_template/workers/gpu/__init__.py +19 -0
  36. tetra_rp/cli/utils/skeleton_template/workers/gpu/endpoint.py +61 -0
  37. tetra_rp/client.py +136 -33
  38. tetra_rp/config.py +29 -0
  39. tetra_rp/core/api/runpod.py +591 -39
  40. tetra_rp/core/deployment.py +232 -0
  41. tetra_rp/core/discovery.py +425 -0
  42. tetra_rp/core/exceptions.py +50 -0
  43. tetra_rp/core/resources/__init__.py +27 -9
  44. tetra_rp/core/resources/app.py +738 -0
  45. tetra_rp/core/resources/base.py +139 -4
  46. tetra_rp/core/resources/constants.py +21 -0
  47. tetra_rp/core/resources/cpu.py +115 -13
  48. tetra_rp/core/resources/gpu.py +182 -16
  49. tetra_rp/core/resources/live_serverless.py +153 -16
  50. tetra_rp/core/resources/load_balancer_sls_resource.py +440 -0
  51. tetra_rp/core/resources/network_volume.py +126 -31
  52. tetra_rp/core/resources/resource_manager.py +436 -35
  53. tetra_rp/core/resources/serverless.py +537 -120
  54. tetra_rp/core/resources/serverless_cpu.py +201 -0
  55. tetra_rp/core/resources/template.py +1 -59
  56. tetra_rp/core/utils/constants.py +10 -0
  57. tetra_rp/core/utils/file_lock.py +260 -0
  58. tetra_rp/core/utils/http.py +67 -0
  59. tetra_rp/core/utils/lru_cache.py +75 -0
  60. tetra_rp/core/utils/singleton.py +36 -1
  61. tetra_rp/core/validation.py +44 -0
  62. tetra_rp/execute_class.py +301 -0
  63. tetra_rp/protos/remote_execution.py +98 -9
  64. tetra_rp/runtime/__init__.py +1 -0
  65. tetra_rp/runtime/circuit_breaker.py +274 -0
  66. tetra_rp/runtime/config.py +12 -0
  67. tetra_rp/runtime/exceptions.py +49 -0
  68. tetra_rp/runtime/generic_handler.py +206 -0
  69. tetra_rp/runtime/lb_handler.py +189 -0
  70. tetra_rp/runtime/load_balancer.py +160 -0
  71. tetra_rp/runtime/manifest_fetcher.py +192 -0
  72. tetra_rp/runtime/metrics.py +325 -0
  73. tetra_rp/runtime/models.py +73 -0
  74. tetra_rp/runtime/mothership_provisioner.py +512 -0
  75. tetra_rp/runtime/production_wrapper.py +266 -0
  76. tetra_rp/runtime/reliability_config.py +149 -0
  77. tetra_rp/runtime/retry_manager.py +118 -0
  78. tetra_rp/runtime/serialization.py +124 -0
  79. tetra_rp/runtime/service_registry.py +346 -0
  80. tetra_rp/runtime/state_manager_client.py +248 -0
  81. tetra_rp/stubs/live_serverless.py +35 -17
  82. tetra_rp/stubs/load_balancer_sls.py +357 -0
  83. tetra_rp/stubs/registry.py +145 -19
  84. {tetra_rp-0.6.0.dist-info → tetra_rp-0.24.0.dist-info}/METADATA +398 -60
  85. tetra_rp-0.24.0.dist-info/RECORD +99 -0
  86. {tetra_rp-0.6.0.dist-info → tetra_rp-0.24.0.dist-info}/WHEEL +1 -1
  87. tetra_rp-0.24.0.dist-info/entry_points.txt +2 -0
  88. tetra_rp/core/pool/cluster_manager.py +0 -177
  89. tetra_rp/core/pool/dataclass.py +0 -18
  90. tetra_rp/core/pool/ex.py +0 -38
  91. tetra_rp/core/pool/job.py +0 -22
  92. tetra_rp/core/pool/worker.py +0 -19
  93. tetra_rp/core/resources/utils.py +0 -50
  94. tetra_rp/core/utils/json.py +0 -33
  95. tetra_rp-0.6.0.dist-info/RECORD +0 -39
  96. /tetra_rp/{core/pool → cli}/__init__.py +0 -0
  97. {tetra_rp-0.6.0.dist-info → tetra_rp-0.24.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,580 @@
1
+ """Deployment environment management commands."""
2
+
3
+ import typer
4
+ from rich.console import Console
5
+ from rich.table import Table
6
+ from rich.panel import Panel
7
+ from pathlib import Path
8
+ import questionary
9
+ import asyncio
10
+
11
+ from ..utils.deployment import (
12
+ get_deployment_environments,
13
+ create_deployment_environment,
14
+ remove_deployment_environment,
15
+ deploy_to_environment,
16
+ rollback_deployment,
17
+ get_environment_info,
18
+ )
19
+
20
+ from ..utils.app import discover_flash_project
21
+
22
+ from tetra_rp.core.resources.app import FlashApp
23
+
24
+ console = Console()
25
+
26
+
27
+ def _get_resource_manager():
28
+ from tetra_rp.core.resources.resource_manager import ResourceManager
29
+
30
+ return ResourceManager()
31
+
32
+
33
+ async def _undeploy_environment_resources(env_name: str, env: dict) -> None:
34
+ """Undeploy resources tied to a flash environment before deletion."""
35
+ endpoints = env.get("endpoints") or []
36
+ network_volumes = env.get("networkVolumes") or []
37
+
38
+ if not endpoints and not network_volumes:
39
+ return
40
+
41
+ manager = _get_resource_manager()
42
+ failures = []
43
+ undeployed = 0
44
+ seen_resource_ids = set()
45
+
46
+ with console.status(f"Undeploying resources for '{env_name}'..."):
47
+ for label, items in (
48
+ ("Endpoint", endpoints),
49
+ ("Network volume", network_volumes),
50
+ ):
51
+ for item in items:
52
+ provider_id = item.get("id") if isinstance(item, dict) else None
53
+ name = item.get("name") if isinstance(item, dict) else None
54
+ if not provider_id:
55
+ failures.append(f"{label} missing id in environment '{env_name}'")
56
+ continue
57
+
58
+ matches = manager.find_resources_by_provider_id(provider_id)
59
+ if not matches:
60
+ display_name = name if name else provider_id
61
+ failures.append(
62
+ f"{label} '{display_name}' ({provider_id}) not found in local tracking"
63
+ )
64
+ continue
65
+
66
+ for resource_id, resource in matches:
67
+ if resource_id in seen_resource_ids:
68
+ continue
69
+ seen_resource_ids.add(resource_id)
70
+ resource_name = getattr(resource, "name", name) or provider_id
71
+ result = await manager.undeploy_resource(resource_id, resource_name)
72
+ if result.get("success"):
73
+ undeployed += 1
74
+ else:
75
+ failures.append(
76
+ result.get(
77
+ "message",
78
+ f"Failed to undeploy {label.lower()} '{resource_name}'",
79
+ )
80
+ )
81
+
82
+ if failures:
83
+ console.print(
84
+ "❌ Failed to undeploy all resources; environment deletion aborted."
85
+ )
86
+ for message in failures:
87
+ console.print(f" • {message}")
88
+ raise typer.Exit(1)
89
+
90
+ if undeployed:
91
+ console.print(f"✅ Undeployed {undeployed} resource(s) for '{env_name}'")
92
+
93
+
94
+ def list_command(
95
+ app_name: str | None = typer.Option(
96
+ None, "--app-name", "-a", help="flash app name to inspect"
97
+ ),
98
+ ):
99
+ """Show available deployment environments."""
100
+ if not app_name:
101
+ _, app_name = discover_flash_project()
102
+ asyncio.run(list_flash_environments(app_name))
103
+
104
+
105
+ async def new_flash_deployment_environment(app_name: str, env_name: str):
106
+ """
107
+ Create a new flash deployment environment. Creates a flash app if it doesn't already exist.
108
+ """
109
+ app, env = await FlashApp.create_environment_and_app(app_name, env_name)
110
+
111
+ panel_content = (
112
+ f"Environment '[bold]{env_name}[/bold]' created successfully\n\n"
113
+ f"App: {app_name}\n"
114
+ f"Environment ID: {env.get('id')}\n"
115
+ f"Status: {env.get('state', 'PENDING')}"
116
+ )
117
+ console.print(Panel(panel_content, title="✅ Environment Created", expand=False))
118
+
119
+ table = Table(show_header=True, header_style="bold")
120
+ table.add_column("Name", style="bold")
121
+ table.add_column("ID", overflow="fold")
122
+ table.add_column("Status", overflow="fold")
123
+ table.add_column("Created At", overflow="fold")
124
+
125
+ table.add_row(
126
+ env.get("name"),
127
+ env.get("id"),
128
+ env.get("state", "PENDING"),
129
+ env.get("createdAt", "Just now"),
130
+ )
131
+ console.print(table)
132
+
133
+ console.print(f"\nNext: [bold]flash deploy send {env_name}[/bold]")
134
+
135
+
136
+ async def info_flash_environment(app_name: str, env_name: str):
137
+ """
138
+ Get detailed information about a flash deployment environment.
139
+ """
140
+ app = await FlashApp.from_name(app_name)
141
+ env = await app.get_environment_by_name(env_name)
142
+
143
+ main_info = f"Environment: {env.get('name')}\n"
144
+ main_info += f"ID: {env.get('id')}\n"
145
+ main_info += f"State: {env.get('state', 'UNKNOWN')}\n"
146
+ main_info += f"Active Build: {env.get('activeBuildId', 'None')}\n"
147
+
148
+ if env.get("createdAt"):
149
+ main_info += f"Created: {env.get('createdAt')}\n"
150
+
151
+ console.print(Panel(main_info, title=f"📊 Environment: {env_name}", expand=False))
152
+
153
+ endpoints = env.get("endpoints") or []
154
+ if endpoints:
155
+ endpoint_table = Table(title="Associated Endpoints")
156
+ endpoint_table.add_column("Name", style="cyan")
157
+ endpoint_table.add_column("ID", overflow="fold")
158
+
159
+ for endpoint in endpoints:
160
+ endpoint_table.add_row(
161
+ endpoint.get("name", "—"),
162
+ endpoint.get("id", "—"),
163
+ )
164
+ console.print(endpoint_table)
165
+
166
+ network_volumes = env.get("networkVolumes") or []
167
+ if network_volumes:
168
+ nv_table = Table(title="Associated Network Volumes")
169
+ nv_table.add_column("Name", style="cyan")
170
+ nv_table.add_column("ID", overflow="fold")
171
+
172
+ for nv in network_volumes:
173
+ nv_table.add_row(
174
+ nv.get("name", "—"),
175
+ nv.get("id", "—"),
176
+ )
177
+ console.print(nv_table)
178
+
179
+
180
+ async def list_flash_environments(app_name: str):
181
+ app = await FlashApp.from_name(app_name)
182
+ envs = await app.list_environments()
183
+
184
+ if not envs:
185
+ console.print(f"No environments found for '{app_name}'.")
186
+ return
187
+
188
+ table = Table(show_header=True, header_style="bold")
189
+ table.add_column("Name", style="bold")
190
+ table.add_column("ID", overflow="fold")
191
+ table.add_column("Active Build", overflow="fold")
192
+ table.add_column("Created At", overflow="fold")
193
+
194
+ for env in envs:
195
+ table.add_row(
196
+ env.get("name"),
197
+ env.get("id"),
198
+ env.get("activeBuildId", "-"),
199
+ env.get("createdAt"),
200
+ )
201
+
202
+ console.print(table)
203
+
204
+
205
+ def new_command(
206
+ app_name: str | None = typer.Option(
207
+ None, "--app-name", "-a", help="Flash app name to create a new environment in"
208
+ ),
209
+ name: str = typer.Argument(
210
+ ..., help="Name of the deployment environment to create"
211
+ ),
212
+ ):
213
+ """Create a new deployment environment."""
214
+ if not app_name:
215
+ _, app_name = discover_flash_project()
216
+ assert app_name is not None
217
+ asyncio.run(new_flash_deployment_environment(app_name, name))
218
+ return
219
+
220
+ environments = get_deployment_environments()
221
+
222
+ if name in environments:
223
+ console.print(f"Environment '{name}' already exists")
224
+ raise typer.Exit(1)
225
+
226
+ # Interactive configuration
227
+ config = {}
228
+
229
+ try:
230
+ config["region"] = questionary.select(
231
+ "Select region:",
232
+ choices=["us-east-1", "us-west-2", "eu-west-1", "ap-southeast-1"],
233
+ ).ask()
234
+
235
+ config["instance_type"] = questionary.select(
236
+ "Instance type:", choices=["A40", "A100", "H100", "RTX4090"]
237
+ ).ask()
238
+
239
+ config["auto_scale"] = questionary.confirm("Enable auto-scaling?").ask()
240
+
241
+ if not all([config["region"], config["instance_type"]]):
242
+ console.print("Configuration cancelled")
243
+ raise typer.Exit(1)
244
+
245
+ except KeyboardInterrupt:
246
+ console.print("\nEnvironment creation cancelled")
247
+ raise typer.Exit(1)
248
+
249
+ # Create environment
250
+ with console.status(f"Creating environment '{name}'..."):
251
+ create_deployment_environment(name, config)
252
+
253
+ # Success message
254
+ panel_content = f"Environment '[bold]{name}[/bold]' created successfully\n\n"
255
+ panel_content += f"Region: {config['region']}\n"
256
+ panel_content += f"Instance: {config['instance_type']}\n"
257
+ panel_content += f"Auto-scale: {'Enabled' if config['auto_scale'] else 'Disabled'}"
258
+
259
+ console.print(Panel(panel_content, title="🚀 Environment Created", expand=False))
260
+
261
+ console.print(f"\nNext: [bold]flash deploy send {name}[/bold]")
262
+
263
+
264
+ def send_command(
265
+ env_name: str = typer.Argument(..., help="Name of the deployment environment"),
266
+ app_name: str = typer.Option(None, "--app-name", "-a", help="Flash app name"),
267
+ ):
268
+ """Deploy project to deployment environment."""
269
+
270
+ if not app_name:
271
+ _, app_name = discover_flash_project()
272
+
273
+ build_path = Path(".flash/archive.tar.gz")
274
+ if not build_path.exists():
275
+ console.print(
276
+ "no build path found in current directory. Build your project with flash build first"
277
+ )
278
+ raise typer.Exit(1)
279
+
280
+ console.print(f"🚀 Deploying to '[bold]{env_name}[/bold]'...")
281
+
282
+ try:
283
+ asyncio.run(deploy_to_environment(app_name, env_name, build_path))
284
+
285
+ panel_content = f"Deployed to '[bold]{env_name}[/bold]' successfully\n\n"
286
+
287
+ console.print(Panel(panel_content, title="Deployment Complete", expand=False))
288
+
289
+ except Exception as e:
290
+ console.print(f"Deployment failed: {e}")
291
+ raise typer.Exit(1)
292
+
293
+
294
+ def info_command(
295
+ env_name: str = typer.Argument(..., help="Name of the deployment environment"),
296
+ app_name: str = typer.Option(None, "--app-name", "-a", help="Flash app name"),
297
+ ):
298
+ """Show detailed information about a deployment environment."""
299
+ if not app_name:
300
+ _, app_name = discover_flash_project()
301
+ asyncio.run(info_flash_environment(app_name, env_name))
302
+
303
+
304
+ async def _fetch_environment_info(app_name: str, env_name: str) -> dict:
305
+ """Fetch environment information for display.
306
+
307
+ Args:
308
+ app_name: Flash application name
309
+ env_name: Environment name to fetch
310
+
311
+ Returns:
312
+ Environment dictionary with id, name, activeBuildId, etc.
313
+
314
+ Raises:
315
+ Exception: If environment doesn't exist or API call fails
316
+ """
317
+ app = await FlashApp.from_name(app_name)
318
+ env = await app.get_environment_by_name(env_name)
319
+ return env
320
+
321
+
322
+ async def delete_flash_environment(app_name: str, env_name: str):
323
+ """Delete a flash deployment environment.
324
+
325
+ Note: User confirmation should be handled by caller before calling this function.
326
+ This function only performs the deletion operation.
327
+
328
+ This design ensures questionary prompts run in sync context, avoiding
329
+ event loop conflicts between asyncio.run() and prompt_toolkit's Application.run().
330
+
331
+ Args:
332
+ app_name: Flash application name
333
+ env_name: Environment name to delete
334
+
335
+ Raises:
336
+ typer.Exit: If deletion fails
337
+ """
338
+ app = await FlashApp.from_name(app_name)
339
+ env = await app.get_environment_by_name(env_name)
340
+
341
+ await _undeploy_environment_resources(env_name, env)
342
+
343
+ with console.status(f"Deleting environment '{env_name}'..."):
344
+ success = await app.delete_environment(env_name)
345
+
346
+ if success:
347
+ console.print(f"✅ Environment '{env_name}' deleted successfully")
348
+ else:
349
+ console.print(f"❌ Failed to delete environment '{env_name}'")
350
+ raise typer.Exit(1)
351
+
352
+
353
+ def delete_command(
354
+ env_name: str = typer.Argument(
355
+ ..., help="Name of the deployment environment to delete"
356
+ ),
357
+ app_name: str = typer.Option(None, "--app-name", "-a", help="Flash app name"),
358
+ ):
359
+ """Delete a deployment environment."""
360
+ if not app_name:
361
+ _, app_name = discover_flash_project()
362
+
363
+ # Fetch environment info in async context for display
364
+ try:
365
+ env = asyncio.run(_fetch_environment_info(app_name, env_name))
366
+ except Exception as e:
367
+ console.print(f"[red]Error:[/red] Failed to fetch environment info: {e}")
368
+ raise typer.Exit(1)
369
+
370
+ # Display deletion preview in sync context
371
+ panel_content = (
372
+ f"Environment '[bold]{env_name}[/bold]' will be deleted\n\n"
373
+ f"Environment ID: {env.get('id')}\n"
374
+ f"App: {app_name}\n"
375
+ f"Active Build: {env.get('activeBuildId', 'None')}"
376
+ )
377
+ console.print(Panel(panel_content, title="⚠️ Delete Confirmation", expand=False))
378
+
379
+ # Get user confirmation in sync context (BEFORE asyncio.run for deletion)
380
+ try:
381
+ confirmed = questionary.confirm(
382
+ f"Are you sure you want to delete environment '{env_name}'? This will delete all resources associated with this environment!"
383
+ ).ask()
384
+
385
+ if not confirmed:
386
+ console.print("Deletion cancelled")
387
+ raise typer.Exit(0)
388
+ except KeyboardInterrupt:
389
+ console.print("\nDeletion cancelled")
390
+ raise typer.Exit(0)
391
+
392
+ # Perform async deletion after confirmation
393
+ asyncio.run(delete_flash_environment(app_name, env_name))
394
+
395
+
396
+ def report_command(
397
+ name: str = typer.Argument(..., help="Name of the deployment environment"),
398
+ ):
399
+ """Show detailed environment status and metrics."""
400
+
401
+ environments = get_deployment_environments()
402
+
403
+ if name not in environments:
404
+ console.print(f"Environment '{name}' not found")
405
+ raise typer.Exit(1)
406
+
407
+ env_info = get_environment_info(name)
408
+
409
+ # Environment status
410
+ status = env_info.get("status", "unknown")
411
+ status_display = {
412
+ "active": "🟢 Active",
413
+ "idle": "🟡 Idle",
414
+ "error": "🔴 Error",
415
+ }.get(status, "❓ Unknown")
416
+
417
+ # Main info panel
418
+ main_info = f"Status: {status_display}\n"
419
+ main_info += f"Current Version: {env_info.get('current_version', 'N/A')}\n"
420
+ main_info += f"URL: {env_info.get('url', 'N/A')}\n"
421
+ main_info += f"Last Deployed: {env_info.get('last_deployed', 'Never')}\n"
422
+ main_info += f"Uptime: {env_info.get('uptime', 'N/A')}"
423
+
424
+ console.print(
425
+ Panel(main_info, title=f"📊 Environment Report: {name}", expand=False)
426
+ )
427
+
428
+ # Version history
429
+ versions = env_info.get("version_history", [])
430
+ if versions:
431
+ version_table = Table(title="Version History")
432
+ version_table.add_column("Version", style="cyan")
433
+ version_table.add_column("Status", justify="center")
434
+ version_table.add_column("Deployed", style="yellow")
435
+ version_table.add_column("Description", style="white")
436
+
437
+ for version in versions[:5]: # Show last 5 versions
438
+ version_status = (
439
+ "🟢 Current" if version.get("is_current") else "📦 Previous"
440
+ )
441
+ version_table.add_row(
442
+ version.get("version", "N/A"),
443
+ version_status,
444
+ version.get("deployed_at", "N/A"),
445
+ version.get("description", "No description"),
446
+ )
447
+
448
+ console.print(version_table)
449
+
450
+ # Mock metrics
451
+ console.print("\n[bold]Metrics (Last 24h):[/bold]")
452
+ metrics_info = [
453
+ "• Requests: 145,234",
454
+ "• Avg Response Time: 245ms",
455
+ "• Error Rate: 0.02%",
456
+ "• CPU Usage: 45%",
457
+ "• Memory Usage: 62%",
458
+ ]
459
+
460
+ for metric in metrics_info:
461
+ console.print(f" {metric}")
462
+
463
+
464
+ def rollback_command(
465
+ name: str = typer.Argument(..., help="Name of the deployment environment"),
466
+ ):
467
+ """Rollback deployment to previous version."""
468
+
469
+ environments = get_deployment_environments()
470
+
471
+ if name not in environments:
472
+ console.print(f"Environment '{name}' not found")
473
+ raise typer.Exit(1)
474
+
475
+ env_info = get_environment_info(name)
476
+ versions = env_info.get("version_history", [])
477
+
478
+ if len(versions) < 2:
479
+ console.print("No previous versions available for rollback")
480
+ raise typer.Exit(1)
481
+
482
+ # Show available versions (excluding current)
483
+ previous_versions = [v for v in versions if not v.get("is_current")]
484
+
485
+ if not previous_versions:
486
+ console.print("No previous versions available for rollback")
487
+ raise typer.Exit(1)
488
+
489
+ try:
490
+ version_choices = [
491
+ f"{v['version']} - {v.get('description', 'No description')}"
492
+ for v in previous_versions[:5]
493
+ ]
494
+
495
+ selected = questionary.select(
496
+ "Select version to rollback to:", choices=version_choices
497
+ ).ask()
498
+
499
+ if not selected:
500
+ console.print("Rollback cancelled")
501
+ raise typer.Exit(1)
502
+
503
+ target_version = selected.split(" - ")[0]
504
+
505
+ # Confirmation
506
+ confirmed = questionary.confirm(
507
+ f"Rollback environment '{name}' to version {target_version}?"
508
+ ).ask()
509
+
510
+ if not confirmed:
511
+ console.print("Rollback cancelled")
512
+ raise typer.Exit(1)
513
+
514
+ except KeyboardInterrupt:
515
+ console.print("\nRollback cancelled")
516
+ raise typer.Exit(1)
517
+
518
+ # Perform rollback
519
+ with console.status(f"Rolling back to {target_version}..."):
520
+ rollback_deployment(name, target_version)
521
+
522
+ console.print(f"Rolled back to version {target_version}")
523
+ console.print(f"Environment '{name}' is now running the previous version.")
524
+
525
+
526
+ def remove_command(
527
+ name: str = typer.Argument(
528
+ ..., help="Name of the deployment environment to remove"
529
+ ),
530
+ ):
531
+ """Remove deployment environment."""
532
+
533
+ environments = get_deployment_environments()
534
+
535
+ if name not in environments:
536
+ console.print(f"Environment '{name}' not found")
537
+ raise typer.Exit(1)
538
+
539
+ env_info = get_environment_info(name)
540
+
541
+ # Show removal preview
542
+ preview_content = f"Environment: {name}\n"
543
+ preview_content += f"Status: {env_info.get('status', 'unknown')}\n"
544
+ preview_content += f"URL: {env_info.get('url', 'N/A')}\n"
545
+ preview_content += f"Current Version: {env_info.get('current_version', 'N/A')}\n\n"
546
+ preview_content += "⚠️ This will permanently remove:\n"
547
+ preview_content += " • All deployment history\n"
548
+ preview_content += " • All associated resources\n"
549
+ preview_content += " • Environment configuration\n"
550
+ preview_content += " • Access URLs\n\n"
551
+ preview_content += "🚨 This action cannot be undone!"
552
+
553
+ console.print(Panel(preview_content, title="⚠️ Removal Preview", expand=False))
554
+
555
+ try:
556
+ # Double confirmation for safety
557
+ confirmed = questionary.confirm(
558
+ f"Are you sure you want to remove environment '{name}'?"
559
+ ).ask()
560
+
561
+ if not confirmed:
562
+ console.print("Removal cancelled")
563
+ raise typer.Exit(1)
564
+
565
+ # Type confirmation
566
+ typed_name = questionary.text(f"Type '{name}' to confirm removal:").ask()
567
+
568
+ if typed_name != name:
569
+ console.print("Confirmation failed - names do not match")
570
+ raise typer.Exit(1)
571
+
572
+ except KeyboardInterrupt:
573
+ console.print("\nRemoval cancelled")
574
+ raise typer.Exit(1)
575
+
576
+ # Remove environment
577
+ with console.status(f"Removing environment '{name}'..."):
578
+ remove_deployment_environment(name)
579
+
580
+ console.print(f"Environment '{name}' removed successfully")
@@ -0,0 +1,123 @@
1
+ """Project initialization command."""
2
+
3
+ from pathlib import Path
4
+ from typing import Optional
5
+
6
+ import typer
7
+ from rich.console import Console
8
+ from rich.panel import Panel
9
+ from rich.table import Table
10
+
11
+ from ..utils.skeleton import create_project_skeleton, detect_file_conflicts
12
+
13
+ console = Console()
14
+
15
+
16
+ def init_command(
17
+ project_name: Optional[str] = typer.Argument(
18
+ None, help="Project name or '.' for current directory"
19
+ ),
20
+ force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing files"),
21
+ ):
22
+ """Create new Flash project with Flash Server and GPU workers."""
23
+
24
+ # Determine target directory and initialization mode
25
+ if project_name is None or project_name == ".":
26
+ # Initialize in current directory
27
+ project_dir = Path.cwd()
28
+ is_current_dir = True
29
+ # Use current directory name as project name
30
+ actual_project_name = project_dir.name
31
+ else:
32
+ # Create new directory
33
+ project_dir = Path(project_name)
34
+ is_current_dir = False
35
+ actual_project_name = project_name
36
+
37
+ # Create project directory if needed
38
+ if not is_current_dir:
39
+ project_dir.mkdir(parents=True, exist_ok=True)
40
+
41
+ # Check for file conflicts in target directory
42
+ conflicts = detect_file_conflicts(project_dir)
43
+ should_overwrite = force # Start with force flag value
44
+
45
+ if conflicts and not force:
46
+ # Show warning and prompt user
47
+ console.print(
48
+ Panel(
49
+ "[yellow]Warning: The following files will be overwritten:[/yellow]\n\n"
50
+ + "\n".join(f" • {conflict}" for conflict in conflicts),
51
+ title="File Conflicts Detected",
52
+ expand=False,
53
+ )
54
+ )
55
+
56
+ # Prompt user for confirmation
57
+ proceed = typer.confirm("Continue and overwrite these files?", default=False)
58
+ if not proceed:
59
+ console.print("[yellow]Initialization aborted.[/yellow]")
60
+ raise typer.Exit(0)
61
+
62
+ # User confirmed, so we should overwrite
63
+ should_overwrite = True
64
+
65
+ # Create project skeleton
66
+ status_msg = (
67
+ "Initializing Flash project in current directory..."
68
+ if is_current_dir
69
+ else f"Creating Flash project '{project_name}'..."
70
+ )
71
+ with console.status(status_msg):
72
+ create_project_skeleton(project_dir, should_overwrite)
73
+
74
+ # Success output
75
+ if is_current_dir:
76
+ panel_content = f"Flash project '[bold]{actual_project_name}[/bold]' initialized in current directory!\n\n"
77
+ panel_content += "Project structure:\n"
78
+ panel_content += " ./\n"
79
+ else:
80
+ panel_content = f"Flash project '[bold]{actual_project_name}[/bold]' created successfully!\n\n"
81
+ panel_content += "Project structure:\n"
82
+ panel_content += f" {actual_project_name}/\n"
83
+
84
+ panel_content += " ├── main.py # Flash Server (FastAPI)\n"
85
+ panel_content += " ├── mothership.py # Mothership endpoint config\n"
86
+ panel_content += " ├── pyproject.toml # Python project config\n"
87
+ panel_content += " ├── workers/\n"
88
+ panel_content += " │ ├── gpu/ # GPU worker\n"
89
+ panel_content += " │ └── cpu/ # CPU worker\n"
90
+ panel_content += " ├── .env.example\n"
91
+ panel_content += " ├── requirements.txt\n"
92
+ panel_content += " └── README.md\n"
93
+
94
+ title = "Project Initialized" if is_current_dir else "Project Created"
95
+ console.print(Panel(panel_content, title=title, expand=False))
96
+
97
+ # Next steps
98
+ console.print("\n[bold]Next steps:[/bold]")
99
+ steps_table = Table(show_header=False, box=None, padding=(0, 1))
100
+ steps_table.add_column("Step", style="bold cyan")
101
+ steps_table.add_column("Description")
102
+
103
+ step_num = 1
104
+ if not is_current_dir:
105
+ steps_table.add_row(f"{step_num}.", f"cd {actual_project_name}")
106
+ step_num += 1
107
+
108
+ steps_table.add_row(f"{step_num}.", "Review and customize mothership.py (optional)")
109
+ step_num += 1
110
+ steps_table.add_row(f"{step_num}.", "pip install -r requirements.txt")
111
+ step_num += 1
112
+ steps_table.add_row(f"{step_num}.", "cp .env.example .env")
113
+ step_num += 1
114
+ steps_table.add_row(f"{step_num}.", "Add your RUNPOD_API_KEY to .env")
115
+ step_num += 1
116
+ steps_table.add_row(f"{step_num}.", "flash run")
117
+
118
+ console.print(steps_table)
119
+
120
+ console.print("\n[bold]Get your API key:[/bold]")
121
+ console.print(" https://docs.runpod.io/get-started/api-keys")
122
+ console.print("\nVisit http://localhost:8888/docs after running")
123
+ console.print("\nCheck out the README.md for more")