plato-sdk-v2 2.8.7__py3-none-any.whl → 2.8.8__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.
- plato/_generated/models/__init__.py +20 -8
- plato/cli/chronos.py +76 -13
- plato/cli/compose.py +1379 -0
- plato/cli/main.py +4 -0
- plato/cli/sandbox.py +62 -14
- plato/cli/session.py +492 -0
- plato/v2/async_/environment.py +572 -0
- plato/v2/async_/session.py +45 -0
- plato/v2/sync/environment.py +6 -0
- plato/v2/sync/sandbox.py +235 -37
- plato/v2/sync/session.py +9 -0
- plato/v2/types.py +46 -15
- {plato_sdk_v2-2.8.7.dist-info → plato_sdk_v2-2.8.8.dist-info}/METADATA +1 -1
- {plato_sdk_v2-2.8.7.dist-info → plato_sdk_v2-2.8.8.dist-info}/RECORD +16 -14
- {plato_sdk_v2-2.8.7.dist-info → plato_sdk_v2-2.8.8.dist-info}/WHEEL +0 -0
- {plato_sdk_v2-2.8.7.dist-info → plato_sdk_v2-2.8.8.dist-info}/entry_points.txt +0 -0
plato/cli/main.py
CHANGED
|
@@ -10,8 +10,10 @@ from dotenv import load_dotenv
|
|
|
10
10
|
|
|
11
11
|
from plato.cli.agent import agent_app
|
|
12
12
|
from plato.cli.chronos import chronos_app
|
|
13
|
+
from plato.cli.compose import app as compose_app
|
|
13
14
|
from plato.cli.pm import pm_app
|
|
14
15
|
from plato.cli.sandbox import sandbox_app
|
|
16
|
+
from plato.cli.session import app as session_app
|
|
15
17
|
from plato.cli.utils import console
|
|
16
18
|
from plato.cli.world import world_app
|
|
17
19
|
|
|
@@ -69,6 +71,8 @@ app = typer.Typer(help="[bold blue]Plato CLI[/bold blue] - Manage Plato environm
|
|
|
69
71
|
|
|
70
72
|
# Register sub-apps
|
|
71
73
|
app.add_typer(sandbox_app, name="sandbox")
|
|
74
|
+
app.add_typer(session_app, name="session")
|
|
75
|
+
app.add_typer(compose_app, name="compose")
|
|
72
76
|
app.add_typer(pm_app, name="pm")
|
|
73
77
|
app.add_typer(agent_app, name="agent")
|
|
74
78
|
app.add_typer(world_app, name="world")
|
plato/cli/sandbox.py
CHANGED
|
@@ -305,10 +305,11 @@ def sandbox_context(
|
|
|
305
305
|
logging.getLogger("httpcore").setLevel(logging.INFO)
|
|
306
306
|
|
|
307
307
|
out = Output(json_output, verbose)
|
|
308
|
+
# Use super_console for status updates (always visible), not the quiet console
|
|
308
309
|
client = SandboxClient(
|
|
309
310
|
working_dir=working_dir,
|
|
310
311
|
api_key=require_api_key(),
|
|
311
|
-
console=out.console,
|
|
312
|
+
console=out.super_console if not json_output else out.console,
|
|
312
313
|
)
|
|
313
314
|
try:
|
|
314
315
|
yield client, out
|
|
@@ -743,34 +744,81 @@ def sandbox_ssh(
|
|
|
743
744
|
ctx: typer.Context,
|
|
744
745
|
ssh_config: SshConfigArg,
|
|
745
746
|
ssh_host: SshHostArg,
|
|
747
|
+
job_id: Annotated[
|
|
748
|
+
str | None,
|
|
749
|
+
typer.Option(
|
|
750
|
+
"--job-id",
|
|
751
|
+
"-J",
|
|
752
|
+
help="Connect to a specific job ID (bypasses .plato/state.json)",
|
|
753
|
+
),
|
|
754
|
+
] = None,
|
|
746
755
|
json_output: JsonArg = False,
|
|
747
756
|
verbose: VerboseArg = False,
|
|
748
757
|
):
|
|
749
758
|
"""SSH to the sandbox VM.
|
|
750
759
|
|
|
751
|
-
Uses .plato/ssh_config from 'start'
|
|
760
|
+
Uses .plato/ssh_config from 'start', or connect directly to a job with -J.
|
|
752
761
|
|
|
753
762
|
NOTE FOR AGENTS: Do not use this command. Instead, use the raw SSH command
|
|
754
763
|
from 'plato sandbox status' which shows: ssh -F .plato/ssh_config sandbox
|
|
755
764
|
|
|
756
765
|
Examples:
|
|
757
|
-
plato sandbox ssh
|
|
766
|
+
plato sandbox ssh # Use saved state
|
|
767
|
+
plato sandbox ssh -J <job-id> # Connect to specific job
|
|
758
768
|
plato sandbox ssh -- -L 8080:localhost:8080
|
|
759
769
|
"""
|
|
760
770
|
import subprocess
|
|
771
|
+
import tempfile
|
|
761
772
|
|
|
762
773
|
with sandbox_context(working_dir, json_output, verbose) as (client, out):
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
+
# If job_id provided, generate SSH config dynamically
|
|
775
|
+
if job_id:
|
|
776
|
+
out.console.print(f"Connecting to job: {job_id}")
|
|
777
|
+
|
|
778
|
+
# Fetch SSH config for the job (generates temp key and adds to VM)
|
|
779
|
+
try:
|
|
780
|
+
ssh_info = client.get_ssh_config_for_job(job_id)
|
|
781
|
+
except Exception as e:
|
|
782
|
+
out.error(f"Failed to get SSH config for job {job_id}: {e}")
|
|
783
|
+
raise typer.Exit(1)
|
|
784
|
+
|
|
785
|
+
# Write temporary SSH config
|
|
786
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".ssh_config", delete=False) as f:
|
|
787
|
+
f.write(ssh_info.config_content)
|
|
788
|
+
temp_config_path = f.name
|
|
789
|
+
|
|
790
|
+
cmd = ["ssh", "-F", temp_config_path, "sandbox"] + (ctx.args or [])
|
|
791
|
+
|
|
792
|
+
try:
|
|
793
|
+
raise typer.Exit(subprocess.run(cmd).returncode)
|
|
794
|
+
except KeyboardInterrupt:
|
|
795
|
+
raise typer.Exit(130) from None
|
|
796
|
+
finally:
|
|
797
|
+
# Clean up temp config file
|
|
798
|
+
try:
|
|
799
|
+
os.unlink(temp_config_path)
|
|
800
|
+
except Exception:
|
|
801
|
+
pass
|
|
802
|
+
# Clean up temp key directory
|
|
803
|
+
try:
|
|
804
|
+
import shutil
|
|
805
|
+
|
|
806
|
+
shutil.rmtree(os.path.dirname(ssh_info.private_key_path))
|
|
807
|
+
except Exception:
|
|
808
|
+
pass
|
|
809
|
+
else:
|
|
810
|
+
# Use saved SSH config
|
|
811
|
+
if not ssh_config:
|
|
812
|
+
out.error("No SSH config found. Run 'plato sandbox start' first or use -J <job-id>.")
|
|
813
|
+
raise typer.Exit(1)
|
|
814
|
+
|
|
815
|
+
config_path = client.working_dir / ssh_config if not Path(ssh_config).is_absolute() else Path(ssh_config)
|
|
816
|
+
cmd = ["ssh", "-F", str(config_path), ssh_host or "sandbox"] + (ctx.args or [])
|
|
817
|
+
|
|
818
|
+
try:
|
|
819
|
+
raise typer.Exit(subprocess.run(cmd).returncode)
|
|
820
|
+
except KeyboardInterrupt:
|
|
821
|
+
raise typer.Exit(130) from None
|
|
774
822
|
|
|
775
823
|
|
|
776
824
|
@sandbox_app.command(name="tunnel")
|
plato/cli/session.py
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
"""Session CLI commands for Plato - manage multi-environment sessions."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Annotated
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
from rich.text import Text
|
|
12
|
+
|
|
13
|
+
from plato.cli.utils import require_api_key
|
|
14
|
+
from plato.v2.sync.client import Plato
|
|
15
|
+
from plato.v2.types import Env
|
|
16
|
+
|
|
17
|
+
app = typer.Typer(
|
|
18
|
+
name="session",
|
|
19
|
+
help="Manage sessions with multiple environments.",
|
|
20
|
+
no_args_is_help=True,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
console = Console()
|
|
24
|
+
|
|
25
|
+
# State file location
|
|
26
|
+
STATE_DIR = Path(".plato")
|
|
27
|
+
STATE_FILE = STATE_DIR / "session.json"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def save_state(state: dict) -> None:
|
|
31
|
+
"""Save session state to .plato/session.json."""
|
|
32
|
+
STATE_DIR.mkdir(exist_ok=True)
|
|
33
|
+
STATE_FILE.write_text(json.dumps(state, indent=2))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def load_state() -> dict | None:
|
|
37
|
+
"""Load session state from .plato/session.json."""
|
|
38
|
+
if not STATE_FILE.exists():
|
|
39
|
+
return None
|
|
40
|
+
try:
|
|
41
|
+
return json.loads(STATE_FILE.read_text())
|
|
42
|
+
except Exception:
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def clear_state() -> None:
|
|
47
|
+
"""Clear session state."""
|
|
48
|
+
if STATE_FILE.exists():
|
|
49
|
+
STATE_FILE.unlink()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def make_status_table(session, title: str = "Session Status") -> Table:
|
|
53
|
+
"""Create a rich table showing session/environment status."""
|
|
54
|
+
table = Table(title=title, show_header=True, header_style="bold cyan")
|
|
55
|
+
table.add_column("Alias", style="bold")
|
|
56
|
+
table.add_column("Simulator")
|
|
57
|
+
table.add_column("Status")
|
|
58
|
+
table.add_column("Job ID", style="dim")
|
|
59
|
+
table.add_column("Public URL", style="blue underline")
|
|
60
|
+
|
|
61
|
+
for env in session.envs:
|
|
62
|
+
status_style = "green" if env.status == "running" else "yellow"
|
|
63
|
+
table.add_row(
|
|
64
|
+
env.alias or "-",
|
|
65
|
+
env.simulator or "-",
|
|
66
|
+
Text(env.status or "unknown", style=status_style),
|
|
67
|
+
env.job_id[:12] + "..." if env.job_id and len(env.job_id) > 12 else (env.job_id or "-"),
|
|
68
|
+
env.public_url or "-",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
return table
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@app.command("start")
|
|
75
|
+
def start(
|
|
76
|
+
# Environment specification options
|
|
77
|
+
sim: Annotated[
|
|
78
|
+
list[str] | None,
|
|
79
|
+
typer.Option(
|
|
80
|
+
"--sim",
|
|
81
|
+
"-s",
|
|
82
|
+
help="Simulator to start (can specify multiple). Format: 'name' or 'name:tag' or 'name:tag@dataset'",
|
|
83
|
+
),
|
|
84
|
+
] = None,
|
|
85
|
+
artifact: Annotated[
|
|
86
|
+
list[str] | None,
|
|
87
|
+
typer.Option(
|
|
88
|
+
"--artifact",
|
|
89
|
+
"-a",
|
|
90
|
+
help="Artifact ID to start (can specify multiple)",
|
|
91
|
+
),
|
|
92
|
+
] = None,
|
|
93
|
+
task: Annotated[
|
|
94
|
+
str | None,
|
|
95
|
+
typer.Option(
|
|
96
|
+
"--task",
|
|
97
|
+
"-t",
|
|
98
|
+
help="Task ID to create session from",
|
|
99
|
+
),
|
|
100
|
+
] = None,
|
|
101
|
+
n: Annotated[
|
|
102
|
+
int,
|
|
103
|
+
typer.Option(
|
|
104
|
+
"--n",
|
|
105
|
+
"-n",
|
|
106
|
+
help="Number of instances (only with single --sim)",
|
|
107
|
+
),
|
|
108
|
+
] = 1,
|
|
109
|
+
# Resource options
|
|
110
|
+
timeout: Annotated[
|
|
111
|
+
int,
|
|
112
|
+
typer.Option(
|
|
113
|
+
"--timeout",
|
|
114
|
+
help="VM timeout in seconds",
|
|
115
|
+
),
|
|
116
|
+
] = 1800,
|
|
117
|
+
no_network: Annotated[
|
|
118
|
+
bool,
|
|
119
|
+
typer.Option(
|
|
120
|
+
"--no-network",
|
|
121
|
+
help="Don't connect VMs to network",
|
|
122
|
+
),
|
|
123
|
+
] = False,
|
|
124
|
+
# Output options
|
|
125
|
+
json_output: Annotated[
|
|
126
|
+
bool,
|
|
127
|
+
typer.Option(
|
|
128
|
+
"--json",
|
|
129
|
+
"-j",
|
|
130
|
+
help="Output as JSON",
|
|
131
|
+
),
|
|
132
|
+
] = False,
|
|
133
|
+
wait: Annotated[
|
|
134
|
+
bool,
|
|
135
|
+
typer.Option(
|
|
136
|
+
"--wait/--no-wait",
|
|
137
|
+
"-w",
|
|
138
|
+
help="Wait for all environments to be ready",
|
|
139
|
+
),
|
|
140
|
+
] = True,
|
|
141
|
+
) -> None:
|
|
142
|
+
"""Start a new session with one or more environments.
|
|
143
|
+
|
|
144
|
+
Examples:
|
|
145
|
+
# Single simulator
|
|
146
|
+
plato session start --sim espocrm
|
|
147
|
+
|
|
148
|
+
# Multiple simulators
|
|
149
|
+
plato session start --sim espocrm --sim gitea
|
|
150
|
+
|
|
151
|
+
# With specific tags
|
|
152
|
+
plato session start --sim espocrm:staging --sim gitea:latest
|
|
153
|
+
|
|
154
|
+
# With dataset
|
|
155
|
+
plato session start --sim espocrm:latest@blank
|
|
156
|
+
|
|
157
|
+
# Multiple instances of same simulator
|
|
158
|
+
plato session start --sim espocrm -n 3
|
|
159
|
+
|
|
160
|
+
# From artifact
|
|
161
|
+
plato session start --artifact abc123
|
|
162
|
+
|
|
163
|
+
# From task
|
|
164
|
+
plato session start --task task-id-123
|
|
165
|
+
"""
|
|
166
|
+
api_key = require_api_key()
|
|
167
|
+
|
|
168
|
+
# Validate inputs
|
|
169
|
+
if task and (sim or artifact):
|
|
170
|
+
console.print("[red]Error:[/red] Cannot specify --task with --sim or --artifact")
|
|
171
|
+
raise typer.Exit(1)
|
|
172
|
+
|
|
173
|
+
if not task and not sim and not artifact:
|
|
174
|
+
console.print("[red]Error:[/red] Must specify --sim, --artifact, or --task")
|
|
175
|
+
raise typer.Exit(1)
|
|
176
|
+
|
|
177
|
+
if n > 1 and (len(sim or []) > 1 or artifact):
|
|
178
|
+
console.print("[red]Error:[/red] --n only works with a single --sim")
|
|
179
|
+
raise typer.Exit(1)
|
|
180
|
+
|
|
181
|
+
# Build environment list
|
|
182
|
+
envs = []
|
|
183
|
+
|
|
184
|
+
if sim:
|
|
185
|
+
for i, s in enumerate(sim):
|
|
186
|
+
# Parse format: name, name:tag, or name:tag@dataset
|
|
187
|
+
dataset = None
|
|
188
|
+
tag = "latest"
|
|
189
|
+
|
|
190
|
+
if "@" in s:
|
|
191
|
+
s, dataset = s.rsplit("@", 1)
|
|
192
|
+
if ":" in s:
|
|
193
|
+
name, tag = s.split(":", 1)
|
|
194
|
+
else:
|
|
195
|
+
name = s
|
|
196
|
+
|
|
197
|
+
# Handle --n for multiple instances
|
|
198
|
+
count = n if len(sim) == 1 else 1
|
|
199
|
+
for j in range(count):
|
|
200
|
+
alias = f"{name}-{j}" if count > 1 else (name if len(sim) == 1 else f"{name}-{i}")
|
|
201
|
+
envs.append(Env.simulator(name, tag=tag, dataset=dataset, alias=alias))
|
|
202
|
+
|
|
203
|
+
if artifact:
|
|
204
|
+
for i, a in enumerate(artifact):
|
|
205
|
+
alias = f"artifact-{i}" if len(artifact) > 1 else "artifact"
|
|
206
|
+
envs.append(Env.artifact(a, alias=alias))
|
|
207
|
+
|
|
208
|
+
# Create session
|
|
209
|
+
plato = Plato(api_key=api_key)
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
if not json_output:
|
|
213
|
+
console.print(f"\n[bold]Starting session with {len(envs) if envs else 'task'} environment(s)...[/bold]\n")
|
|
214
|
+
|
|
215
|
+
if envs:
|
|
216
|
+
for env in envs:
|
|
217
|
+
if hasattr(env, "simulator"):
|
|
218
|
+
console.print(
|
|
219
|
+
f" • {env.alias}: {env.simulator}:{env.tag}"
|
|
220
|
+
+ (f" (dataset: {env.dataset})" if env.dataset else "")
|
|
221
|
+
)
|
|
222
|
+
else:
|
|
223
|
+
console.print(f" • {env.alias}: artifact {env.artifact_id}")
|
|
224
|
+
console.print()
|
|
225
|
+
|
|
226
|
+
start_time = time.time()
|
|
227
|
+
|
|
228
|
+
with console.status("[bold green]Creating session..."):
|
|
229
|
+
if task:
|
|
230
|
+
session = plato.sessions.create(task=task, timeout=timeout, connect_network=not no_network)
|
|
231
|
+
else:
|
|
232
|
+
session = plato.sessions.create(envs=envs, timeout=timeout, connect_network=not no_network)
|
|
233
|
+
|
|
234
|
+
elapsed = time.time() - start_time
|
|
235
|
+
|
|
236
|
+
if json_output:
|
|
237
|
+
output = {
|
|
238
|
+
"session_id": session.session_id,
|
|
239
|
+
"environments": [
|
|
240
|
+
{
|
|
241
|
+
"alias": env.alias,
|
|
242
|
+
"job_id": env.job_id,
|
|
243
|
+
"simulator": env.simulator,
|
|
244
|
+
"status": env.status,
|
|
245
|
+
"public_url": env.public_url,
|
|
246
|
+
}
|
|
247
|
+
for env in session.envs
|
|
248
|
+
],
|
|
249
|
+
}
|
|
250
|
+
print(json.dumps(output, indent=2))
|
|
251
|
+
else:
|
|
252
|
+
console.print(f"[green]✓[/green] Session created in {elapsed:.1f}s\n")
|
|
253
|
+
console.print(f"[bold]Session ID:[/bold] {session.session_id}\n")
|
|
254
|
+
console.print(make_status_table(session))
|
|
255
|
+
console.print()
|
|
256
|
+
|
|
257
|
+
# Show helpful commands
|
|
258
|
+
console.print("[dim]Helpful commands:[/dim]")
|
|
259
|
+
console.print(" [cyan]plato session status[/cyan] - Check environment status")
|
|
260
|
+
console.print(" [cyan]plato session stop[/cyan] - Stop the session")
|
|
261
|
+
console.print()
|
|
262
|
+
|
|
263
|
+
# Save state
|
|
264
|
+
save_state(
|
|
265
|
+
{
|
|
266
|
+
"session_id": session.session_id,
|
|
267
|
+
"environments": [
|
|
268
|
+
{
|
|
269
|
+
"alias": env.alias,
|
|
270
|
+
"job_id": env.job_id,
|
|
271
|
+
"simulator": env.simulator,
|
|
272
|
+
"public_url": env.public_url,
|
|
273
|
+
}
|
|
274
|
+
for env in session.envs
|
|
275
|
+
],
|
|
276
|
+
}
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
# Start heartbeat in background
|
|
280
|
+
session.start_heartbeat()
|
|
281
|
+
|
|
282
|
+
except Exception as e:
|
|
283
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
284
|
+
raise typer.Exit(1)
|
|
285
|
+
finally:
|
|
286
|
+
plato.close()
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
@app.command("status")
|
|
290
|
+
def status(
|
|
291
|
+
session_id: Annotated[
|
|
292
|
+
str | None,
|
|
293
|
+
typer.Option(
|
|
294
|
+
"--session-id",
|
|
295
|
+
"-s",
|
|
296
|
+
help="Session ID (uses saved state if not provided)",
|
|
297
|
+
),
|
|
298
|
+
] = None,
|
|
299
|
+
json_output: Annotated[
|
|
300
|
+
bool,
|
|
301
|
+
typer.Option(
|
|
302
|
+
"--json",
|
|
303
|
+
"-j",
|
|
304
|
+
help="Output as JSON",
|
|
305
|
+
),
|
|
306
|
+
] = False,
|
|
307
|
+
) -> None:
|
|
308
|
+
"""Show status of the current session and all environments."""
|
|
309
|
+
from plato._generated.api.v2.sessions import state as sessions_state
|
|
310
|
+
|
|
311
|
+
api_key = require_api_key()
|
|
312
|
+
|
|
313
|
+
# Get session ID and env info from state if not provided
|
|
314
|
+
saved_state = load_state()
|
|
315
|
+
if not session_id:
|
|
316
|
+
if not saved_state:
|
|
317
|
+
console.print("[red]Error:[/red] No session found. Run `plato session start` first or provide --session-id")
|
|
318
|
+
raise typer.Exit(1)
|
|
319
|
+
session_id = saved_state.get("session_id")
|
|
320
|
+
|
|
321
|
+
if not session_id:
|
|
322
|
+
console.print("[red]Error:[/red] No session ID found")
|
|
323
|
+
raise typer.Exit(1)
|
|
324
|
+
|
|
325
|
+
plato = Plato(api_key=api_key)
|
|
326
|
+
|
|
327
|
+
try:
|
|
328
|
+
# Get session state from API
|
|
329
|
+
state_response = sessions_state.sync(
|
|
330
|
+
client=plato._http,
|
|
331
|
+
session_id=session_id,
|
|
332
|
+
x_api_key=api_key,
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
# Build env info from saved state and API response
|
|
336
|
+
saved_envs = {e.get("job_id"): e for e in (saved_state or {}).get("environments", [])}
|
|
337
|
+
|
|
338
|
+
if json_output:
|
|
339
|
+
output = {
|
|
340
|
+
"session_id": session_id,
|
|
341
|
+
"environments": [
|
|
342
|
+
{
|
|
343
|
+
"job_id": job_id,
|
|
344
|
+
"alias": saved_envs.get(job_id, {}).get("alias", job_id[:8]),
|
|
345
|
+
"simulator": saved_envs.get(job_id, {}).get("simulator", "-"),
|
|
346
|
+
"status": result.status if hasattr(result, "status") else "unknown",
|
|
347
|
+
}
|
|
348
|
+
for job_id, result in state_response.results.items()
|
|
349
|
+
],
|
|
350
|
+
}
|
|
351
|
+
print(json.dumps(output, indent=2))
|
|
352
|
+
else:
|
|
353
|
+
console.print(f"\n[bold]Session ID:[/bold] {session_id}\n")
|
|
354
|
+
|
|
355
|
+
table = Table(title="Session Status", show_header=True, header_style="bold cyan")
|
|
356
|
+
table.add_column("Alias", style="bold")
|
|
357
|
+
table.add_column("Simulator")
|
|
358
|
+
table.add_column("Job ID", style="dim")
|
|
359
|
+
table.add_column("Status")
|
|
360
|
+
|
|
361
|
+
for job_id, result in state_response.results.items():
|
|
362
|
+
env_info = saved_envs.get(job_id, {})
|
|
363
|
+
status_val = getattr(result, "status", "unknown") if result else "unknown"
|
|
364
|
+
status_style = "green" if status_val == "running" else "yellow"
|
|
365
|
+
table.add_row(
|
|
366
|
+
env_info.get("alias", job_id[:8]),
|
|
367
|
+
env_info.get("simulator", "-"),
|
|
368
|
+
job_id[:12] + "..." if len(job_id) > 12 else job_id,
|
|
369
|
+
Text(status_val, style=status_style),
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
console.print(table)
|
|
373
|
+
console.print()
|
|
374
|
+
|
|
375
|
+
except Exception as e:
|
|
376
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
377
|
+
raise typer.Exit(1)
|
|
378
|
+
finally:
|
|
379
|
+
plato.close()
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
@app.command("stop")
|
|
383
|
+
def stop(
|
|
384
|
+
session_id: Annotated[
|
|
385
|
+
str | None,
|
|
386
|
+
typer.Option(
|
|
387
|
+
"--session-id",
|
|
388
|
+
"-s",
|
|
389
|
+
help="Session ID (uses saved state if not provided)",
|
|
390
|
+
),
|
|
391
|
+
] = None,
|
|
392
|
+
force: Annotated[
|
|
393
|
+
bool,
|
|
394
|
+
typer.Option(
|
|
395
|
+
"--force",
|
|
396
|
+
"-f",
|
|
397
|
+
help="Force stop without confirmation",
|
|
398
|
+
),
|
|
399
|
+
] = False,
|
|
400
|
+
) -> None:
|
|
401
|
+
"""Stop the current session and all environments."""
|
|
402
|
+
from plato._generated.api.v2.sessions import close as sessions_close
|
|
403
|
+
|
|
404
|
+
api_key = require_api_key()
|
|
405
|
+
|
|
406
|
+
# Get session ID from state if not provided
|
|
407
|
+
if not session_id:
|
|
408
|
+
state = load_state()
|
|
409
|
+
if not state:
|
|
410
|
+
console.print("[red]Error:[/red] No session found. Run `plato session start` first or provide --session-id")
|
|
411
|
+
raise typer.Exit(1)
|
|
412
|
+
session_id = state.get("session_id")
|
|
413
|
+
|
|
414
|
+
if not session_id:
|
|
415
|
+
console.print("[red]Error:[/red] No session ID found")
|
|
416
|
+
raise typer.Exit(1)
|
|
417
|
+
|
|
418
|
+
if not force:
|
|
419
|
+
confirm = typer.confirm(f"Stop session {session_id}?")
|
|
420
|
+
if not confirm:
|
|
421
|
+
console.print("Cancelled")
|
|
422
|
+
raise typer.Exit(0)
|
|
423
|
+
|
|
424
|
+
plato = Plato(api_key=api_key)
|
|
425
|
+
|
|
426
|
+
try:
|
|
427
|
+
with console.status("[bold red]Stopping session..."):
|
|
428
|
+
sessions_close.sync(
|
|
429
|
+
client=plato._http,
|
|
430
|
+
session_id=session_id,
|
|
431
|
+
x_api_key=api_key,
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
console.print(f"[green]✓[/green] Session {session_id} stopped")
|
|
435
|
+
clear_state()
|
|
436
|
+
|
|
437
|
+
except Exception as e:
|
|
438
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
439
|
+
raise typer.Exit(1)
|
|
440
|
+
finally:
|
|
441
|
+
plato.close()
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
@app.command("list")
|
|
445
|
+
def list_sessions(
|
|
446
|
+
json_output: Annotated[
|
|
447
|
+
bool,
|
|
448
|
+
typer.Option(
|
|
449
|
+
"--json",
|
|
450
|
+
"-j",
|
|
451
|
+
help="Output as JSON",
|
|
452
|
+
),
|
|
453
|
+
] = False,
|
|
454
|
+
) -> None:
|
|
455
|
+
"""List all active sessions."""
|
|
456
|
+
require_api_key() # Validate API key is set
|
|
457
|
+
|
|
458
|
+
# Just show saved state for now
|
|
459
|
+
state = load_state()
|
|
460
|
+
|
|
461
|
+
if not state:
|
|
462
|
+
if json_output:
|
|
463
|
+
print(json.dumps({"sessions": []}))
|
|
464
|
+
else:
|
|
465
|
+
console.print("[dim]No active session saved locally.[/dim]")
|
|
466
|
+
console.print("[dim]Use `plato session start` to create one.[/dim]")
|
|
467
|
+
return
|
|
468
|
+
|
|
469
|
+
if json_output:
|
|
470
|
+
print(json.dumps({"sessions": [state]}))
|
|
471
|
+
else:
|
|
472
|
+
console.print(f"\n[bold]Active Session:[/bold] {state.get('session_id')}")
|
|
473
|
+
console.print(f"[dim]Environments: {len(state.get('environments', []))}[/dim]\n")
|
|
474
|
+
|
|
475
|
+
table = Table(show_header=True, header_style="bold cyan")
|
|
476
|
+
table.add_column("Alias")
|
|
477
|
+
table.add_column("Simulator")
|
|
478
|
+
table.add_column("Job ID", style="dim")
|
|
479
|
+
|
|
480
|
+
for env in state.get("environments", []):
|
|
481
|
+
table.add_row(
|
|
482
|
+
env.get("alias", "-"),
|
|
483
|
+
env.get("simulator", "-"),
|
|
484
|
+
env.get("job_id", "-")[:12] + "..." if env.get("job_id") else "-",
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
console.print(table)
|
|
488
|
+
console.print()
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
if __name__ == "__main__":
|
|
492
|
+
app()
|