runtime-sdk 0.4.28__tar.gz → 0.4.30__tar.gz
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.
- {runtime_sdk-0.4.28 → runtime_sdk-0.4.30}/PKG-INFO +1 -1
- {runtime_sdk-0.4.28 → runtime_sdk-0.4.30}/pyproject.toml +1 -1
- {runtime_sdk-0.4.28 → runtime_sdk-0.4.30}/scripts/runtime_sdk/cli.py +200 -70
- {runtime_sdk-0.4.28 → runtime_sdk-0.4.30}/scripts/runtime_sdk/client.py +70 -9
- {runtime_sdk-0.4.28 → runtime_sdk-0.4.30}/scripts/runtime_sdk.egg-info/PKG-INFO +1 -1
- {runtime_sdk-0.4.28 → runtime_sdk-0.4.30}/README.md +0 -0
- {runtime_sdk-0.4.28 → runtime_sdk-0.4.30}/scripts/runtime_sdk/__init__.py +0 -0
- {runtime_sdk-0.4.28 → runtime_sdk-0.4.30}/scripts/runtime_sdk/config.py +0 -0
- {runtime_sdk-0.4.28 → runtime_sdk-0.4.30}/scripts/runtime_sdk.egg-info/SOURCES.txt +0 -0
- {runtime_sdk-0.4.28 → runtime_sdk-0.4.30}/scripts/runtime_sdk.egg-info/dependency_links.txt +0 -0
- {runtime_sdk-0.4.28 → runtime_sdk-0.4.30}/scripts/runtime_sdk.egg-info/entry_points.txt +0 -0
- {runtime_sdk-0.4.28 → runtime_sdk-0.4.30}/scripts/runtime_sdk.egg-info/requires.txt +0 -0
- {runtime_sdk-0.4.28 → runtime_sdk-0.4.30}/scripts/runtime_sdk.egg-info/top_level.txt +0 -0
- {runtime_sdk-0.4.28 → runtime_sdk-0.4.30}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: runtime-sdk
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.30
|
|
4
4
|
Summary: Runtime Python SDK and CLI
|
|
5
5
|
Project-URL: Repository, https://github.com/The-Money-Company-Limited/runtimevm
|
|
6
6
|
Project-URL: Issues, https://github.com/The-Money-Company-Limited/runtimevm/issues
|
|
@@ -12,6 +12,7 @@ import shlex
|
|
|
12
12
|
import shutil
|
|
13
13
|
import signal
|
|
14
14
|
import socket
|
|
15
|
+
import struct
|
|
15
16
|
import subprocess
|
|
16
17
|
import sys
|
|
17
18
|
import threading
|
|
@@ -220,9 +221,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
220
221
|
default=None,
|
|
221
222
|
help="Subdomain name (e.g. redsox → redsox.runruntime.dev). Random if skipped.",
|
|
222
223
|
)
|
|
223
|
-
create_cmd.add_argument("--command", dest="startup_command", help="
|
|
224
|
-
create_cmd.add_argument("--cwd", help="Working directory for the
|
|
225
|
-
create_cmd.add_argument("--port", type=int, help="App port to publish for
|
|
224
|
+
create_cmd.add_argument("--command", dest="startup_command", help="Initial service command to save on create")
|
|
225
|
+
create_cmd.add_argument("--cwd", help="Working directory for the initial service command")
|
|
226
|
+
create_cmd.add_argument("--port", type=int, help="App port to publish for the initial service")
|
|
226
227
|
list_cmd = subparsers.add_parser("list", aliases=["ls"], help="List computers")
|
|
227
228
|
list_cmd.add_argument(
|
|
228
229
|
"-w",
|
|
@@ -282,8 +283,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
282
283
|
)
|
|
283
284
|
publish_cmd.add_argument("id", nargs="?", default=None, help="Computer ID (hostname)")
|
|
284
285
|
publish_cmd.add_argument("port", nargs="?", type=int, default=None, help="Local app port")
|
|
286
|
+
publish_cmd.add_argument("publish_command", nargs="*", help="Command after --")
|
|
285
287
|
|
|
286
|
-
startup_cmd = subparsers.add_parser("startup", help=
|
|
288
|
+
startup_cmd = subparsers.add_parser("startup", help=argparse.SUPPRESS)
|
|
287
289
|
startup_subparsers = startup_cmd.add_subparsers(dest="startup_action", required=True)
|
|
288
290
|
startup_show = startup_subparsers.add_parser("show", help="Show durable startup config")
|
|
289
291
|
startup_show.add_argument("id", nargs="?", default=None, help="Computer ID (hostname)")
|
|
@@ -297,8 +299,22 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
297
299
|
|
|
298
300
|
service_cmd = subparsers.add_parser("service", help="Manage the durable published app service")
|
|
299
301
|
service_subparsers = service_cmd.add_subparsers(dest="service_action", required=True)
|
|
302
|
+
service_run = service_subparsers.add_parser("run", help="Start and publish a durable service command")
|
|
303
|
+
service_run.add_argument("id", nargs="?", default=None, help="Computer ID (hostname)")
|
|
304
|
+
service_run.add_argument("--port", required=True, type=int, help="Service port")
|
|
305
|
+
service_run.add_argument("--cwd", help="Working directory for the service command")
|
|
306
|
+
service_run.add_argument("service_command", nargs="*", help="Command after --")
|
|
300
307
|
service_show = service_subparsers.add_parser("show", help="Show the durable published app service")
|
|
301
308
|
service_show.add_argument("id", nargs="?", default=None, help="Computer ID (hostname)")
|
|
309
|
+
service_status = service_subparsers.add_parser("status", help="Show durable service status")
|
|
310
|
+
service_status.add_argument("id", nargs="?", default=None, help="Computer ID (hostname)")
|
|
311
|
+
service_logs = service_subparsers.add_parser("logs", help="Stream durable service logs")
|
|
312
|
+
service_logs.add_argument("id", nargs="?", default=None, help="Computer ID (hostname)")
|
|
313
|
+
service_logs.add_argument("--follow", action="store_true", help="Follow live logs")
|
|
314
|
+
service_restart = service_subparsers.add_parser("restart", help="Restart the durable service")
|
|
315
|
+
service_restart.add_argument("id", nargs="?", default=None, help="Computer ID (hostname)")
|
|
316
|
+
service_stop = service_subparsers.add_parser("stop", help="Stop the durable service but keep its config")
|
|
317
|
+
service_stop.add_argument("id", nargs="?", default=None, help="Computer ID (hostname)")
|
|
302
318
|
service_clear = service_subparsers.add_parser("clear", help="Clear the durable published app service")
|
|
303
319
|
service_clear.add_argument("id", nargs="?", default=None, help="Computer ID (hostname)")
|
|
304
320
|
|
|
@@ -345,7 +361,7 @@ _HELP_SECTIONS: tuple[tuple[str, tuple[tuple[str, str], ...]], ...] = (
|
|
|
345
361
|
("start [id]", "Wake a cold computer"),
|
|
346
362
|
("enter [id]", "Enter a computer's console (TTY)"),
|
|
347
363
|
("run [id] <cmd>", "Run a one-shot command"),
|
|
348
|
-
("publish [id]
|
|
364
|
+
("publish [id] <port> -- <cmd>", "Run and publish a durable service command"),
|
|
349
365
|
("delete, rm [id]", "Delete a computer (typed-name confirm; --force / -y to skip)"),
|
|
350
366
|
),
|
|
351
367
|
),
|
|
@@ -359,10 +375,11 @@ _HELP_SECTIONS: tuple[tuple[str, tuple[tuple[str, str], ...]], ...] = (
|
|
|
359
375
|
(
|
|
360
376
|
"Startup & service",
|
|
361
377
|
(
|
|
362
|
-
("
|
|
363
|
-
("
|
|
364
|
-
("
|
|
365
|
-
("service
|
|
378
|
+
("service run [id] --port N -- <cmd>", "Run and publish a durable service command"),
|
|
379
|
+
("service status [id]", "Show durable service status"),
|
|
380
|
+
("service logs [id] --follow", "Stream service logs"),
|
|
381
|
+
("service restart [id]", "Restart the durable service"),
|
|
382
|
+
("service stop [id]", "Stop service but keep config"),
|
|
366
383
|
("service clear [id]", "Clear the durable published app service"),
|
|
367
384
|
),
|
|
368
385
|
),
|
|
@@ -554,7 +571,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
554
571
|
config, args.id, args.run_command, args.uid, args.gid, args.cwd, args.shell
|
|
555
572
|
)
|
|
556
573
|
if args.command == "publish":
|
|
557
|
-
return handle_publish(config, args.id, args.port)
|
|
574
|
+
return handle_publish(config, args.id, args.port, args.publish_command)
|
|
558
575
|
if args.command == "startup":
|
|
559
576
|
if args.startup_action == "show":
|
|
560
577
|
return handle_startup_show(config, args.id)
|
|
@@ -564,8 +581,16 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
564
581
|
return handle_startup_clear(config, args.id)
|
|
565
582
|
return report_error("unknown startup command")
|
|
566
583
|
if args.command == "service":
|
|
567
|
-
if args.service_action == "
|
|
584
|
+
if args.service_action == "run":
|
|
585
|
+
return handle_service_run(config, args.id, args.port, args.cwd, args.service_command)
|
|
586
|
+
if args.service_action in ("show", "status"):
|
|
568
587
|
return handle_service_show(config, args.id)
|
|
588
|
+
if args.service_action == "logs":
|
|
589
|
+
return handle_service_logs(config, args.id, args.follow)
|
|
590
|
+
if args.service_action == "restart":
|
|
591
|
+
return handle_service_restart(config, args.id)
|
|
592
|
+
if args.service_action == "stop":
|
|
593
|
+
return handle_service_stop(config, args.id)
|
|
569
594
|
if args.service_action == "clear":
|
|
570
595
|
return handle_service_clear(config, args.id)
|
|
571
596
|
return report_error("unknown service command")
|
|
@@ -1314,6 +1339,10 @@ def _render_startup_panel(payload: dict[str, Any], *, title: str = "startup") ->
|
|
|
1314
1339
|
body = Text()
|
|
1315
1340
|
configured = bool(payload.get("configured"))
|
|
1316
1341
|
rows: list[tuple[str, str, str | None]] = [("configured", "yes" if configured else "no", "bold white" if configured else "dim")]
|
|
1342
|
+
if payload.get("status"):
|
|
1343
|
+
rows.append(("status", str(payload.get("status")), "bold white"))
|
|
1344
|
+
if payload.get("public_url"):
|
|
1345
|
+
rows.append(("url", str(payload.get("public_url")), "cyan"))
|
|
1317
1346
|
if configured:
|
|
1318
1347
|
rows.extend(
|
|
1319
1348
|
[
|
|
@@ -1322,6 +1351,12 @@ def _render_startup_panel(payload: dict[str, Any], *, title: str = "startup") ->
|
|
|
1322
1351
|
("port", str(payload.get("port") or "—"), "bold white"),
|
|
1323
1352
|
]
|
|
1324
1353
|
)
|
|
1354
|
+
if payload.get("exec_id"):
|
|
1355
|
+
rows.append(("exec", str(payload.get("exec_id")), "dim"))
|
|
1356
|
+
if payload.get("started_at"):
|
|
1357
|
+
rows.append(("started", str(payload.get("started_at")), "dim"))
|
|
1358
|
+
if payload.get("exit_code") is not None:
|
|
1359
|
+
rows.append(("exit", str(payload.get("exit_code")), "red"))
|
|
1325
1360
|
for label, value, style in rows:
|
|
1326
1361
|
body.append(f"{label:<10}", style="dim")
|
|
1327
1362
|
body.append(value, style=style or "white")
|
|
@@ -2066,10 +2101,18 @@ def handle_create(
|
|
|
2066
2101
|
label = "creating computer…"
|
|
2067
2102
|
if startup_command is not None:
|
|
2068
2103
|
label = f"creating computer and starting app on port {port}…"
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2104
|
+
try:
|
|
2105
|
+
result = _with_spinner(
|
|
2106
|
+
label,
|
|
2107
|
+
lambda: client.create_computer(slug=name, command=startup_command, cwd=cwd, port=port),
|
|
2108
|
+
)
|
|
2109
|
+
except RuntimeAPIError as exc:
|
|
2110
|
+
if exc.status_code == 409 and "slug already in use" in str(exc) and name:
|
|
2111
|
+
return report_error(
|
|
2112
|
+
f"computer name {name!r} is already in use. Names become public URL slugs and must be unique across active and cold computers. Pick it from `runtime list`, delete it, or choose a different name.",
|
|
2113
|
+
status_code=exc.status_code,
|
|
2114
|
+
)
|
|
2115
|
+
raise
|
|
2073
2116
|
|
|
2074
2117
|
def render(payload: dict[str, Any]) -> None:
|
|
2075
2118
|
_render_computer_panel(payload, title="created")
|
|
@@ -2293,6 +2336,36 @@ def handle_startup_clear(config: RuntimeConfig, computer_id: str | None) -> int:
|
|
|
2293
2336
|
return report_success(result or {"configured": False}, render)
|
|
2294
2337
|
|
|
2295
2338
|
|
|
2339
|
+
def _command_from_remainder(parts: list[str] | None) -> str | None:
|
|
2340
|
+
if not parts:
|
|
2341
|
+
return None
|
|
2342
|
+
if parts and parts[0] == "--":
|
|
2343
|
+
parts = parts[1:]
|
|
2344
|
+
if not parts:
|
|
2345
|
+
return None
|
|
2346
|
+
return shlex.join(parts)
|
|
2347
|
+
|
|
2348
|
+
|
|
2349
|
+
def handle_service_run(config: RuntimeConfig, computer_id: str | None, port: int | None, cwd: str | None, command_parts: list[str] | None) -> int:
|
|
2350
|
+
if (err := _require_api_key(config)) is not None:
|
|
2351
|
+
return err
|
|
2352
|
+
if not computer_id:
|
|
2353
|
+
return report_error("computer id is required")
|
|
2354
|
+
if port is None or port <= 0 or port > 65535:
|
|
2355
|
+
return report_error("port must be between 1 and 65535")
|
|
2356
|
+
command = _command_from_remainder(command_parts)
|
|
2357
|
+
if not command:
|
|
2358
|
+
return report_error("service run requires -- <command...>")
|
|
2359
|
+
|
|
2360
|
+
client = RuntimeClient(base_url=config.base_url, api_key=config.api_key)
|
|
2361
|
+
computer_id = _resolve_computer_ref(client, computer_id)
|
|
2362
|
+
result = _with_spinner(
|
|
2363
|
+
f"starting service on {computer_id}…",
|
|
2364
|
+
lambda: client.run_service(computer_id, command=command, cwd=cwd, port=port),
|
|
2365
|
+
)
|
|
2366
|
+
return report_success(result, lambda p: _render_startup_panel(p, title="service"))
|
|
2367
|
+
|
|
2368
|
+
|
|
2296
2369
|
def handle_service_show(config: RuntimeConfig, computer_id: str | None) -> int:
|
|
2297
2370
|
if (err := _require_api_key(config)) is not None:
|
|
2298
2371
|
return err
|
|
@@ -2313,6 +2386,56 @@ def handle_service_show(config: RuntimeConfig, computer_id: str | None) -> int:
|
|
|
2313
2386
|
return report_success(result, lambda p: _render_startup_panel(p, title="service"))
|
|
2314
2387
|
|
|
2315
2388
|
|
|
2389
|
+
def handle_service_logs(config: RuntimeConfig, computer_id: str | None, follow: bool) -> int:
|
|
2390
|
+
if (err := _require_api_key(config)) is not None:
|
|
2391
|
+
return err
|
|
2392
|
+
if not computer_id:
|
|
2393
|
+
return report_error("computer id is required")
|
|
2394
|
+
client = RuntimeClient(base_url=config.base_url, api_key=config.api_key)
|
|
2395
|
+
computer_id = _resolve_computer_ref(client, computer_id)
|
|
2396
|
+
for line in client.stream_service_logs(computer_id, follow=follow):
|
|
2397
|
+
try:
|
|
2398
|
+
frame = json.loads(line)
|
|
2399
|
+
except ValueError:
|
|
2400
|
+
print(line)
|
|
2401
|
+
continue
|
|
2402
|
+
stdout = frame.get("stdout") or ""
|
|
2403
|
+
stderr = frame.get("stderr") or ""
|
|
2404
|
+
data = frame.get("data") or ""
|
|
2405
|
+
error = frame.get("error") or ""
|
|
2406
|
+
if stdout:
|
|
2407
|
+
print(stdout, end="")
|
|
2408
|
+
if stderr:
|
|
2409
|
+
print(stderr, end="", file=sys.stderr)
|
|
2410
|
+
if data:
|
|
2411
|
+
print(data, end="")
|
|
2412
|
+
if error:
|
|
2413
|
+
print(error, file=sys.stderr)
|
|
2414
|
+
return 0
|
|
2415
|
+
|
|
2416
|
+
|
|
2417
|
+
def handle_service_restart(config: RuntimeConfig, computer_id: str | None) -> int:
|
|
2418
|
+
if (err := _require_api_key(config)) is not None:
|
|
2419
|
+
return err
|
|
2420
|
+
if not computer_id:
|
|
2421
|
+
return report_error("computer id is required")
|
|
2422
|
+
client = RuntimeClient(base_url=config.base_url, api_key=config.api_key)
|
|
2423
|
+
computer_id = _resolve_computer_ref(client, computer_id)
|
|
2424
|
+
result = _with_spinner(f"restarting service for {computer_id}…", lambda: client.restart_service(computer_id))
|
|
2425
|
+
return report_success(result, lambda p: _render_startup_panel(p, title="service restarted"))
|
|
2426
|
+
|
|
2427
|
+
|
|
2428
|
+
def handle_service_stop(config: RuntimeConfig, computer_id: str | None) -> int:
|
|
2429
|
+
if (err := _require_api_key(config)) is not None:
|
|
2430
|
+
return err
|
|
2431
|
+
if not computer_id:
|
|
2432
|
+
return report_error("computer id is required")
|
|
2433
|
+
client = RuntimeClient(base_url=config.base_url, api_key=config.api_key)
|
|
2434
|
+
computer_id = _resolve_computer_ref(client, computer_id)
|
|
2435
|
+
result = _with_spinner(f"stopping service for {computer_id}…", lambda: client.stop_service(computer_id))
|
|
2436
|
+
return report_success(result, lambda p: _render_startup_panel(p, title="service stopped"))
|
|
2437
|
+
|
|
2438
|
+
|
|
2316
2439
|
def handle_service_clear(config: RuntimeConfig, computer_id: str | None) -> int:
|
|
2317
2440
|
if (err := _require_api_key(config)) is not None:
|
|
2318
2441
|
return err
|
|
@@ -2465,7 +2588,7 @@ def handle_run(
|
|
|
2465
2588
|
return report_success(result, _render_run_result)
|
|
2466
2589
|
|
|
2467
2590
|
|
|
2468
|
-
def handle_publish(config: RuntimeConfig, computer_id: str | None, port: int | None) -> int:
|
|
2591
|
+
def handle_publish(config: RuntimeConfig, computer_id: str | None, port: int | None, command_parts: list[str] | None = None) -> int:
|
|
2469
2592
|
if (err := _require_api_key(config)) is not None:
|
|
2470
2593
|
return err
|
|
2471
2594
|
|
|
@@ -2497,22 +2620,17 @@ def handle_publish(config: RuntimeConfig, computer_id: str | None, port: int | N
|
|
|
2497
2620
|
if port <= 0 or port > 65535:
|
|
2498
2621
|
return report_error("port must be between 1 and 65535")
|
|
2499
2622
|
|
|
2623
|
+
command = _command_from_remainder(command_parts)
|
|
2624
|
+
if not command:
|
|
2625
|
+
return report_error(f"publish requires -- <command...>, for example: runtime publish {computer_id} {port} -- npm run dev -- --host 0.0.0.0")
|
|
2626
|
+
|
|
2500
2627
|
client = RuntimeClient(base_url=config.base_url, api_key=config.api_key)
|
|
2501
2628
|
computer_id = _resolve_computer_ref(client, computer_id)
|
|
2502
2629
|
result = _with_spinner(
|
|
2503
|
-
f"publishing
|
|
2504
|
-
lambda: client.publish_port(computer_id, port),
|
|
2630
|
+
f"publishing service on {computer_id}…",
|
|
2631
|
+
lambda: client.publish_port(computer_id, port, command=command),
|
|
2505
2632
|
)
|
|
2506
|
-
|
|
2507
|
-
def render(payload: dict[str, Any]) -> None:
|
|
2508
|
-
_UI.console().print(
|
|
2509
|
-
f"[green]✓[/green] public url for [bold]{computer_id}[/bold] now points at durable app port [bold]{payload.get('published_port') or port}[/bold]"
|
|
2510
|
-
)
|
|
2511
|
-
url = payload.get("public_url")
|
|
2512
|
-
if url:
|
|
2513
|
-
_UI.console().print(f"[bold green]→[/bold green] [cyan]{url}[/cyan]")
|
|
2514
|
-
|
|
2515
|
-
return report_success(result, render)
|
|
2633
|
+
return report_success(result, lambda p: _render_startup_panel(p, title="service"))
|
|
2516
2634
|
|
|
2517
2635
|
|
|
2518
2636
|
def handle_proxy_start(config: RuntimeConfig, computer_id: str | None, specs: list[str] | None) -> int:
|
|
@@ -2769,6 +2887,26 @@ def _load_posix_tty_modules() -> tuple[Any, Any]:
|
|
|
2769
2887
|
return termios_mod, tty_mod
|
|
2770
2888
|
|
|
2771
2889
|
|
|
2890
|
+
TERMINAL_FRAME_DATA = 0x01
|
|
2891
|
+
TERMINAL_FRAME_WINDOW_SIZE = 0x02
|
|
2892
|
+
TERMINAL_FRAME_SHUTDOWN = 0x03
|
|
2893
|
+
TERMINAL_FRAME_HEARTBEAT = 0x04
|
|
2894
|
+
TERMINAL_FRAME_SESSION_CLOSE = 0x05
|
|
2895
|
+
TERMINAL_FRAME_HEADER_SIZE = 5
|
|
2896
|
+
|
|
2897
|
+
|
|
2898
|
+
def _encode_terminal_frame(frame_type: int, payload: bytes = b"") -> bytes:
|
|
2899
|
+
return bytes([frame_type]) + struct.pack(">I", len(payload)) + payload
|
|
2900
|
+
|
|
2901
|
+
|
|
2902
|
+
def _parse_terminal_frame(raw: bytes) -> tuple[int, bytes]:
|
|
2903
|
+
if len(raw) < TERMINAL_FRAME_HEADER_SIZE:
|
|
2904
|
+
raise RuntimeAPIError("terminal received a truncated frame")
|
|
2905
|
+
payload_len = struct.unpack(">I", raw[1:TERMINAL_FRAME_HEADER_SIZE])[0]
|
|
2906
|
+
if len(raw) != TERMINAL_FRAME_HEADER_SIZE + payload_len:
|
|
2907
|
+
raise RuntimeAPIError("terminal received a malformed frame")
|
|
2908
|
+
return raw[0], raw[TERMINAL_FRAME_HEADER_SIZE:]
|
|
2909
|
+
|
|
2772
2910
|
|
|
2773
2911
|
def _run_terminal_session(ws_url: str) -> int:
|
|
2774
2912
|
if not sys.stdin.isatty() or not sys.stdout.isatty():
|
|
@@ -2792,26 +2930,18 @@ def _run_terminal_session(ws_url: str) -> int:
|
|
|
2792
2930
|
result["code"] = 0
|
|
2793
2931
|
stop.set()
|
|
2794
2932
|
return
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2933
|
+
if not isinstance(raw, bytes):
|
|
2934
|
+
raise RuntimeAPIError("terminal received a non-binary frame")
|
|
2935
|
+
frame_type, payload = _parse_terminal_frame(raw)
|
|
2936
|
+
if frame_type == TERMINAL_FRAME_HEARTBEAT:
|
|
2798
2937
|
continue
|
|
2799
|
-
if
|
|
2800
|
-
data = _filter_terminal_output(
|
|
2938
|
+
if frame_type == TERMINAL_FRAME_DATA:
|
|
2939
|
+
data = _filter_terminal_output(payload.decode("utf-8", errors="ignore"), output_state)
|
|
2801
2940
|
if data:
|
|
2802
2941
|
sys.stdout.write(data)
|
|
2803
2942
|
sys.stdout.flush()
|
|
2804
2943
|
continue
|
|
2805
|
-
if
|
|
2806
|
-
result["code"] = int(event.get("code") or 0)
|
|
2807
|
-
message = str(event.get("message") or "").strip()
|
|
2808
|
-
if message:
|
|
2809
|
-
result["error"] = message
|
|
2810
|
-
stop.set()
|
|
2811
|
-
return
|
|
2812
|
-
if event_type == "error":
|
|
2813
|
-
result["code"] = 1
|
|
2814
|
-
result["error"] = str(event.get("message") or "terminal error")
|
|
2944
|
+
if frame_type in (TERMINAL_FRAME_SHUTDOWN, TERMINAL_FRAME_SESSION_CLOSE):
|
|
2815
2945
|
stop.set()
|
|
2816
2946
|
return
|
|
2817
2947
|
except ConnectionClosed:
|
|
@@ -2843,9 +2973,11 @@ def _run_terminal_session(ws_url: str) -> int:
|
|
|
2843
2973
|
if not data:
|
|
2844
2974
|
stop.set()
|
|
2845
2975
|
break
|
|
2846
|
-
ws.send(
|
|
2976
|
+
ws.send(_encode_terminal_frame(TERMINAL_FRAME_DATA, data))
|
|
2847
2977
|
finally:
|
|
2848
2978
|
stop.set()
|
|
2979
|
+
with contextlib.suppress(Exception):
|
|
2980
|
+
ws.send(_encode_terminal_frame(TERMINAL_FRAME_SHUTDOWN))
|
|
2849
2981
|
with contextlib.suppress(Exception):
|
|
2850
2982
|
ws.close()
|
|
2851
2983
|
termios_mod.tcsetattr(fd, termios_mod.TCSADRAIN, old_settings)
|
|
@@ -2935,7 +3067,7 @@ def _strip_terminal_noise(text: str) -> str:
|
|
|
2935
3067
|
|
|
2936
3068
|
def _send_terminal_resize(ws: Any) -> None:
|
|
2937
3069
|
size = shutil.get_terminal_size((100, 24))
|
|
2938
|
-
ws.send(
|
|
3070
|
+
ws.send(_encode_terminal_frame(TERMINAL_FRAME_WINDOW_SIZE, struct.pack(">II", size.columns, size.lines)))
|
|
2939
3071
|
|
|
2940
3072
|
|
|
2941
3073
|
# --------------------------------------------------------------------------- #
|
|
@@ -3022,7 +3154,7 @@ def _vm_detail_menu(client: RuntimeClient, vm: dict[str, Any]) -> str:
|
|
|
3022
3154
|
choices.append(questionary.Choice(title="⌨ enter console", value="enter"))
|
|
3023
3155
|
if internal_status == "running":
|
|
3024
3156
|
choices.append(questionary.Choice(title="▶ run a command", value="run"))
|
|
3025
|
-
choices.append(questionary.Choice(title="⤴ publish
|
|
3157
|
+
choices.append(questionary.Choice(title="⤴ publish service", value="publish"))
|
|
3026
3158
|
if power_state == "cold":
|
|
3027
3159
|
choices.append(questionary.Choice(title="☀ start / wake", value="start"))
|
|
3028
3160
|
choices.extend(
|
|
@@ -3030,7 +3162,7 @@ def _vm_detail_menu(client: RuntimeClient, vm: dict[str, Any]) -> str:
|
|
|
3030
3162
|
questionary.Choice(title="⧉ copy public url", value="copy-url"),
|
|
3031
3163
|
questionary.Choice(title="↗ open in browser", value="open-url"),
|
|
3032
3164
|
questionary.Choice(title=f"⚑ make {flip_visibility}", value=f"share:{flip_visibility}"),
|
|
3033
|
-
questionary.Choice(title="⚙
|
|
3165
|
+
questionary.Choice(title="⚙ service…", value="startup"),
|
|
3034
3166
|
questionary.Choice(title="ℹ refresh info", value="info"),
|
|
3035
3167
|
questionary.Separator(),
|
|
3036
3168
|
questionary.Choice(title="✕ delete", value="delete"),
|
|
@@ -3080,13 +3212,14 @@ def _vm_detail_menu(client: RuntimeClient, vm: dict[str, Any]) -> str:
|
|
|
3080
3212
|
if port <= 0 or port > 65535:
|
|
3081
3213
|
_UI.err().print("[red]port must be between 1 and 65535[/red]")
|
|
3082
3214
|
continue
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3215
|
+
command = _prompt_text("Command to run as the service")
|
|
3216
|
+
if not command:
|
|
3217
|
+
continue
|
|
3218
|
+
result = _with_spinner(
|
|
3219
|
+
f"publishing service on {slug}…",
|
|
3220
|
+
lambda: client.publish_port(vm_id, port, command=command),
|
|
3089
3221
|
)
|
|
3222
|
+
_render_startup_panel(result, title=f"service: {slug}")
|
|
3090
3223
|
continue
|
|
3091
3224
|
|
|
3092
3225
|
if action == "info":
|
|
@@ -3143,11 +3276,11 @@ def _vm_detail_menu(client: RuntimeClient, vm: dict[str, Any]) -> str:
|
|
|
3143
3276
|
|
|
3144
3277
|
|
|
3145
3278
|
def _vm_startup_submenu(client: RuntimeClient, vm_id: str, slug: str) -> None:
|
|
3146
|
-
"""Inner submenu for durable
|
|
3279
|
+
"""Inner submenu for the durable service."""
|
|
3147
3280
|
questionary = _UI.q()
|
|
3148
3281
|
while True:
|
|
3149
|
-
current = _with_spinner("fetching
|
|
3150
|
-
_render_startup_panel(current, title=f"
|
|
3282
|
+
current = _with_spinner("fetching service…", lambda: client.get_service(vm_id))
|
|
3283
|
+
_render_startup_panel(current, title=f"service: {slug}")
|
|
3151
3284
|
configured = bool(current.get("configured"))
|
|
3152
3285
|
choices: list[Any] = [
|
|
3153
3286
|
questionary.Choice(title="✎ set / replace", value="set"),
|
|
@@ -3160,11 +3293,11 @@ def _vm_startup_submenu(client: RuntimeClient, vm_id: str, slug: str) -> None:
|
|
|
3160
3293
|
questionary.Choice(title="← back", value="__back__"),
|
|
3161
3294
|
]
|
|
3162
3295
|
)
|
|
3163
|
-
action = _select_prompt("
|
|
3296
|
+
action = _select_prompt("service", choices).unsafe_ask()
|
|
3164
3297
|
if action in (None, "__back__"):
|
|
3165
3298
|
return
|
|
3166
3299
|
if action == "set":
|
|
3167
|
-
command = _prompt_text("
|
|
3300
|
+
command = _prompt_text("Service command", default=str(current.get("command") or ""))
|
|
3168
3301
|
if not command:
|
|
3169
3302
|
_UI.console().print("[dim]cancelled[/dim]")
|
|
3170
3303
|
continue
|
|
@@ -3182,20 +3315,20 @@ def _vm_startup_submenu(client: RuntimeClient, vm_id: str, slug: str) -> None:
|
|
|
3182
3315
|
continue
|
|
3183
3316
|
cwd = _prompt_text("Working directory (optional)", default=str(current.get("cwd") or ""))
|
|
3184
3317
|
_with_spinner(
|
|
3185
|
-
"
|
|
3186
|
-
lambda: client.
|
|
3318
|
+
"starting service…",
|
|
3319
|
+
lambda: client.run_service(vm_id, command=command, cwd=cwd or None, port=port),
|
|
3187
3320
|
)
|
|
3188
|
-
_UI.console().print(f"[green]✓[/green]
|
|
3321
|
+
_UI.console().print(f"[green]✓[/green] service started for [bold]{slug}[/bold]")
|
|
3189
3322
|
continue
|
|
3190
3323
|
if action == "clear":
|
|
3191
|
-
if not _prompt_confirm(f"Clear
|
|
3324
|
+
if not _prompt_confirm(f"Clear service for {slug}?", default=False):
|
|
3192
3325
|
_UI.console().print("[dim]cancelled[/dim]")
|
|
3193
3326
|
continue
|
|
3194
3327
|
_with_spinner(
|
|
3195
|
-
"clearing
|
|
3196
|
-
lambda: client.
|
|
3328
|
+
"clearing service…",
|
|
3329
|
+
lambda: client.clear_service(vm_id),
|
|
3197
3330
|
)
|
|
3198
|
-
_UI.console().print(f"[green]✓[/green] cleared
|
|
3331
|
+
_UI.console().print(f"[green]✓[/green] cleared service for [bold]{slug}[/bold]")
|
|
3199
3332
|
continue
|
|
3200
3333
|
|
|
3201
3334
|
|
|
@@ -3209,7 +3342,6 @@ _HELP_TOPICS: dict[str, tuple[str, ...]] = {
|
|
|
3209
3342
|
"computers": ("Computers",),
|
|
3210
3343
|
"share": ("Sharing",),
|
|
3211
3344
|
"sharing": ("Sharing",),
|
|
3212
|
-
"startup": ("Startup & service",),
|
|
3213
3345
|
"service": ("Startup & service",),
|
|
3214
3346
|
"proxy": ("Proxy",),
|
|
3215
3347
|
"github": ("GitHub",),
|
|
@@ -3251,7 +3383,7 @@ _runtime_complete() {
|
|
|
3251
3383
|
COMPREPLY=()
|
|
3252
3384
|
cur="${COMP_WORDS[COMP_CWORD]}"
|
|
3253
3385
|
if [[ ${COMP_CWORD} -eq 1 ]]; then
|
|
3254
|
-
COMPREPLY=( $(compgen -W "create list ls info url start enter run publish delete rm share
|
|
3386
|
+
COMPREPLY=( $(compgen -W "create list ls info url start enter run publish delete rm share service proxy switch checkout github integrate secrets api-keys signup verify login whoami logout completion help" -- "$cur") )
|
|
3255
3387
|
return
|
|
3256
3388
|
fi
|
|
3257
3389
|
sub="${COMP_WORDS[1]}"
|
|
@@ -3295,7 +3427,6 @@ _runtime() {
|
|
|
3295
3427
|
'delete:Delete a computer'
|
|
3296
3428
|
'rm:Delete a computer'
|
|
3297
3429
|
'share:Set visibility'
|
|
3298
|
-
'startup:Startup config'
|
|
3299
3430
|
'service:Service config'
|
|
3300
3431
|
'proxy:Port proxies'
|
|
3301
3432
|
'switch:Open a GitHub branch workspace'
|
|
@@ -3363,7 +3494,6 @@ complete -c runtime -n '__fish_use_subcommand' -a publish -d 'Publish port'
|
|
|
3363
3494
|
complete -c runtime -n '__fish_use_subcommand' -a delete -d 'Delete'
|
|
3364
3495
|
complete -c runtime -n '__fish_use_subcommand' -a rm -d 'Delete'
|
|
3365
3496
|
complete -c runtime -n '__fish_use_subcommand' -a share -d 'Set visibility'
|
|
3366
|
-
complete -c runtime -n '__fish_use_subcommand' -a startup -d 'Startup config'
|
|
3367
3497
|
complete -c runtime -n '__fish_use_subcommand' -a service -d 'Service config'
|
|
3368
3498
|
complete -c runtime -n '__fish_use_subcommand' -a proxy -d 'Port proxies'
|
|
3369
3499
|
complete -c runtime -n '__fish_use_subcommand' -a github -d 'GitHub connections'
|
|
@@ -3371,7 +3501,7 @@ complete -c runtime -n '__fish_use_subcommand' -a integrate -d 'Connect GitHub'
|
|
|
3371
3501
|
complete -c runtime -n '__fish_use_subcommand' -a help -d 'Extended help'
|
|
3372
3502
|
complete -c runtime -n '__fish_use_subcommand' -a completion -d 'Completion'
|
|
3373
3503
|
|
|
3374
|
-
complete -c runtime -n '__fish_seen_subcommand_from info url start enter run publish delete rm
|
|
3504
|
+
complete -c runtime -n '__fish_seen_subcommand_from info url start enter run publish delete rm service' -a '(__runtime_slugs)'
|
|
3375
3505
|
complete -c runtime -n '__fish_seen_subcommand_from completion' -a 'bash zsh fish'
|
|
3376
3506
|
complete -c runtime -n '__fish_seen_subcommand_from share' -a 'public private'
|
|
3377
3507
|
"""
|
|
@@ -148,7 +148,71 @@ class RuntimeClient:
|
|
|
148
148
|
return self._request("GET", f"/api/computers/{computer_id}/startup", auth_required=True)
|
|
149
149
|
|
|
150
150
|
def get_service_config(self, computer_id: str) -> dict[str, Any]:
|
|
151
|
-
return self.
|
|
151
|
+
return self.get_service(computer_id)
|
|
152
|
+
|
|
153
|
+
def get_service(self, computer_id: str) -> dict[str, Any]:
|
|
154
|
+
return self._request("GET", f"/api/computers/{computer_id}/service", auth_required=True)
|
|
155
|
+
|
|
156
|
+
def run_service(
|
|
157
|
+
self,
|
|
158
|
+
computer_id: str,
|
|
159
|
+
*,
|
|
160
|
+
command: str,
|
|
161
|
+
cwd: str | None = None,
|
|
162
|
+
port: int,
|
|
163
|
+
) -> dict[str, Any]:
|
|
164
|
+
body: dict[str, Any] = {"command": command, "port": port}
|
|
165
|
+
if cwd is not None:
|
|
166
|
+
body["cwd"] = cwd
|
|
167
|
+
return self._request(
|
|
168
|
+
"POST",
|
|
169
|
+
f"/api/computers/{computer_id}/service/run",
|
|
170
|
+
json=body,
|
|
171
|
+
auth_required=True,
|
|
172
|
+
timeout=max(self.timeout, DEFAULT_WARMUP_TIMEOUT),
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
def restart_service(self, computer_id: str) -> dict[str, Any]:
|
|
176
|
+
return self._request(
|
|
177
|
+
"POST",
|
|
178
|
+
f"/api/computers/{computer_id}/service/restart",
|
|
179
|
+
auth_required=True,
|
|
180
|
+
timeout=max(self.timeout, DEFAULT_WARMUP_TIMEOUT),
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
def stop_service(self, computer_id: str) -> dict[str, Any]:
|
|
184
|
+
return self._request("POST", f"/api/computers/{computer_id}/service/stop", auth_required=True)
|
|
185
|
+
|
|
186
|
+
def clear_service(self, computer_id: str) -> dict[str, Any]:
|
|
187
|
+
return self._request("DELETE", f"/api/computers/{computer_id}/service", auth_required=True)
|
|
188
|
+
|
|
189
|
+
def stream_service_logs(self, computer_id: str, *, follow: bool = False):
|
|
190
|
+
if not self.api_key:
|
|
191
|
+
raise RuntimeAPIError("missing api key")
|
|
192
|
+
path = f"/api/computers/{computer_id}/service/logs"
|
|
193
|
+
if follow:
|
|
194
|
+
path += "?follow=true"
|
|
195
|
+
client = httpx.Client(base_url=self.base_url, timeout=None, transport=self.transport)
|
|
196
|
+
try:
|
|
197
|
+
with client.stream("GET", path, headers={"Authorization": f"Bearer {self.api_key}"}) as response:
|
|
198
|
+
if not response.is_success:
|
|
199
|
+
body = response.read()
|
|
200
|
+
payload: dict[str, Any] = {}
|
|
201
|
+
if body:
|
|
202
|
+
try:
|
|
203
|
+
decoded = response.json()
|
|
204
|
+
if isinstance(decoded, dict):
|
|
205
|
+
payload = decoded
|
|
206
|
+
except ValueError:
|
|
207
|
+
pass
|
|
208
|
+
message = payload.get("error") or payload.get("message") or f"http {response.status_code}"
|
|
209
|
+
raise RuntimeAPIError(str(message), status_code=response.status_code)
|
|
210
|
+
for line in response.iter_lines():
|
|
211
|
+
if not line:
|
|
212
|
+
continue
|
|
213
|
+
yield line
|
|
214
|
+
finally:
|
|
215
|
+
client.close()
|
|
152
216
|
|
|
153
217
|
def set_startup_config(
|
|
154
218
|
self,
|
|
@@ -167,7 +231,7 @@ class RuntimeClient:
|
|
|
167
231
|
return self._request("DELETE", f"/api/computers/{computer_id}/startup", auth_required=True)
|
|
168
232
|
|
|
169
233
|
def clear_service_config(self, computer_id: str) -> dict[str, Any]:
|
|
170
|
-
return self.
|
|
234
|
+
return self.clear_service(computer_id)
|
|
171
235
|
|
|
172
236
|
def start_computer(self, computer_id: str) -> dict[str, Any]:
|
|
173
237
|
return self._request(
|
|
@@ -220,15 +284,12 @@ class RuntimeClient:
|
|
|
220
284
|
body["shell"] = shell
|
|
221
285
|
return self._request("POST", f"/api/computers/{computer_id}/exec", json=body, auth_required=True)
|
|
222
286
|
|
|
223
|
-
def publish_port(self, computer_id: str, port: int) -> dict[str, Any]:
|
|
287
|
+
def publish_port(self, computer_id: str, port: int, *, command: str | None = None, cwd: str | None = None) -> dict[str, Any]:
|
|
224
288
|
if port <= 0 or port > 65535:
|
|
225
289
|
raise RuntimeAPIError("port must be between 1 and 65535")
|
|
226
|
-
|
|
227
|
-
"
|
|
228
|
-
|
|
229
|
-
json={"port": port},
|
|
230
|
-
auth_required=True,
|
|
231
|
-
)
|
|
290
|
+
if not command:
|
|
291
|
+
raise RuntimeAPIError("publish requires -- <command...>")
|
|
292
|
+
return self.run_service(computer_id, command=command, cwd=cwd, port=port)
|
|
232
293
|
|
|
233
294
|
def _request(
|
|
234
295
|
self,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: runtime-sdk
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.30
|
|
4
4
|
Summary: Runtime Python SDK and CLI
|
|
5
5
|
Project-URL: Repository, https://github.com/The-Money-Company-Limited/runtimevm
|
|
6
6
|
Project-URL: Issues, https://github.com/The-Money-Company-Limited/runtimevm/issues
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|