supervaizer 0.9.7__py3-none-any.whl → 0.10.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 (58) hide show
  1. supervaizer/__init__.py +11 -2
  2. supervaizer/__version__.py +1 -1
  3. supervaizer/account.py +4 -0
  4. supervaizer/account_service.py +7 -1
  5. supervaizer/admin/routes.py +46 -7
  6. supervaizer/admin/static/js/job-start-form.js +373 -0
  7. supervaizer/admin/templates/agents.html +74 -0
  8. supervaizer/admin/templates/agents_grid.html +5 -3
  9. supervaizer/admin/templates/job_start_test.html +109 -0
  10. supervaizer/admin/templates/navigation.html +11 -1
  11. supervaizer/admin/templates/supervaize_instructions.html +212 -0
  12. supervaizer/agent.py +165 -25
  13. supervaizer/case.py +46 -14
  14. supervaizer/cli.py +248 -8
  15. supervaizer/common.py +45 -4
  16. supervaizer/deploy/__init__.py +16 -0
  17. supervaizer/deploy/cli.py +296 -0
  18. supervaizer/deploy/commands/__init__.py +9 -0
  19. supervaizer/deploy/commands/clean.py +294 -0
  20. supervaizer/deploy/commands/down.py +119 -0
  21. supervaizer/deploy/commands/local.py +460 -0
  22. supervaizer/deploy/commands/plan.py +167 -0
  23. supervaizer/deploy/commands/status.py +169 -0
  24. supervaizer/deploy/commands/up.py +281 -0
  25. supervaizer/deploy/docker.py +370 -0
  26. supervaizer/deploy/driver_factory.py +42 -0
  27. supervaizer/deploy/drivers/__init__.py +39 -0
  28. supervaizer/deploy/drivers/aws_app_runner.py +607 -0
  29. supervaizer/deploy/drivers/base.py +196 -0
  30. supervaizer/deploy/drivers/cloud_run.py +570 -0
  31. supervaizer/deploy/drivers/do_app_platform.py +504 -0
  32. supervaizer/deploy/health.py +404 -0
  33. supervaizer/deploy/state.py +210 -0
  34. supervaizer/deploy/templates/Dockerfile.template +44 -0
  35. supervaizer/deploy/templates/debug_env.py +69 -0
  36. supervaizer/deploy/templates/docker-compose.yml.template +37 -0
  37. supervaizer/deploy/templates/dockerignore.template +66 -0
  38. supervaizer/deploy/templates/entrypoint.sh +20 -0
  39. supervaizer/deploy/utils.py +41 -0
  40. supervaizer/examples/{controller-template.py → controller_template.py} +5 -4
  41. supervaizer/job.py +18 -5
  42. supervaizer/job_service.py +6 -5
  43. supervaizer/parameter.py +61 -1
  44. supervaizer/protocol/__init__.py +2 -2
  45. supervaizer/protocol/a2a/routes.py +1 -1
  46. supervaizer/routes.py +262 -12
  47. supervaizer/server.py +5 -11
  48. supervaizer/utils/__init__.py +16 -0
  49. supervaizer/utils/version_check.py +56 -0
  50. {supervaizer-0.9.7.dist-info → supervaizer-0.10.0.dist-info}/METADATA +105 -34
  51. supervaizer-0.10.0.dist-info/RECORD +76 -0
  52. {supervaizer-0.9.7.dist-info → supervaizer-0.10.0.dist-info}/WHEEL +1 -1
  53. supervaizer/protocol/acp/__init__.py +0 -21
  54. supervaizer/protocol/acp/model.py +0 -198
  55. supervaizer/protocol/acp/routes.py +0 -74
  56. supervaizer-0.9.7.dist-info/RECORD +0 -50
  57. {supervaizer-0.9.7.dist-info → supervaizer-0.10.0.dist-info}/entry_points.txt +0 -0
  58. {supervaizer-0.9.7.dist-info → supervaizer-0.10.0.dist-info}/licenses/LICENSE.md +0 -0
supervaizer/cli.py CHANGED
@@ -4,6 +4,7 @@
4
4
  # If a copy of the MPL was not distributed with this file, you can obtain one at
5
5
  # https://mozilla.org/MPL/2.0/.
6
6
 
7
+ import asyncio
7
8
  import os
8
9
  import shutil
9
10
  import signal
@@ -12,16 +13,94 @@ import sys
12
13
  from typing import Any
13
14
  from pathlib import Path
14
15
  from typing import Optional
16
+ from supervaizer.deploy.cli import deploy_app
15
17
 
16
18
  import typer
17
19
  from rich.console import Console
20
+ from rich.prompt import Confirm
18
21
 
19
22
  from supervaizer.__version__ import VERSION
23
+ from supervaizer.utils.version_check import check_is_latest_version
24
+
25
+ console = Console()
26
+
27
+ # Cache version status to avoid multiple checks
28
+ _version_status: tuple[bool, str | None] | None = None
29
+
30
+
31
+ def _check_version() -> tuple[bool, str | None]:
32
+ """Check version status, caching the result."""
33
+ global _version_status
34
+ if _version_status is None:
35
+ try:
36
+ _version_status = asyncio.run(check_is_latest_version())
37
+ except Exception:
38
+ # On error, assume we're on latest to avoid false positives
39
+ _version_status = (True, None)
40
+ return _version_status
41
+
42
+
43
+ def _display_version_info() -> None:
44
+ """Display version information."""
45
+ is_latest, latest_version = _check_version()
46
+ console.print(f"Supervaizer v{VERSION}")
47
+ if latest_version:
48
+ if is_latest:
49
+ console.print(f"[green]✓[/] Up to date (latest: v{latest_version})")
50
+ else:
51
+ console.print(f"[yellow]⚠[/] Latest available: v{latest_version}")
52
+ console.print("Update with: [bold]pip install --upgrade supervaizer[/]")
53
+ else:
54
+ console.print("(Unable to check for latest version)")
55
+
56
+
57
+ def _display_update_warning() -> None:
58
+ """Display update warning for commands."""
59
+ is_latest, latest_version = _check_version()
60
+ if latest_version and not is_latest:
61
+ console.print(
62
+ f"\n[bold yellow]⚠ Warning:[/] You are running Supervaizer v{VERSION}, "
63
+ f"but v{latest_version} is available.\n"
64
+ f"Update with: [bold]pip install --upgrade supervaizer[/]\n",
65
+ style="yellow",
66
+ )
67
+
20
68
 
21
69
  app = typer.Typer(
22
- help=f"Supervaizer Controller CLI v{VERSION} - Documentation @ https://docs.supervaize.com"
70
+ help=f"Supervaizer Controller CLI v{VERSION} - Documentation @ https://doc.supervaize.com/docs/category/supervaizer-controller"
23
71
  )
24
- console = Console()
72
+
73
+
74
+ @app.callback(invoke_without_command=True)
75
+ def main_callback(
76
+ ctx: typer.Context,
77
+ version: bool = typer.Option(
78
+ False, "--version", "-v", help="Show version information and exit"
79
+ ),
80
+ ) -> None:
81
+ """CLI callback that runs before any command."""
82
+ # Handle --version option
83
+ if version:
84
+ _display_version_info()
85
+ raise typer.Exit()
86
+
87
+ # Show version status in help or as warning for commands
88
+ if ctx.invoked_subcommand is None:
89
+ # For help display, show version status
90
+ _display_version_info()
91
+ else:
92
+ # For actual commands, show warning if not latest
93
+ _display_update_warning()
94
+
95
+
96
+ # Add deploy subcommand
97
+ app.add_typer(
98
+ deploy_app, name="deploy", help="Deploy Supervaizer agents to cloud platforms"
99
+ )
100
+
101
+ # Create scaffold subcommand group
102
+ scaffold_app = typer.Typer(help="Scaffold commands for creating project files")
103
+ app.add_typer(scaffold_app, name="scaffold", invoke_without_command=True)
25
104
 
26
105
 
27
106
  @app.command()
@@ -98,8 +177,45 @@ def start(
98
177
  process.wait()
99
178
 
100
179
 
101
- @app.command()
180
+ def _create_instructions_file(
181
+ output_dir: Path, force: bool = False, silent: bool = False
182
+ ) -> Path:
183
+ """Create supervaize_instructions.html file in the given directory.
184
+
185
+ Args:
186
+ output_dir: Directory where to create the instructions file
187
+ force: If True, overwrite existing file
188
+ silent: If True, don't show warnings if file already exists (just skip)
189
+ """
190
+ instructions_path = output_dir / "supervaize_instructions.html"
191
+
192
+ # Check if file already exists
193
+ if instructions_path.exists() and not force:
194
+ if not silent:
195
+ console.print(
196
+ f"[bold yellow]Warning:[/] {instructions_path} already exists"
197
+ )
198
+ console.print(
199
+ "Use [bold]--force[/] to overwrite it, or run [bold]supervaizer refresh-instructions[/]"
200
+ )
201
+ return instructions_path
202
+
203
+ # Get the path to the admin templates directory
204
+ admin_templates_dir = Path(__file__).parent / "admin" / "templates"
205
+ template_file = admin_templates_dir / "supervaize_instructions.html"
206
+
207
+ if not template_file.exists():
208
+ console.print("[bold red]Error:[/] Template file not found")
209
+ sys.exit(1)
210
+
211
+ # Copy the template file
212
+ shutil.copy(template_file, instructions_path)
213
+ return instructions_path
214
+
215
+
216
+ @scaffold_app.callback(invoke_without_command=True)
102
217
  def scaffold(
218
+ ctx: typer.Context,
103
219
  output_path: str = typer.Option(
104
220
  os.environ.get("SUPERVAIZER_OUTPUT_PATH", "supervaizer_control.py"),
105
221
  help="Path to save the script",
@@ -109,7 +225,10 @@ def scaffold(
109
225
  help="Overwrite existing file",
110
226
  ),
111
227
  ) -> None:
112
- """Create a draft supervaizer_control.py script."""
228
+ """Create a draft supervaizer_control.py script and supervaize_instructions.html."""
229
+ # Only run if no subcommand was invoked
230
+ if ctx.invoked_subcommand is not None:
231
+ return
113
232
  # Check if file already exists
114
233
  if os.path.exists(output_path) and not force:
115
234
  console.print(f"[bold red]Error:[/] {output_path} already exists")
@@ -118,7 +237,7 @@ def scaffold(
118
237
 
119
238
  # Get the path to the examples directory
120
239
  examples_dir = Path(__file__).parent / "examples"
121
- example_file = examples_dir / "controller-template.py"
240
+ example_file = examples_dir / "controller_template.py"
122
241
 
123
242
  if not example_file.exists():
124
243
  console.print("[bold red]Error:[/] Example file not found")
@@ -129,18 +248,139 @@ def scaffold(
129
248
  console.print(
130
249
  f"[bold green]Success:[/] Created an example file at [bold blue]{output_path}[/]"
131
250
  )
251
+
252
+ # Create instructions file in the same directory (silently if it already exists)
253
+ output_dir = Path(output_path).parent
254
+ instructions_path = output_dir / "supervaize_instructions.html"
255
+ instructions_existed = instructions_path.exists()
256
+ _create_instructions_file(output_dir, force=force, silent=True)
257
+ # Only show success message if we actually created the file (didn't exist before or force was used)
258
+ if not instructions_existed or force:
259
+ console.print(
260
+ f"[bold green]Success:[/] Created instructions template at [bold blue]{instructions_path}[/]"
261
+ )
262
+
132
263
  console.print(
133
264
  "1. Copy this file to [bold]supervaizer_control.py[/] and edit it to configure your agent(s)"
134
265
  )
135
266
  console.print(
136
- "2. (Optional) Get your API from [bold]supervaizer.com and setup your environment variables"
267
+ "2. Customize [bold]supervaize_instructions.html[/] to match your agent's documentation"
137
268
  )
138
269
  console.print(
139
- "3. Run [bold]supervaizer start[/] to start the supervaizer controller"
270
+ "3. (Optional) Get your API from [bold]supervaizer.com and setup your environment variables"
140
271
  )
141
- console.print("4. Open [bold]http://localhost:8000/docs[/] to explore the API")
272
+ console.print(
273
+ "4. Run [bold]supervaizer start[/] to start the supervaizer controller"
274
+ )
275
+ console.print("5. Open [bold]http://localhost:8000/docs[/] to explore the API")
142
276
  sys.exit(0)
143
277
 
144
278
 
279
+ @scaffold_app.command(name="instructions")
280
+ def scaffold_instructions(
281
+ control_file: Optional[str] = typer.Option(
282
+ None,
283
+ help="Path to supervaizer_control.py (default: auto-detect)",
284
+ ),
285
+ output_path: Optional[str] = typer.Option(
286
+ None,
287
+ help="Path to save supervaize_instructions.html (default: same directory as control file)",
288
+ ),
289
+ force: bool = typer.Option(
290
+ False,
291
+ help="Overwrite existing file",
292
+ ),
293
+ ) -> None:
294
+ """Create supervaize_instructions.html file."""
295
+ # Determine control file path
296
+ if control_file is None:
297
+ control_file = (
298
+ os.environ.get("SUPERVAIZER_SCRIPT_PATH") or "supervaizer_control.py"
299
+ )
300
+
301
+ control_path = Path(control_file)
302
+
303
+ # Determine output directory
304
+ if output_path is None:
305
+ output_dir = control_path.parent
306
+ instructions_path = output_dir / "supervaize_instructions.html"
307
+ else:
308
+ instructions_path = Path(output_path)
309
+ output_dir = instructions_path.parent
310
+
311
+ # Check if control file exists (informational)
312
+ if not control_path.exists():
313
+ console.print(f"[bold yellow]Warning:[/] Control file {control_file} not found")
314
+ console.print("Creating instructions file anyway...")
315
+
316
+ # Create instructions file
317
+ _create_instructions_file(output_dir, force=force)
318
+ console.print(
319
+ f"[bold green]Success:[/] Created instructions template at [bold blue]{instructions_path}[/]"
320
+ )
321
+ console.print(
322
+ "Customize this file to match your agent's documentation and instructions."
323
+ )
324
+
325
+
326
+ @scaffold_app.command(name="refresh-instructions")
327
+ def refresh_instructions(
328
+ control_file: Optional[str] = typer.Option(
329
+ None,
330
+ help="Path to supervaizer_control.py (default: auto-detect)",
331
+ ),
332
+ output_path: Optional[str] = typer.Option(
333
+ None,
334
+ help="Path to supervaize_instructions.html (default: same directory as control file)",
335
+ ),
336
+ force: bool = typer.Option(
337
+ False,
338
+ help="Skip confirmation prompt",
339
+ ),
340
+ ) -> None:
341
+ """Refresh/update supervaize_instructions.html file."""
342
+ # Determine control file path
343
+ if control_file is None:
344
+ control_file = (
345
+ os.environ.get("SUPERVAIZER_SCRIPT_PATH") or "supervaizer_control.py"
346
+ )
347
+
348
+ control_path = Path(control_file)
349
+
350
+ # Determine output path
351
+ if output_path is None:
352
+ output_dir = control_path.parent
353
+ instructions_path = output_dir / "supervaize_instructions.html"
354
+ else:
355
+ instructions_path = Path(output_path)
356
+
357
+ # Check if instructions file exists
358
+ if instructions_path.exists():
359
+ if not force:
360
+ console.print(
361
+ f"[bold yellow]Warning:[/] {instructions_path} already exists"
362
+ )
363
+ if not Confirm.ask(
364
+ "Delete existing file and create a fresh template?",
365
+ default=False,
366
+ ):
367
+ console.print("[bold]Cancelled.[/]")
368
+ sys.exit(0)
369
+
370
+ # Delete existing file
371
+ instructions_path.unlink()
372
+ console.print(f"[bold]Deleted[/] existing {instructions_path}")
373
+
374
+ # Create new instructions file
375
+ output_dir = instructions_path.parent
376
+ _create_instructions_file(output_dir, force=True)
377
+ console.print(
378
+ f"[bold green]Success:[/] Created fresh instructions template at [bold blue]{instructions_path}[/]"
379
+ )
380
+ console.print(
381
+ "Customize this file to match your agent's documentation and instructions."
382
+ )
383
+
384
+
145
385
  if __name__ == "__main__":
146
386
  app()
supervaizer/common.py CHANGED
@@ -28,19 +28,44 @@ class SvBaseModel(BaseModel):
28
28
  Base model for all Supervaize models.
29
29
  """
30
30
 
31
+ @staticmethod
32
+ def serialize_value(value: Any) -> Any:
33
+ """Recursively serialize values, converting type objects and datetimes to strings."""
34
+ from datetime import datetime
35
+
36
+ if isinstance(value, type):
37
+ # Convert type objects to their string name
38
+ return value.__name__
39
+ elif isinstance(value, datetime):
40
+ # Convert datetime to ISO format string
41
+ return value.isoformat()
42
+ elif isinstance(value, dict):
43
+ # Recursively process dictionaries
44
+ return {k: SvBaseModel.serialize_value(v) for k, v in value.items()}
45
+ elif isinstance(value, (list, tuple)):
46
+ # Recursively process lists and tuples
47
+ return [SvBaseModel.serialize_value(item) for item in value]
48
+ else:
49
+ # Return value as-is for other types
50
+ return value
51
+
31
52
  @property
32
53
  def to_dict(self) -> Dict[str, Any]:
33
54
  """
34
55
  Convert the model to a dictionary.
35
56
 
36
- Note: Using mode="json" to handle datetime serialization.
57
+ Note: Handles datetime serialization and type objects by converting them
58
+ to their string representation.
37
59
  Tested in tests/test_common.test_sv_base_model_json_conversion
38
60
  """
39
- return self.model_dump(mode="json")
61
+ # Use mode="python" to avoid Pydantic's JSON serialization errors with type objects
62
+ # Then post-process to handle type objects and datetimes
63
+ data = self.model_dump(mode="python")
64
+ return self.serialize_value(data)
40
65
 
41
66
  @property
42
67
  def to_json(self) -> str:
43
- return self.model_dump_json()
68
+ return json.dumps(self.to_dict)
44
69
 
45
70
 
46
71
  class ApiResult:
@@ -251,8 +276,24 @@ def decrypt_value(encrypted_value: str, private_key: rsa.RSAPrivateKey) -> str:
251
276
  ValueError: If decryption fails
252
277
  """
253
278
 
279
+ # Basic validation
280
+ if not encrypted_value:
281
+ raise ValueError("Empty encrypted value")
282
+
283
+ # Clean the string
284
+ encrypted_value = encrypted_value.strip()
285
+
254
286
  # Decode base64
255
- combined = base64.b64decode(encrypted_value)
287
+ try:
288
+ combined = base64.b64decode(encrypted_value)
289
+ except Exception as e:
290
+ raise ValueError(f"Base64 decode failed: {str(e)}")
291
+
292
+ # Validate combined data structure
293
+ if len(combined) < 272:
294
+ raise ValueError(
295
+ f"Invalid encrypted data structure: too short ({len(combined)} bytes)"
296
+ )
256
297
 
257
298
  # Extract components - first 256 bytes are RSA encrypted key
258
299
  encrypted_key = combined[:256] # RSA-2048 output is 256 bytes
@@ -0,0 +1,16 @@
1
+ # Copyright (c) 2024-2025 Alain Prasquier - Supervaize.com. All rights reserved.
2
+ #
3
+ # This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
4
+ # If a copy of the MPL was not distributed with this file, you can obtain one at
5
+ # https://mozilla.org/MPL/2.0/.
6
+
7
+ """
8
+ Supervaizer Deployment CLI
9
+
10
+ This module provides automated deployment capabilities for Supervaizer agents
11
+ to cloud platforms including GCP Cloud Run, AWS App Runner, and DigitalOcean App Platform.
12
+ """
13
+
14
+ from supervaizer.__version__ import VERSION
15
+
16
+ __version__ = VERSION
@@ -0,0 +1,296 @@
1
+ # Copyright (c) 2024-2025 Alain Prasquier - Supervaize.com. All rights reserved.
2
+ #
3
+ # This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
4
+ # If a copy of the MPL was not distributed with this file, you can obtain one at
5
+ # https://mozilla.org/MPL/2.0/.
6
+
7
+ """
8
+ Deployment CLI Commands
9
+
10
+ This module contains the main CLI commands for the deploy subcommand.
11
+ """
12
+
13
+ import typer
14
+ from pathlib import Path
15
+ from rich.console import Console
16
+
17
+ from supervaizer.deploy.commands.plan import plan_deployment
18
+ from supervaizer.deploy.commands.up import deploy_up
19
+ from supervaizer.deploy.commands.down import deploy_down
20
+ from supervaizer.deploy.commands.status import deploy_status
21
+ from supervaizer.deploy.commands.local import local_docker
22
+ from supervaizer.deploy.commands.clean import (
23
+ clean_deployment,
24
+ clean_docker_artifacts,
25
+ clean_state_only,
26
+ )
27
+
28
+ console = Console()
29
+
30
+ # Create the deploy subcommand
31
+ deploy_app = typer.Typer(
32
+ name="deploy",
33
+ help="Deploy Supervaizer agents to cloud platforms. Python dependencies must be managed in pyproject.toml file.",
34
+ no_args_is_help=True,
35
+ )
36
+
37
+ # Common parameters
38
+ platform_option = typer.Option(
39
+ None,
40
+ "--platform",
41
+ "-p",
42
+ help="Target platform (cloud-run|aws-app-runner|do-app-platform)",
43
+ )
44
+ name_option = typer.Option(
45
+ ...,
46
+ "--name",
47
+ "-n",
48
+ help="Service name. Required for local command.",
49
+ prompt="Service name (e.g. my-service)",
50
+ )
51
+ env_option = typer.Option("dev", "--env", "-e", help="Environment (dev|staging|prod)")
52
+ region_option = typer.Option(None, "--region", "-r", help="Provider region")
53
+ project_id_option = typer.Option(
54
+ None, "--project-id", help="GCP project / AWS account / DO project"
55
+ )
56
+ verbose_option = typer.Option(False, "--verbose", "-v", help="Show detailed output")
57
+
58
+ # Additional parameters for specific commands
59
+ image_option = typer.Option(None, "--image", help="Container image (registry/repo:tag)")
60
+ port_option = typer.Option(8000, "--port", help="Application port")
61
+ generate_api_key_option = typer.Option(
62
+ False, "--generate-api-key", help="Generate secure API key"
63
+ )
64
+ generate_rsa_option = typer.Option(
65
+ False, "--generate-rsa", help="Generate RSA private key"
66
+ )
67
+ yes_option = typer.Option(False, "--yes", "-y", help="Non-interactive mode")
68
+ no_rollback_option = typer.Option(False, "--no-rollback", help="Keep failed revision")
69
+ timeout_option = typer.Option(300, "--timeout", help="Deployment timeout in seconds")
70
+ docker_files_only_option = typer.Option(
71
+ False, "--docker-files-only", help="Only generate Docker files without running them"
72
+ )
73
+
74
+ controller_file_option = typer.Option(
75
+ "supervaizer_control.py",
76
+ "--controller-file",
77
+ help="Controller file name (default: supervaizer_control.py)",
78
+ )
79
+
80
+ # Clean command options
81
+ force_option = typer.Option(False, "--force", "-f", help="Skip confirmation prompts")
82
+ verbose_option_clean = typer.Option(
83
+ False, "--verbose", "-v", help="Show detailed output"
84
+ )
85
+
86
+
87
+ def _check_pyproject_toml() -> Path:
88
+ """Check if pyproject.toml exists in current directory or parent directories."""
89
+ current_dir = Path.cwd()
90
+
91
+ # Check current directory first
92
+ pyproject_path = current_dir / "pyproject.toml"
93
+ if pyproject_path.exists():
94
+ return current_dir
95
+
96
+ # Check parent directories up to 3 levels
97
+ for _ in range(3):
98
+ current_dir = current_dir.parent
99
+ pyproject_path = current_dir / "pyproject.toml"
100
+ if pyproject_path.exists():
101
+ return current_dir
102
+
103
+ # If not found, show help and exit
104
+ _show_pyproject_toml_help()
105
+ raise typer.Exit(1)
106
+
107
+
108
+ def _show_pyproject_toml_help() -> None:
109
+ """Show help message when pyproject.toml is not found."""
110
+ console.print("[bold red]Error:[/] pyproject.toml file not found")
111
+ console.print(
112
+ "The supervaizer deploy command must be run from a directory containing pyproject.toml"
113
+ )
114
+ console.print("or from a subdirectory of such a directory.")
115
+ console.print("\n[bold]Current directory:[/] " + str(Path.cwd()))
116
+ console.print("\n[bold]Please ensure:[/]")
117
+ console.print(" • You are in the correct project directory")
118
+ console.print(" • The pyproject.toml file exists in the project root")
119
+ console.print(" • Python dependencies are properly defined in pyproject.toml")
120
+ console.print("\n[bold]Available deploy commands:[/]")
121
+ console.print(
122
+ " • [bold]supervaizer deploy local[/] - Test deployment locally using Docker Compose"
123
+ )
124
+ console.print(" • [bold]supervaizer deploy up[/] - Deploy or update the service")
125
+ console.print(
126
+ " • [bold]supervaizer deploy down[/] - Destroy the service and cleanup resources"
127
+ )
128
+ console.print(
129
+ " • [bold]supervaizer deploy status[/] - Show deployment status and health information"
130
+ )
131
+ console.print(
132
+ " • [bold]supervaizer deploy plan[/] - Plan deployment changes without applying them"
133
+ )
134
+ console.print(
135
+ " • [bold]supervaizer deploy clean[/] - Clean up deployment artifacts and generated files"
136
+ )
137
+ console.print(
138
+ "\nUse [bold]supervaizer deploy <command> --help[/] for more information about each command."
139
+ )
140
+
141
+
142
+ @deploy_app.callback()
143
+ def deploy_callback() -> None:
144
+ """Deploy Supervaizer agents to cloud platforms."""
145
+ # This callback is called when no subcommand is provided
146
+ # The no_args_is_help=True will automatically show help
147
+ pass
148
+
149
+
150
+ def _check_platform_required(platform: str, command_name: str) -> None:
151
+ """Check if platform is provided and show helpful error if not."""
152
+ if platform is None:
153
+ console.print("[bold red]Error:[/] --platform is required")
154
+ console.print(
155
+ f"Use [bold]supervaizer deploy {command_name} --help[/] for more information"
156
+ )
157
+ raise typer.Exit(1)
158
+
159
+
160
+ @deploy_app.command(no_args_is_help=True)
161
+ def plan(
162
+ platform: str = platform_option,
163
+ name: str = name_option,
164
+ env: str = env_option,
165
+ region: str = region_option,
166
+ project_id: str = project_id_option,
167
+ verbose: bool = verbose_option,
168
+ ) -> None:
169
+ """Plan deployment changes without applying them."""
170
+ _check_platform_required(platform, "plan")
171
+ source_dir = _check_pyproject_toml()
172
+ plan_deployment(platform, name, env, region, project_id, verbose, source_dir)
173
+
174
+
175
+ @deploy_app.command(no_args_is_help=True)
176
+ def up(
177
+ platform: str = platform_option,
178
+ name: str = name_option,
179
+ env: str = env_option,
180
+ region: str = region_option,
181
+ project_id: str = project_id_option,
182
+ image: str = image_option,
183
+ port: int = port_option,
184
+ generate_api_key: bool = generate_api_key_option,
185
+ generate_rsa: bool = generate_rsa_option,
186
+ yes: bool = yes_option,
187
+ no_rollback: bool = no_rollback_option,
188
+ timeout: int = timeout_option,
189
+ verbose: bool = verbose_option,
190
+ ) -> None:
191
+ """Deploy or update the service."""
192
+ _check_platform_required(platform, "up")
193
+ source_dir = _check_pyproject_toml()
194
+ deploy_up(
195
+ platform,
196
+ name,
197
+ env,
198
+ region,
199
+ project_id,
200
+ image,
201
+ port,
202
+ generate_api_key,
203
+ generate_rsa,
204
+ yes,
205
+ no_rollback,
206
+ timeout,
207
+ verbose,
208
+ source_dir,
209
+ )
210
+
211
+
212
+ @deploy_app.command(no_args_is_help=True)
213
+ def down(
214
+ platform: str = platform_option,
215
+ name: str = name_option,
216
+ env: str = env_option,
217
+ region: str = region_option,
218
+ project_id: str = project_id_option,
219
+ yes: bool = yes_option,
220
+ verbose: bool = verbose_option,
221
+ ) -> None:
222
+ """Destroy the service and cleanup resources."""
223
+ _check_platform_required(platform, "down")
224
+ source_dir = _check_pyproject_toml()
225
+ deploy_down(platform, name, env, region, project_id, yes, verbose, source_dir)
226
+
227
+
228
+ @deploy_app.command(no_args_is_help=True)
229
+ def status(
230
+ platform: str = platform_option,
231
+ name: str = name_option,
232
+ env: str = env_option,
233
+ region: str = region_option,
234
+ project_id: str = project_id_option,
235
+ verbose: bool = verbose_option,
236
+ ) -> None:
237
+ """Show deployment status and health information."""
238
+ _check_platform_required(platform, "status")
239
+ source_dir = _check_pyproject_toml()
240
+ deploy_status(platform, name, env, region, project_id, verbose, source_dir)
241
+
242
+
243
+ @deploy_app.command()
244
+ def local(
245
+ name: str = name_option,
246
+ env: str = env_option,
247
+ port: int = port_option,
248
+ generate_api_key: bool = generate_api_key_option,
249
+ generate_rsa: bool = generate_rsa_option,
250
+ timeout: int = timeout_option,
251
+ verbose: bool = verbose_option,
252
+ docker_files_only: bool = docker_files_only_option,
253
+ controller_file: str = controller_file_option,
254
+ ) -> None:
255
+ """Test deployment locally using Docker Compose. Requires --name."""
256
+ source_dir = _check_pyproject_toml()
257
+ local_docker(
258
+ name,
259
+ env,
260
+ port,
261
+ generate_api_key,
262
+ generate_rsa,
263
+ timeout,
264
+ verbose,
265
+ docker_files_only,
266
+ str(source_dir),
267
+ controller_file,
268
+ )
269
+
270
+
271
+ @deploy_app.command()
272
+ def clean(
273
+ force: bool = force_option,
274
+ verbose: bool = verbose_option_clean,
275
+ docker_only: bool = typer.Option(
276
+ False, "--docker-only", help="Clean only Docker artifacts"
277
+ ),
278
+ state_only: bool = typer.Option(
279
+ False, "--state-only", help="Clean only deployment state"
280
+ ),
281
+ ) -> None:
282
+ """Clean up deployment artifacts and generated files."""
283
+ _check_pyproject_toml()
284
+
285
+ if docker_only and state_only:
286
+ console.print(
287
+ "[bold red]Error:[/] Cannot use both --docker-only and --state-only"
288
+ )
289
+ raise typer.Exit(1)
290
+
291
+ if docker_only:
292
+ clean_docker_artifacts(force=force, verbose=verbose)
293
+ elif state_only:
294
+ clean_state_only(force=force, verbose=verbose)
295
+ else:
296
+ clean_deployment(force=force, verbose=verbose)