runtime-sdk 0.4.28__tar.gz → 0.4.29__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.29}/PKG-INFO +1 -1
- {runtime_sdk-0.4.28 → runtime_sdk-0.4.29}/pyproject.toml +1 -1
- {runtime_sdk-0.4.28 → runtime_sdk-0.4.29}/scripts/runtime_sdk/cli.py +188 -66
- {runtime_sdk-0.4.28 → runtime_sdk-0.4.29}/scripts/runtime_sdk/client.py +70 -9
- {runtime_sdk-0.4.28 → runtime_sdk-0.4.29}/scripts/runtime_sdk.egg-info/PKG-INFO +1 -1
- {runtime_sdk-0.4.28 → runtime_sdk-0.4.29}/README.md +0 -0
- {runtime_sdk-0.4.28 → runtime_sdk-0.4.29}/scripts/runtime_sdk/__init__.py +0 -0
- {runtime_sdk-0.4.28 → runtime_sdk-0.4.29}/scripts/runtime_sdk/config.py +0 -0
- {runtime_sdk-0.4.28 → runtime_sdk-0.4.29}/scripts/runtime_sdk.egg-info/SOURCES.txt +0 -0
- {runtime_sdk-0.4.28 → runtime_sdk-0.4.29}/scripts/runtime_sdk.egg-info/dependency_links.txt +0 -0
- {runtime_sdk-0.4.28 → runtime_sdk-0.4.29}/scripts/runtime_sdk.egg-info/entry_points.txt +0 -0
- {runtime_sdk-0.4.28 → runtime_sdk-0.4.29}/scripts/runtime_sdk.egg-info/requires.txt +0 -0
- {runtime_sdk-0.4.28 → runtime_sdk-0.4.29}/scripts/runtime_sdk.egg-info/top_level.txt +0 -0
- {runtime_sdk-0.4.28 → runtime_sdk-0.4.29}/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.29
|
|
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")
|
|
@@ -2293,6 +2328,36 @@ def handle_startup_clear(config: RuntimeConfig, computer_id: str | None) -> int:
|
|
|
2293
2328
|
return report_success(result or {"configured": False}, render)
|
|
2294
2329
|
|
|
2295
2330
|
|
|
2331
|
+
def _command_from_remainder(parts: list[str] | None) -> str | None:
|
|
2332
|
+
if not parts:
|
|
2333
|
+
return None
|
|
2334
|
+
if parts and parts[0] == "--":
|
|
2335
|
+
parts = parts[1:]
|
|
2336
|
+
if not parts:
|
|
2337
|
+
return None
|
|
2338
|
+
return shlex.join(parts)
|
|
2339
|
+
|
|
2340
|
+
|
|
2341
|
+
def handle_service_run(config: RuntimeConfig, computer_id: str | None, port: int | None, cwd: str | None, command_parts: list[str] | None) -> int:
|
|
2342
|
+
if (err := _require_api_key(config)) is not None:
|
|
2343
|
+
return err
|
|
2344
|
+
if not computer_id:
|
|
2345
|
+
return report_error("computer id is required")
|
|
2346
|
+
if port is None or port <= 0 or port > 65535:
|
|
2347
|
+
return report_error("port must be between 1 and 65535")
|
|
2348
|
+
command = _command_from_remainder(command_parts)
|
|
2349
|
+
if not command:
|
|
2350
|
+
return report_error("service run requires -- <command...>")
|
|
2351
|
+
|
|
2352
|
+
client = RuntimeClient(base_url=config.base_url, api_key=config.api_key)
|
|
2353
|
+
computer_id = _resolve_computer_ref(client, computer_id)
|
|
2354
|
+
result = _with_spinner(
|
|
2355
|
+
f"starting service on {computer_id}…",
|
|
2356
|
+
lambda: client.run_service(computer_id, command=command, cwd=cwd, port=port),
|
|
2357
|
+
)
|
|
2358
|
+
return report_success(result, lambda p: _render_startup_panel(p, title="service"))
|
|
2359
|
+
|
|
2360
|
+
|
|
2296
2361
|
def handle_service_show(config: RuntimeConfig, computer_id: str | None) -> int:
|
|
2297
2362
|
if (err := _require_api_key(config)) is not None:
|
|
2298
2363
|
return err
|
|
@@ -2313,6 +2378,56 @@ def handle_service_show(config: RuntimeConfig, computer_id: str | None) -> int:
|
|
|
2313
2378
|
return report_success(result, lambda p: _render_startup_panel(p, title="service"))
|
|
2314
2379
|
|
|
2315
2380
|
|
|
2381
|
+
def handle_service_logs(config: RuntimeConfig, computer_id: str | None, follow: bool) -> int:
|
|
2382
|
+
if (err := _require_api_key(config)) is not None:
|
|
2383
|
+
return err
|
|
2384
|
+
if not computer_id:
|
|
2385
|
+
return report_error("computer id is required")
|
|
2386
|
+
client = RuntimeClient(base_url=config.base_url, api_key=config.api_key)
|
|
2387
|
+
computer_id = _resolve_computer_ref(client, computer_id)
|
|
2388
|
+
for line in client.stream_service_logs(computer_id, follow=follow):
|
|
2389
|
+
try:
|
|
2390
|
+
frame = json.loads(line)
|
|
2391
|
+
except ValueError:
|
|
2392
|
+
print(line)
|
|
2393
|
+
continue
|
|
2394
|
+
stdout = frame.get("stdout") or ""
|
|
2395
|
+
stderr = frame.get("stderr") or ""
|
|
2396
|
+
data = frame.get("data") or ""
|
|
2397
|
+
error = frame.get("error") or ""
|
|
2398
|
+
if stdout:
|
|
2399
|
+
print(stdout, end="")
|
|
2400
|
+
if stderr:
|
|
2401
|
+
print(stderr, end="", file=sys.stderr)
|
|
2402
|
+
if data:
|
|
2403
|
+
print(data, end="")
|
|
2404
|
+
if error:
|
|
2405
|
+
print(error, file=sys.stderr)
|
|
2406
|
+
return 0
|
|
2407
|
+
|
|
2408
|
+
|
|
2409
|
+
def handle_service_restart(config: RuntimeConfig, computer_id: str | None) -> int:
|
|
2410
|
+
if (err := _require_api_key(config)) is not None:
|
|
2411
|
+
return err
|
|
2412
|
+
if not computer_id:
|
|
2413
|
+
return report_error("computer id is required")
|
|
2414
|
+
client = RuntimeClient(base_url=config.base_url, api_key=config.api_key)
|
|
2415
|
+
computer_id = _resolve_computer_ref(client, computer_id)
|
|
2416
|
+
result = _with_spinner(f"restarting service for {computer_id}…", lambda: client.restart_service(computer_id))
|
|
2417
|
+
return report_success(result, lambda p: _render_startup_panel(p, title="service restarted"))
|
|
2418
|
+
|
|
2419
|
+
|
|
2420
|
+
def handle_service_stop(config: RuntimeConfig, computer_id: str | None) -> int:
|
|
2421
|
+
if (err := _require_api_key(config)) is not None:
|
|
2422
|
+
return err
|
|
2423
|
+
if not computer_id:
|
|
2424
|
+
return report_error("computer id is required")
|
|
2425
|
+
client = RuntimeClient(base_url=config.base_url, api_key=config.api_key)
|
|
2426
|
+
computer_id = _resolve_computer_ref(client, computer_id)
|
|
2427
|
+
result = _with_spinner(f"stopping service for {computer_id}…", lambda: client.stop_service(computer_id))
|
|
2428
|
+
return report_success(result, lambda p: _render_startup_panel(p, title="service stopped"))
|
|
2429
|
+
|
|
2430
|
+
|
|
2316
2431
|
def handle_service_clear(config: RuntimeConfig, computer_id: str | None) -> int:
|
|
2317
2432
|
if (err := _require_api_key(config)) is not None:
|
|
2318
2433
|
return err
|
|
@@ -2465,7 +2580,7 @@ def handle_run(
|
|
|
2465
2580
|
return report_success(result, _render_run_result)
|
|
2466
2581
|
|
|
2467
2582
|
|
|
2468
|
-
def handle_publish(config: RuntimeConfig, computer_id: str | None, port: int | None) -> int:
|
|
2583
|
+
def handle_publish(config: RuntimeConfig, computer_id: str | None, port: int | None, command_parts: list[str] | None = None) -> int:
|
|
2469
2584
|
if (err := _require_api_key(config)) is not None:
|
|
2470
2585
|
return err
|
|
2471
2586
|
|
|
@@ -2497,22 +2612,17 @@ def handle_publish(config: RuntimeConfig, computer_id: str | None, port: int | N
|
|
|
2497
2612
|
if port <= 0 or port > 65535:
|
|
2498
2613
|
return report_error("port must be between 1 and 65535")
|
|
2499
2614
|
|
|
2615
|
+
command = _command_from_remainder(command_parts)
|
|
2616
|
+
if not command:
|
|
2617
|
+
return report_error(f"publish requires -- <command...>, for example: runtime publish {computer_id} {port} -- npm run dev -- --host 0.0.0.0")
|
|
2618
|
+
|
|
2500
2619
|
client = RuntimeClient(base_url=config.base_url, api_key=config.api_key)
|
|
2501
2620
|
computer_id = _resolve_computer_ref(client, computer_id)
|
|
2502
2621
|
result = _with_spinner(
|
|
2503
|
-
f"publishing
|
|
2504
|
-
lambda: client.publish_port(computer_id, port),
|
|
2622
|
+
f"publishing service on {computer_id}…",
|
|
2623
|
+
lambda: client.publish_port(computer_id, port, command=command),
|
|
2505
2624
|
)
|
|
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)
|
|
2625
|
+
return report_success(result, lambda p: _render_startup_panel(p, title="service"))
|
|
2516
2626
|
|
|
2517
2627
|
|
|
2518
2628
|
def handle_proxy_start(config: RuntimeConfig, computer_id: str | None, specs: list[str] | None) -> int:
|
|
@@ -2769,6 +2879,26 @@ def _load_posix_tty_modules() -> tuple[Any, Any]:
|
|
|
2769
2879
|
return termios_mod, tty_mod
|
|
2770
2880
|
|
|
2771
2881
|
|
|
2882
|
+
TERMINAL_FRAME_DATA = 0x01
|
|
2883
|
+
TERMINAL_FRAME_WINDOW_SIZE = 0x02
|
|
2884
|
+
TERMINAL_FRAME_SHUTDOWN = 0x03
|
|
2885
|
+
TERMINAL_FRAME_HEARTBEAT = 0x04
|
|
2886
|
+
TERMINAL_FRAME_SESSION_CLOSE = 0x05
|
|
2887
|
+
TERMINAL_FRAME_HEADER_SIZE = 5
|
|
2888
|
+
|
|
2889
|
+
|
|
2890
|
+
def _encode_terminal_frame(frame_type: int, payload: bytes = b"") -> bytes:
|
|
2891
|
+
return bytes([frame_type]) + struct.pack(">I", len(payload)) + payload
|
|
2892
|
+
|
|
2893
|
+
|
|
2894
|
+
def _parse_terminal_frame(raw: bytes) -> tuple[int, bytes]:
|
|
2895
|
+
if len(raw) < TERMINAL_FRAME_HEADER_SIZE:
|
|
2896
|
+
raise RuntimeAPIError("terminal received a truncated frame")
|
|
2897
|
+
payload_len = struct.unpack(">I", raw[1:TERMINAL_FRAME_HEADER_SIZE])[0]
|
|
2898
|
+
if len(raw) != TERMINAL_FRAME_HEADER_SIZE + payload_len:
|
|
2899
|
+
raise RuntimeAPIError("terminal received a malformed frame")
|
|
2900
|
+
return raw[0], raw[TERMINAL_FRAME_HEADER_SIZE:]
|
|
2901
|
+
|
|
2772
2902
|
|
|
2773
2903
|
def _run_terminal_session(ws_url: str) -> int:
|
|
2774
2904
|
if not sys.stdin.isatty() or not sys.stdout.isatty():
|
|
@@ -2792,26 +2922,18 @@ def _run_terminal_session(ws_url: str) -> int:
|
|
|
2792
2922
|
result["code"] = 0
|
|
2793
2923
|
stop.set()
|
|
2794
2924
|
return
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2925
|
+
if not isinstance(raw, bytes):
|
|
2926
|
+
raise RuntimeAPIError("terminal received a non-binary frame")
|
|
2927
|
+
frame_type, payload = _parse_terminal_frame(raw)
|
|
2928
|
+
if frame_type == TERMINAL_FRAME_HEARTBEAT:
|
|
2798
2929
|
continue
|
|
2799
|
-
if
|
|
2800
|
-
data = _filter_terminal_output(
|
|
2930
|
+
if frame_type == TERMINAL_FRAME_DATA:
|
|
2931
|
+
data = _filter_terminal_output(payload.decode("utf-8", errors="ignore"), output_state)
|
|
2801
2932
|
if data:
|
|
2802
2933
|
sys.stdout.write(data)
|
|
2803
2934
|
sys.stdout.flush()
|
|
2804
2935
|
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")
|
|
2936
|
+
if frame_type in (TERMINAL_FRAME_SHUTDOWN, TERMINAL_FRAME_SESSION_CLOSE):
|
|
2815
2937
|
stop.set()
|
|
2816
2938
|
return
|
|
2817
2939
|
except ConnectionClosed:
|
|
@@ -2843,9 +2965,11 @@ def _run_terminal_session(ws_url: str) -> int:
|
|
|
2843
2965
|
if not data:
|
|
2844
2966
|
stop.set()
|
|
2845
2967
|
break
|
|
2846
|
-
ws.send(
|
|
2968
|
+
ws.send(_encode_terminal_frame(TERMINAL_FRAME_DATA, data))
|
|
2847
2969
|
finally:
|
|
2848
2970
|
stop.set()
|
|
2971
|
+
with contextlib.suppress(Exception):
|
|
2972
|
+
ws.send(_encode_terminal_frame(TERMINAL_FRAME_SHUTDOWN))
|
|
2849
2973
|
with contextlib.suppress(Exception):
|
|
2850
2974
|
ws.close()
|
|
2851
2975
|
termios_mod.tcsetattr(fd, termios_mod.TCSADRAIN, old_settings)
|
|
@@ -2935,7 +3059,7 @@ def _strip_terminal_noise(text: str) -> str:
|
|
|
2935
3059
|
|
|
2936
3060
|
def _send_terminal_resize(ws: Any) -> None:
|
|
2937
3061
|
size = shutil.get_terminal_size((100, 24))
|
|
2938
|
-
ws.send(
|
|
3062
|
+
ws.send(_encode_terminal_frame(TERMINAL_FRAME_WINDOW_SIZE, struct.pack(">II", size.columns, size.lines)))
|
|
2939
3063
|
|
|
2940
3064
|
|
|
2941
3065
|
# --------------------------------------------------------------------------- #
|
|
@@ -3022,7 +3146,7 @@ def _vm_detail_menu(client: RuntimeClient, vm: dict[str, Any]) -> str:
|
|
|
3022
3146
|
choices.append(questionary.Choice(title="⌨ enter console", value="enter"))
|
|
3023
3147
|
if internal_status == "running":
|
|
3024
3148
|
choices.append(questionary.Choice(title="▶ run a command", value="run"))
|
|
3025
|
-
choices.append(questionary.Choice(title="⤴ publish
|
|
3149
|
+
choices.append(questionary.Choice(title="⤴ publish service", value="publish"))
|
|
3026
3150
|
if power_state == "cold":
|
|
3027
3151
|
choices.append(questionary.Choice(title="☀ start / wake", value="start"))
|
|
3028
3152
|
choices.extend(
|
|
@@ -3030,7 +3154,7 @@ def _vm_detail_menu(client: RuntimeClient, vm: dict[str, Any]) -> str:
|
|
|
3030
3154
|
questionary.Choice(title="⧉ copy public url", value="copy-url"),
|
|
3031
3155
|
questionary.Choice(title="↗ open in browser", value="open-url"),
|
|
3032
3156
|
questionary.Choice(title=f"⚑ make {flip_visibility}", value=f"share:{flip_visibility}"),
|
|
3033
|
-
questionary.Choice(title="⚙
|
|
3157
|
+
questionary.Choice(title="⚙ service…", value="startup"),
|
|
3034
3158
|
questionary.Choice(title="ℹ refresh info", value="info"),
|
|
3035
3159
|
questionary.Separator(),
|
|
3036
3160
|
questionary.Choice(title="✕ delete", value="delete"),
|
|
@@ -3080,13 +3204,14 @@ def _vm_detail_menu(client: RuntimeClient, vm: dict[str, Any]) -> str:
|
|
|
3080
3204
|
if port <= 0 or port > 65535:
|
|
3081
3205
|
_UI.err().print("[red]port must be between 1 and 65535[/red]")
|
|
3082
3206
|
continue
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3207
|
+
command = _prompt_text("Command to run as the service")
|
|
3208
|
+
if not command:
|
|
3209
|
+
continue
|
|
3210
|
+
result = _with_spinner(
|
|
3211
|
+
f"publishing service on {slug}…",
|
|
3212
|
+
lambda: client.publish_port(vm_id, port, command=command),
|
|
3089
3213
|
)
|
|
3214
|
+
_render_startup_panel(result, title=f"service: {slug}")
|
|
3090
3215
|
continue
|
|
3091
3216
|
|
|
3092
3217
|
if action == "info":
|
|
@@ -3143,11 +3268,11 @@ def _vm_detail_menu(client: RuntimeClient, vm: dict[str, Any]) -> str:
|
|
|
3143
3268
|
|
|
3144
3269
|
|
|
3145
3270
|
def _vm_startup_submenu(client: RuntimeClient, vm_id: str, slug: str) -> None:
|
|
3146
|
-
"""Inner submenu for durable
|
|
3271
|
+
"""Inner submenu for the durable service."""
|
|
3147
3272
|
questionary = _UI.q()
|
|
3148
3273
|
while True:
|
|
3149
|
-
current = _with_spinner("fetching
|
|
3150
|
-
_render_startup_panel(current, title=f"
|
|
3274
|
+
current = _with_spinner("fetching service…", lambda: client.get_service(vm_id))
|
|
3275
|
+
_render_startup_panel(current, title=f"service: {slug}")
|
|
3151
3276
|
configured = bool(current.get("configured"))
|
|
3152
3277
|
choices: list[Any] = [
|
|
3153
3278
|
questionary.Choice(title="✎ set / replace", value="set"),
|
|
@@ -3160,11 +3285,11 @@ def _vm_startup_submenu(client: RuntimeClient, vm_id: str, slug: str) -> None:
|
|
|
3160
3285
|
questionary.Choice(title="← back", value="__back__"),
|
|
3161
3286
|
]
|
|
3162
3287
|
)
|
|
3163
|
-
action = _select_prompt("
|
|
3288
|
+
action = _select_prompt("service", choices).unsafe_ask()
|
|
3164
3289
|
if action in (None, "__back__"):
|
|
3165
3290
|
return
|
|
3166
3291
|
if action == "set":
|
|
3167
|
-
command = _prompt_text("
|
|
3292
|
+
command = _prompt_text("Service command", default=str(current.get("command") or ""))
|
|
3168
3293
|
if not command:
|
|
3169
3294
|
_UI.console().print("[dim]cancelled[/dim]")
|
|
3170
3295
|
continue
|
|
@@ -3182,20 +3307,20 @@ def _vm_startup_submenu(client: RuntimeClient, vm_id: str, slug: str) -> None:
|
|
|
3182
3307
|
continue
|
|
3183
3308
|
cwd = _prompt_text("Working directory (optional)", default=str(current.get("cwd") or ""))
|
|
3184
3309
|
_with_spinner(
|
|
3185
|
-
"
|
|
3186
|
-
lambda: client.
|
|
3310
|
+
"starting service…",
|
|
3311
|
+
lambda: client.run_service(vm_id, command=command, cwd=cwd or None, port=port),
|
|
3187
3312
|
)
|
|
3188
|
-
_UI.console().print(f"[green]✓[/green]
|
|
3313
|
+
_UI.console().print(f"[green]✓[/green] service started for [bold]{slug}[/bold]")
|
|
3189
3314
|
continue
|
|
3190
3315
|
if action == "clear":
|
|
3191
|
-
if not _prompt_confirm(f"Clear
|
|
3316
|
+
if not _prompt_confirm(f"Clear service for {slug}?", default=False):
|
|
3192
3317
|
_UI.console().print("[dim]cancelled[/dim]")
|
|
3193
3318
|
continue
|
|
3194
3319
|
_with_spinner(
|
|
3195
|
-
"clearing
|
|
3196
|
-
lambda: client.
|
|
3320
|
+
"clearing service…",
|
|
3321
|
+
lambda: client.clear_service(vm_id),
|
|
3197
3322
|
)
|
|
3198
|
-
_UI.console().print(f"[green]✓[/green] cleared
|
|
3323
|
+
_UI.console().print(f"[green]✓[/green] cleared service for [bold]{slug}[/bold]")
|
|
3199
3324
|
continue
|
|
3200
3325
|
|
|
3201
3326
|
|
|
@@ -3209,7 +3334,6 @@ _HELP_TOPICS: dict[str, tuple[str, ...]] = {
|
|
|
3209
3334
|
"computers": ("Computers",),
|
|
3210
3335
|
"share": ("Sharing",),
|
|
3211
3336
|
"sharing": ("Sharing",),
|
|
3212
|
-
"startup": ("Startup & service",),
|
|
3213
3337
|
"service": ("Startup & service",),
|
|
3214
3338
|
"proxy": ("Proxy",),
|
|
3215
3339
|
"github": ("GitHub",),
|
|
@@ -3251,7 +3375,7 @@ _runtime_complete() {
|
|
|
3251
3375
|
COMPREPLY=()
|
|
3252
3376
|
cur="${COMP_WORDS[COMP_CWORD]}"
|
|
3253
3377
|
if [[ ${COMP_CWORD} -eq 1 ]]; then
|
|
3254
|
-
COMPREPLY=( $(compgen -W "create list ls info url start enter run publish delete rm share
|
|
3378
|
+
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
3379
|
return
|
|
3256
3380
|
fi
|
|
3257
3381
|
sub="${COMP_WORDS[1]}"
|
|
@@ -3295,7 +3419,6 @@ _runtime() {
|
|
|
3295
3419
|
'delete:Delete a computer'
|
|
3296
3420
|
'rm:Delete a computer'
|
|
3297
3421
|
'share:Set visibility'
|
|
3298
|
-
'startup:Startup config'
|
|
3299
3422
|
'service:Service config'
|
|
3300
3423
|
'proxy:Port proxies'
|
|
3301
3424
|
'switch:Open a GitHub branch workspace'
|
|
@@ -3363,7 +3486,6 @@ complete -c runtime -n '__fish_use_subcommand' -a publish -d 'Publish port'
|
|
|
3363
3486
|
complete -c runtime -n '__fish_use_subcommand' -a delete -d 'Delete'
|
|
3364
3487
|
complete -c runtime -n '__fish_use_subcommand' -a rm -d 'Delete'
|
|
3365
3488
|
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
3489
|
complete -c runtime -n '__fish_use_subcommand' -a service -d 'Service config'
|
|
3368
3490
|
complete -c runtime -n '__fish_use_subcommand' -a proxy -d 'Port proxies'
|
|
3369
3491
|
complete -c runtime -n '__fish_use_subcommand' -a github -d 'GitHub connections'
|
|
@@ -3371,7 +3493,7 @@ complete -c runtime -n '__fish_use_subcommand' -a integrate -d 'Connect GitHub'
|
|
|
3371
3493
|
complete -c runtime -n '__fish_use_subcommand' -a help -d 'Extended help'
|
|
3372
3494
|
complete -c runtime -n '__fish_use_subcommand' -a completion -d 'Completion'
|
|
3373
3495
|
|
|
3374
|
-
complete -c runtime -n '__fish_seen_subcommand_from info url start enter run publish delete rm
|
|
3496
|
+
complete -c runtime -n '__fish_seen_subcommand_from info url start enter run publish delete rm service' -a '(__runtime_slugs)'
|
|
3375
3497
|
complete -c runtime -n '__fish_seen_subcommand_from completion' -a 'bash zsh fish'
|
|
3376
3498
|
complete -c runtime -n '__fish_seen_subcommand_from share' -a 'public private'
|
|
3377
3499
|
"""
|
|
@@ -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.29
|
|
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
|