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.
- supervaizer/__init__.py +11 -2
- supervaizer/__version__.py +1 -1
- supervaizer/account.py +4 -0
- supervaizer/account_service.py +7 -1
- supervaizer/admin/routes.py +46 -7
- supervaizer/admin/static/js/job-start-form.js +373 -0
- supervaizer/admin/templates/agents.html +74 -0
- supervaizer/admin/templates/agents_grid.html +5 -3
- supervaizer/admin/templates/job_start_test.html +109 -0
- supervaizer/admin/templates/navigation.html +11 -1
- supervaizer/admin/templates/supervaize_instructions.html +212 -0
- supervaizer/agent.py +165 -25
- supervaizer/case.py +46 -14
- supervaizer/cli.py +248 -8
- supervaizer/common.py +45 -4
- supervaizer/deploy/__init__.py +16 -0
- supervaizer/deploy/cli.py +296 -0
- supervaizer/deploy/commands/__init__.py +9 -0
- supervaizer/deploy/commands/clean.py +294 -0
- supervaizer/deploy/commands/down.py +119 -0
- supervaizer/deploy/commands/local.py +460 -0
- supervaizer/deploy/commands/plan.py +167 -0
- supervaizer/deploy/commands/status.py +169 -0
- supervaizer/deploy/commands/up.py +281 -0
- supervaizer/deploy/docker.py +370 -0
- supervaizer/deploy/driver_factory.py +42 -0
- supervaizer/deploy/drivers/__init__.py +39 -0
- supervaizer/deploy/drivers/aws_app_runner.py +607 -0
- supervaizer/deploy/drivers/base.py +196 -0
- supervaizer/deploy/drivers/cloud_run.py +570 -0
- supervaizer/deploy/drivers/do_app_platform.py +504 -0
- supervaizer/deploy/health.py +404 -0
- supervaizer/deploy/state.py +210 -0
- supervaizer/deploy/templates/Dockerfile.template +44 -0
- supervaizer/deploy/templates/debug_env.py +69 -0
- supervaizer/deploy/templates/docker-compose.yml.template +37 -0
- supervaizer/deploy/templates/dockerignore.template +66 -0
- supervaizer/deploy/templates/entrypoint.sh +20 -0
- supervaizer/deploy/utils.py +41 -0
- supervaizer/examples/{controller-template.py → controller_template.py} +5 -4
- supervaizer/job.py +18 -5
- supervaizer/job_service.py +6 -5
- supervaizer/parameter.py +61 -1
- supervaizer/protocol/__init__.py +2 -2
- supervaizer/protocol/a2a/routes.py +1 -1
- supervaizer/routes.py +262 -12
- supervaizer/server.py +5 -11
- supervaizer/utils/__init__.py +16 -0
- supervaizer/utils/version_check.py +56 -0
- {supervaizer-0.9.7.dist-info → supervaizer-0.10.0.dist-info}/METADATA +105 -34
- supervaizer-0.10.0.dist-info/RECORD +76 -0
- {supervaizer-0.9.7.dist-info → supervaizer-0.10.0.dist-info}/WHEEL +1 -1
- supervaizer/protocol/acp/__init__.py +0 -21
- supervaizer/protocol/acp/model.py +0 -198
- supervaizer/protocol/acp/routes.py +0 -74
- supervaizer-0.9.7.dist-info/RECORD +0 -50
- {supervaizer-0.9.7.dist-info → supervaizer-0.10.0.dist-info}/entry_points.txt +0 -0
- {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://
|
|
70
|
+
help=f"Supervaizer Controller CLI v{VERSION} - Documentation @ https://doc.supervaize.com/docs/category/supervaizer-controller"
|
|
23
71
|
)
|
|
24
|
-
|
|
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
|
-
|
|
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 / "
|
|
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.
|
|
267
|
+
"2. Customize [bold]supervaize_instructions.html[/] to match your agent's documentation"
|
|
137
268
|
)
|
|
138
269
|
console.print(
|
|
139
|
-
"3.
|
|
270
|
+
"3. (Optional) Get your API from [bold]supervaizer.com and setup your environment variables"
|
|
140
271
|
)
|
|
141
|
-
console.print(
|
|
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:
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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)
|