runtime-sdk 0.4.35__tar.gz → 0.4.37__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.35 → runtime_sdk-0.4.37}/PKG-INFO +5 -2
- {runtime_sdk-0.4.35 → runtime_sdk-0.4.37}/README.md +4 -1
- {runtime_sdk-0.4.35 → runtime_sdk-0.4.37}/pyproject.toml +1 -1
- {runtime_sdk-0.4.35 → runtime_sdk-0.4.37}/scripts/runtime_sdk/cli.py +89 -22
- {runtime_sdk-0.4.35 → runtime_sdk-0.4.37}/scripts/runtime_sdk/client.py +5 -2
- {runtime_sdk-0.4.35 → runtime_sdk-0.4.37}/scripts/runtime_sdk.egg-info/PKG-INFO +5 -2
- {runtime_sdk-0.4.35 → runtime_sdk-0.4.37}/scripts/runtime_sdk/__init__.py +0 -0
- {runtime_sdk-0.4.35 → runtime_sdk-0.4.37}/scripts/runtime_sdk/config.py +0 -0
- {runtime_sdk-0.4.35 → runtime_sdk-0.4.37}/scripts/runtime_sdk.egg-info/SOURCES.txt +0 -0
- {runtime_sdk-0.4.35 → runtime_sdk-0.4.37}/scripts/runtime_sdk.egg-info/dependency_links.txt +0 -0
- {runtime_sdk-0.4.35 → runtime_sdk-0.4.37}/scripts/runtime_sdk.egg-info/entry_points.txt +0 -0
- {runtime_sdk-0.4.35 → runtime_sdk-0.4.37}/scripts/runtime_sdk.egg-info/requires.txt +0 -0
- {runtime_sdk-0.4.35 → runtime_sdk-0.4.37}/scripts/runtime_sdk.egg-info/top_level.txt +0 -0
- {runtime_sdk-0.4.35 → runtime_sdk-0.4.37}/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.37
|
|
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
|
|
@@ -76,7 +76,9 @@ runtime switch -c feature/login --repo owner/repo # create a new branch worksp
|
|
|
76
76
|
runtime checkout --repo owner/repo feature/login # alias for switch
|
|
77
77
|
runtime create # creates a computer with the starter app already published
|
|
78
78
|
runtime create myapp --command "python3 app.py" --cwd /home/ubuntu --port 3000
|
|
79
|
-
runtime enter <name-or-id> # accepts slug/name
|
|
79
|
+
runtime enter <name-or-id> # open an interactive shell; accepts slug/name or computer id
|
|
80
|
+
runtime enter <name-or-id> -- claude # open a real TTY and run an interactive command
|
|
81
|
+
runtime enter <name-or-id> -- python # use enter for REPLs, agents, prompts, and full-screen apps
|
|
80
82
|
runtime list
|
|
81
83
|
runtime info <id>
|
|
82
84
|
runtime share public <id>
|
|
@@ -86,6 +88,7 @@ runtime run <id> "echo hello" # one-shot foreground command only
|
|
|
86
88
|
runtime run <id> "apt install -y nodejs" --uid 0
|
|
87
89
|
runtime exec <id> -- bash -lc 'for i in 1 2 3; do echo $i; sleep 1; done'
|
|
88
90
|
printf 'hello' | runtime exec <id> --stdin -- cat
|
|
91
|
+
# Use runtime exec for automation and exact exit codes; use runtime enter -- <cmd> for interactive TTY commands.
|
|
89
92
|
runtime files ls <id> /home/ubuntu
|
|
90
93
|
runtime files read <id> /home/ubuntu/app.py --output app.py
|
|
91
94
|
runtime files write <id> /home/ubuntu/app.py --input app.py --mode 0644
|
|
@@ -54,7 +54,9 @@ runtime switch -c feature/login --repo owner/repo # create a new branch worksp
|
|
|
54
54
|
runtime checkout --repo owner/repo feature/login # alias for switch
|
|
55
55
|
runtime create # creates a computer with the starter app already published
|
|
56
56
|
runtime create myapp --command "python3 app.py" --cwd /home/ubuntu --port 3000
|
|
57
|
-
runtime enter <name-or-id> # accepts slug/name
|
|
57
|
+
runtime enter <name-or-id> # open an interactive shell; accepts slug/name or computer id
|
|
58
|
+
runtime enter <name-or-id> -- claude # open a real TTY and run an interactive command
|
|
59
|
+
runtime enter <name-or-id> -- python # use enter for REPLs, agents, prompts, and full-screen apps
|
|
58
60
|
runtime list
|
|
59
61
|
runtime info <id>
|
|
60
62
|
runtime share public <id>
|
|
@@ -64,6 +66,7 @@ runtime run <id> "echo hello" # one-shot foreground command only
|
|
|
64
66
|
runtime run <id> "apt install -y nodejs" --uid 0
|
|
65
67
|
runtime exec <id> -- bash -lc 'for i in 1 2 3; do echo $i; sleep 1; done'
|
|
66
68
|
printf 'hello' | runtime exec <id> --stdin -- cat
|
|
69
|
+
# Use runtime exec for automation and exact exit codes; use runtime enter -- <cmd> for interactive TTY commands.
|
|
67
70
|
runtime files ls <id> /home/ubuntu
|
|
68
71
|
runtime files read <id> /home/ubuntu/app.py --output app.py
|
|
69
72
|
runtime files write <id> /home/ubuntu/app.py --input app.py --mode 0644
|
|
@@ -189,9 +189,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
189
189
|
secrets_set = secrets_subparsers.add_parser("set", help="Create or replace a secret set from KEY=VALUE pairs")
|
|
190
190
|
secrets_set.add_argument("slug", nargs="?", default=None, help="Secret set slug")
|
|
191
191
|
secrets_set.add_argument("pairs", nargs="*", help="KEY=VALUE pairs")
|
|
192
|
+
secrets_set.add_argument("--proxy", action="append", default=[], metavar="KEY=HOST", help="Deliver KEY through the egress proxy and inject it only for HOST")
|
|
192
193
|
secrets_import = secrets_subparsers.add_parser("import", help="Create or replace a secret set from a .env file")
|
|
193
194
|
secrets_import.add_argument("slug", nargs="?", default=None, help="Secret set slug")
|
|
194
195
|
secrets_import.add_argument("--env-file", required=True, help="Path to .env file")
|
|
196
|
+
secrets_import.add_argument("--proxy", action="append", default=[], metavar="KEY=HOST", help="Deliver KEY through the egress proxy and inject it only for HOST")
|
|
195
197
|
secrets_delete = secrets_subparsers.add_parser("delete", aliases=["rm"], help="Delete a secret set")
|
|
196
198
|
secrets_delete.add_argument("slug", nargs="?", default=None, help="Secret set slug")
|
|
197
199
|
|
|
@@ -238,8 +240,13 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
238
240
|
help="Re-render the table every N seconds (default 2)",
|
|
239
241
|
)
|
|
240
242
|
|
|
241
|
-
enter_cmd = subparsers.add_parser(
|
|
243
|
+
enter_cmd = subparsers.add_parser(
|
|
244
|
+
"enter",
|
|
245
|
+
help="Enter a computer's console",
|
|
246
|
+
epilog="Use `runtime exec` for exact exit codes; use `runtime enter <id> -- <cmd>` for interactive TTY commands.",
|
|
247
|
+
)
|
|
242
248
|
enter_cmd.add_argument("id", nargs="?", default=None, help="Computer ID (hostname)")
|
|
249
|
+
enter_cmd.add_argument("enter_command", nargs="*", help="Command to run in the interactive terminal")
|
|
243
250
|
|
|
244
251
|
info_cmd = subparsers.add_parser("info", help="Get computer details")
|
|
245
252
|
info_cmd.add_argument("id", nargs="?", default=None, help="Computer ID (hostname)")
|
|
@@ -408,7 +415,7 @@ _HELP_SECTIONS: tuple[tuple[str, tuple[tuple[str, str], ...]], ...] = (
|
|
|
408
415
|
("info [id]", "Show computer details"),
|
|
409
416
|
("url [id]", "Show a computer's public URL and live published port"),
|
|
410
417
|
("start [id]", "Wake a cold computer"),
|
|
411
|
-
("enter [id]", "Enter a computer's console
|
|
418
|
+
("enter [id] [-- <cmd>]", "Enter a computer's console or interactive TTY command"),
|
|
412
419
|
("run [id] <cmd>", "Run a one-shot command"),
|
|
413
420
|
("exec [id] -- <cmd>", "Stream stdout/stderr and optionally pipe stdin"),
|
|
414
421
|
("publish [id] <port> -- <cmd>", "Run and publish a durable service command"),
|
|
@@ -529,13 +536,17 @@ def _windows_unsupported() -> bool:
|
|
|
529
536
|
|
|
530
537
|
|
|
531
538
|
def main(argv: list[str] | None = None) -> int:
|
|
539
|
+
raw_argv = list(sys.argv[1:] if argv is None else argv)
|
|
532
540
|
parser = build_parser()
|
|
533
|
-
args = parser.parse_args(
|
|
541
|
+
args = parser.parse_args(raw_argv)
|
|
534
542
|
|
|
535
543
|
if _windows_unsupported():
|
|
536
544
|
return report_error("runtime-sdk supports macOS and Linux only; Windows is not supported")
|
|
537
545
|
|
|
538
546
|
try:
|
|
547
|
+
if args.command == "enter" and args.enter_command and "--" not in raw_argv:
|
|
548
|
+
return report_error("enter command must be separated from the computer id with --")
|
|
549
|
+
|
|
539
550
|
config = load_config()
|
|
540
551
|
if args.base_url:
|
|
541
552
|
config.base_url = args.base_url
|
|
@@ -580,9 +591,9 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
580
591
|
if args.secrets_action == "show":
|
|
581
592
|
return handle_secrets_show(config, args.slug)
|
|
582
593
|
if args.secrets_action == "set":
|
|
583
|
-
return handle_secrets_set(config, args.slug, args.pairs)
|
|
594
|
+
return handle_secrets_set(config, args.slug, args.pairs, args.proxy)
|
|
584
595
|
if args.secrets_action == "import":
|
|
585
|
-
return handle_secrets_import(config, args.slug, args.env_file)
|
|
596
|
+
return handle_secrets_import(config, args.slug, args.env_file, args.proxy)
|
|
586
597
|
if args.secrets_action in ("delete", "rm"):
|
|
587
598
|
return handle_secrets_delete(config, args.slug)
|
|
588
599
|
return report_error("unknown secrets command")
|
|
@@ -621,7 +632,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
621
632
|
if args.command == "start":
|
|
622
633
|
return handle_start(config, args.id)
|
|
623
634
|
if args.command == "enter":
|
|
624
|
-
return handle_enter(config, args.id)
|
|
635
|
+
return handle_enter(config, args.id, args.enter_command)
|
|
625
636
|
if args.command in ("delete", "rm"):
|
|
626
637
|
return handle_delete(config, args.id, force=getattr(args, "force", False))
|
|
627
638
|
if args.command == "run":
|
|
@@ -1591,7 +1602,15 @@ def _render_secret_set_panel(payload: dict[str, Any]) -> None:
|
|
|
1591
1602
|
body.append(f"{'slug':<12}", style="dim")
|
|
1592
1603
|
body.append(f"{payload.get('slug') or '—'}\n")
|
|
1593
1604
|
body.append(f"{'keys':<12}", style="dim")
|
|
1594
|
-
|
|
1605
|
+
deliveries = payload.get("deliveries") or {}
|
|
1606
|
+
lines = []
|
|
1607
|
+
for key in payload.get("keys") or []:
|
|
1608
|
+
meta = deliveries.get(str(key)) if isinstance(deliveries, dict) else None
|
|
1609
|
+
if isinstance(meta, dict) and meta.get("delivery") == "proxy":
|
|
1610
|
+
lines.append(f"{key} proxy {meta.get('target_host') or '—'}")
|
|
1611
|
+
else:
|
|
1612
|
+
lines.append(f"{key} env")
|
|
1613
|
+
body.append("\n".join(lines) or "—")
|
|
1595
1614
|
_UI.console().print(Panel(body, title="secret set", border_style="cyan", expand=False))
|
|
1596
1615
|
|
|
1597
1616
|
|
|
@@ -1880,6 +1899,26 @@ def _parse_secret_pairs(pairs: list[str] | None) -> dict[str, str]:
|
|
|
1880
1899
|
|
|
1881
1900
|
|
|
1882
1901
|
|
|
1902
|
+
def _parse_secret_delivery(proxy_specs: list[str] | None, values: dict[str, str]) -> dict[str, dict[str, str]]:
|
|
1903
|
+
delivery: dict[str, dict[str, str]] = {}
|
|
1904
|
+
for spec in proxy_specs or []:
|
|
1905
|
+
if "=" not in spec:
|
|
1906
|
+
raise RuntimeConfigError(f"invalid proxy delivery: {spec}")
|
|
1907
|
+
key, host = spec.split("=", 1)
|
|
1908
|
+
key = key.strip()
|
|
1909
|
+
host = host.strip()
|
|
1910
|
+
if not key or not host or key not in values:
|
|
1911
|
+
raise RuntimeConfigError(f"invalid proxy delivery: {spec}")
|
|
1912
|
+
delivery[key] = {
|
|
1913
|
+
"delivery": "proxy",
|
|
1914
|
+
"target_host": host,
|
|
1915
|
+
"header": "Authorization",
|
|
1916
|
+
"scheme": "Bearer",
|
|
1917
|
+
}
|
|
1918
|
+
return delivery
|
|
1919
|
+
|
|
1920
|
+
|
|
1921
|
+
|
|
1883
1922
|
def _parse_env_file(path: str) -> dict[str, str]:
|
|
1884
1923
|
try:
|
|
1885
1924
|
lines = Path(path).read_text(encoding="utf-8").splitlines()
|
|
@@ -1929,7 +1968,7 @@ def handle_secrets_show(config: RuntimeConfig, slug: str | None) -> int:
|
|
|
1929
1968
|
|
|
1930
1969
|
|
|
1931
1970
|
|
|
1932
|
-
def handle_secrets_set(config: RuntimeConfig, slug: str | None, pairs: list[str] | None) -> int:
|
|
1971
|
+
def handle_secrets_set(config: RuntimeConfig, slug: str | None, pairs: list[str] | None, proxy_specs: list[str] | None) -> int:
|
|
1933
1972
|
if (err := _require_api_key(config)) is not None:
|
|
1934
1973
|
return err
|
|
1935
1974
|
resolved_slug = _coerce_secret_set_slug(slug)
|
|
@@ -1942,8 +1981,9 @@ def handle_secrets_set(config: RuntimeConfig, slug: str | None, pairs: list[str]
|
|
|
1942
1981
|
values = _parse_secret_pairs(pairs)
|
|
1943
1982
|
if not values:
|
|
1944
1983
|
return report_error("at least one KEY=VALUE pair is required")
|
|
1984
|
+
delivery = _parse_secret_delivery(proxy_specs, values)
|
|
1945
1985
|
client = RuntimeClient(base_url=config.base_url, api_key=config.api_key)
|
|
1946
|
-
secret_set = client.put_secret_set(resolved_slug, values)
|
|
1986
|
+
secret_set = client.put_secret_set(resolved_slug, values, delivery)
|
|
1947
1987
|
return report_success(
|
|
1948
1988
|
{"secret_set": secret_set},
|
|
1949
1989
|
lambda payload: _UI.console().print(f"[green]✓[/green] saved [bold]{payload['secret_set'].get('slug', resolved_slug)}[/bold]"),
|
|
@@ -1951,7 +1991,7 @@ def handle_secrets_set(config: RuntimeConfig, slug: str | None, pairs: list[str]
|
|
|
1951
1991
|
|
|
1952
1992
|
|
|
1953
1993
|
|
|
1954
|
-
def handle_secrets_import(config: RuntimeConfig, slug: str | None, env_file: str) -> int:
|
|
1994
|
+
def handle_secrets_import(config: RuntimeConfig, slug: str | None, env_file: str, proxy_specs: list[str] | None) -> int:
|
|
1955
1995
|
if (err := _require_api_key(config)) is not None:
|
|
1956
1996
|
return err
|
|
1957
1997
|
resolved_slug = _coerce_secret_set_slug(slug)
|
|
@@ -1964,8 +2004,9 @@ def handle_secrets_import(config: RuntimeConfig, slug: str | None, env_file: str
|
|
|
1964
2004
|
values = _parse_env_file(env_file)
|
|
1965
2005
|
if not values:
|
|
1966
2006
|
return report_error("env file did not contain any secrets")
|
|
2007
|
+
delivery = _parse_secret_delivery(proxy_specs, values)
|
|
1967
2008
|
client = RuntimeClient(base_url=config.base_url, api_key=config.api_key)
|
|
1968
|
-
secret_set = client.put_secret_set(resolved_slug, values)
|
|
2009
|
+
secret_set = client.put_secret_set(resolved_slug, values, delivery)
|
|
1969
2010
|
return report_success(
|
|
1970
2011
|
{"secret_set": secret_set},
|
|
1971
2012
|
lambda payload: _UI.console().print(f"[green]✓[/green] saved [bold]{payload['secret_set'].get('slug', resolved_slug)}[/bold] from [bold]{env_file}[/bold]"),
|
|
@@ -2585,12 +2626,14 @@ def handle_start(config: RuntimeConfig, computer_id: str | None) -> int:
|
|
|
2585
2626
|
return report_success(result, render)
|
|
2586
2627
|
|
|
2587
2628
|
|
|
2588
|
-
def handle_enter(config: RuntimeConfig, computer_id: str | None) -> int:
|
|
2629
|
+
def handle_enter(config: RuntimeConfig, computer_id: str | None, command_parts: list[str] | None = None) -> int:
|
|
2589
2630
|
if (err := _require_api_key(config)) is not None:
|
|
2590
2631
|
return err
|
|
2591
2632
|
if not _interactive():
|
|
2592
2633
|
return report_error("enter requires an interactive terminal")
|
|
2593
2634
|
|
|
2635
|
+
command = _command_from_remainder(command_parts)
|
|
2636
|
+
|
|
2594
2637
|
if computer_id is None:
|
|
2595
2638
|
picked = _pick_computer(
|
|
2596
2639
|
config,
|
|
@@ -2605,6 +2648,8 @@ def handle_enter(config: RuntimeConfig, computer_id: str | None) -> int:
|
|
|
2605
2648
|
|
|
2606
2649
|
client = RuntimeClient(base_url=config.base_url, api_key=config.api_key)
|
|
2607
2650
|
computer_id = _resolve_computer_ref(client, str(computer_id))
|
|
2651
|
+
if command:
|
|
2652
|
+
return _enter_computer(config, str(computer_id), command=command)
|
|
2608
2653
|
return _enter_computer(config, str(computer_id))
|
|
2609
2654
|
|
|
2610
2655
|
|
|
@@ -3190,7 +3235,7 @@ def _handle_proxy_client(base_url: str, api_key: str, computer_id: str, remote_p
|
|
|
3190
3235
|
conn.close()
|
|
3191
3236
|
|
|
3192
3237
|
|
|
3193
|
-
def _enter_computer(config: RuntimeConfig, computer_id: str) -> int:
|
|
3238
|
+
def _enter_computer(config: RuntimeConfig, computer_id: str, *, command: str | None = None) -> int:
|
|
3194
3239
|
client = RuntimeClient(base_url=config.base_url, api_key=config.api_key)
|
|
3195
3240
|
ticket_payload = _with_spinner(
|
|
3196
3241
|
f"preparing console for {computer_id}…",
|
|
@@ -3203,7 +3248,7 @@ def _enter_computer(config: RuntimeConfig, computer_id: str) -> int:
|
|
|
3203
3248
|
_UI.console().print("[cyan]●[/cyan] Connecting to console...")
|
|
3204
3249
|
|
|
3205
3250
|
try:
|
|
3206
|
-
return _run_terminal_session(ws_url)
|
|
3251
|
+
return _run_terminal_session(ws_url, bootstrap_command=command)
|
|
3207
3252
|
except RuntimeAPIError as exc:
|
|
3208
3253
|
return report_error(str(exc), status_code=exc.status_code)
|
|
3209
3254
|
except (InvalidHandshake, TimeoutError, OSError) as exc:
|
|
@@ -3242,7 +3287,7 @@ def _parse_terminal_frame(raw: bytes) -> tuple[int, bytes]:
|
|
|
3242
3287
|
return raw[0], raw[TERMINAL_FRAME_HEADER_SIZE:]
|
|
3243
3288
|
|
|
3244
3289
|
|
|
3245
|
-
def _run_terminal_session(ws_url: str) -> int:
|
|
3290
|
+
def _run_terminal_session(ws_url: str, *, bootstrap_command: str | None = None) -> int:
|
|
3246
3291
|
if not sys.stdin.isatty() or not sys.stdout.isatty():
|
|
3247
3292
|
return report_error("enter requires an interactive terminal")
|
|
3248
3293
|
|
|
@@ -3251,10 +3296,12 @@ def _run_terminal_session(ws_url: str) -> int:
|
|
|
3251
3296
|
old_settings = termios_mod.tcgetattr(fd)
|
|
3252
3297
|
stop = threading.Event()
|
|
3253
3298
|
result: dict[str, Any] = {"code": 0, "error": None}
|
|
3254
|
-
|
|
3299
|
+
bootstrap_echo = f"exec {bootstrap_command.strip()}" if bootstrap_command and bootstrap_command.strip() else ""
|
|
3300
|
+
output_state: dict[str, Any] = {"buffer": "", "suppressing_preamble": True, "bootstrap_echo": bootstrap_echo}
|
|
3255
3301
|
|
|
3256
3302
|
with websocket_connect(ws_url, open_timeout=DEFAULT_WARMUP_TIMEOUT, close_timeout=1) as ws:
|
|
3257
3303
|
_send_terminal_resize(ws)
|
|
3304
|
+
_send_terminal_bootstrap(ws, bootstrap_command)
|
|
3258
3305
|
|
|
3259
3306
|
def recv_loop() -> None:
|
|
3260
3307
|
try:
|
|
@@ -3332,7 +3379,7 @@ def _filter_terminal_output(chunk: str, state: dict[str, Any]) -> str:
|
|
|
3332
3379
|
if not chunk:
|
|
3333
3380
|
return ""
|
|
3334
3381
|
if not state.get("suppressing_preamble", False):
|
|
3335
|
-
return _strip_terminal_noise(chunk)
|
|
3382
|
+
return _strip_terminal_noise(chunk, str(state.get("bootstrap_echo") or ""))
|
|
3336
3383
|
|
|
3337
3384
|
buffer = str(state.get("buffer") or "") + chunk
|
|
3338
3385
|
marker = "\x1b[1;34mruntime@"
|
|
@@ -3340,9 +3387,9 @@ def _filter_terminal_output(chunk: str, state: dict[str, Any]) -> str:
|
|
|
3340
3387
|
if idx != -1:
|
|
3341
3388
|
state["suppressing_preamble"] = False
|
|
3342
3389
|
state["buffer"] = ""
|
|
3343
|
-
return _strip_terminal_noise(buffer[idx:])
|
|
3390
|
+
return _strip_terminal_noise(buffer[idx:], str(state.get("bootstrap_echo") or ""))
|
|
3344
3391
|
|
|
3345
|
-
cleaned = _strip_terminal_noise(buffer)
|
|
3392
|
+
cleaned = _strip_terminal_noise(buffer, str(state.get("bootstrap_echo") or ""))
|
|
3346
3393
|
if cleaned and _should_release_terminal_output(cleaned):
|
|
3347
3394
|
state["suppressing_preamble"] = False
|
|
3348
3395
|
state["buffer"] = ""
|
|
@@ -3351,7 +3398,7 @@ def _filter_terminal_output(chunk: str, state: dict[str, Any]) -> str:
|
|
|
3351
3398
|
if len(buffer) > 8192:
|
|
3352
3399
|
state["suppressing_preamble"] = False
|
|
3353
3400
|
state["buffer"] = ""
|
|
3354
|
-
return _strip_terminal_noise(buffer)
|
|
3401
|
+
return _strip_terminal_noise(buffer, str(state.get("bootstrap_echo") or ""))
|
|
3355
3402
|
|
|
3356
3403
|
state["buffer"] = buffer
|
|
3357
3404
|
return ""
|
|
@@ -3374,10 +3421,13 @@ def _should_release_terminal_output(text: str) -> bool:
|
|
|
3374
3421
|
def _flush_terminal_output(state: dict[str, Any]) -> str:
|
|
3375
3422
|
buffer = str(state.get("buffer") or "")
|
|
3376
3423
|
state["buffer"] = ""
|
|
3377
|
-
return _strip_terminal_noise(buffer)
|
|
3424
|
+
return _strip_terminal_noise(buffer, str(state.get("bootstrap_echo") or ""))
|
|
3425
|
+
|
|
3378
3426
|
|
|
3427
|
+
ANSI_ESCAPE_RE = re.compile(r"\x1b\[[0-?]*[ -/]*[@-~]")
|
|
3379
3428
|
|
|
3380
|
-
|
|
3429
|
+
|
|
3430
|
+
def _strip_terminal_noise(text: str, bootstrap_echo: str = "") -> str:
|
|
3381
3431
|
if not text:
|
|
3382
3432
|
return ""
|
|
3383
3433
|
|
|
@@ -3395,10 +3445,27 @@ def _strip_terminal_noise(text: str) -> str:
|
|
|
3395
3445
|
continue
|
|
3396
3446
|
if stripped == "* Docs: https://docs.slicervm.com":
|
|
3397
3447
|
continue
|
|
3448
|
+
if _is_terminal_bootstrap_echo(stripped, bootstrap_echo):
|
|
3449
|
+
continue
|
|
3398
3450
|
filtered.append(line)
|
|
3399
3451
|
return "".join(filtered)
|
|
3400
3452
|
|
|
3401
3453
|
|
|
3454
|
+
def _is_terminal_bootstrap_echo(line: str, bootstrap_echo: str) -> bool:
|
|
3455
|
+
bootstrap_echo = bootstrap_echo.strip()
|
|
3456
|
+
if not bootstrap_echo:
|
|
3457
|
+
return False
|
|
3458
|
+
clean = ANSI_ESCAPE_RE.sub("", line).strip()
|
|
3459
|
+
return clean == bootstrap_echo or clean.endswith(bootstrap_echo)
|
|
3460
|
+
|
|
3461
|
+
|
|
3462
|
+
def _send_terminal_bootstrap(ws: Any, command: str | None) -> None:
|
|
3463
|
+
command = (command or "").strip()
|
|
3464
|
+
if not command:
|
|
3465
|
+
return
|
|
3466
|
+
ws.send(_encode_terminal_frame(TERMINAL_FRAME_DATA, f"exec {command}\n".encode()))
|
|
3467
|
+
|
|
3468
|
+
|
|
3402
3469
|
def _send_terminal_resize(ws: Any) -> None:
|
|
3403
3470
|
size = shutil.get_terminal_size((100, 24))
|
|
3404
3471
|
ws.send(_encode_terminal_frame(TERMINAL_FRAME_WINDOW_SIZE, struct.pack(">II", size.columns, size.lines)))
|
|
@@ -75,8 +75,11 @@ class RuntimeClient:
|
|
|
75
75
|
def get_secret_set(self, slug: str) -> dict[str, Any]:
|
|
76
76
|
return self._request("GET", f"/api/secrets/{slug}", auth_required=True)
|
|
77
77
|
|
|
78
|
-
def put_secret_set(self, slug: str, values: dict[str, str]) -> dict[str, Any]:
|
|
79
|
-
|
|
78
|
+
def put_secret_set(self, slug: str, values: dict[str, str], delivery: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
79
|
+
payload: dict[str, Any] = {"values": values}
|
|
80
|
+
if delivery:
|
|
81
|
+
payload["deliveries"] = delivery
|
|
82
|
+
return self._request("PUT", f"/api/secrets/{slug}", json=payload, auth_required=True)
|
|
80
83
|
|
|
81
84
|
def delete_secret_set(self, slug: str) -> dict[str, Any]:
|
|
82
85
|
return self._request("DELETE", f"/api/secrets/{slug}", auth_required=True)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: runtime-sdk
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.37
|
|
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
|
|
@@ -76,7 +76,9 @@ runtime switch -c feature/login --repo owner/repo # create a new branch worksp
|
|
|
76
76
|
runtime checkout --repo owner/repo feature/login # alias for switch
|
|
77
77
|
runtime create # creates a computer with the starter app already published
|
|
78
78
|
runtime create myapp --command "python3 app.py" --cwd /home/ubuntu --port 3000
|
|
79
|
-
runtime enter <name-or-id> # accepts slug/name
|
|
79
|
+
runtime enter <name-or-id> # open an interactive shell; accepts slug/name or computer id
|
|
80
|
+
runtime enter <name-or-id> -- claude # open a real TTY and run an interactive command
|
|
81
|
+
runtime enter <name-or-id> -- python # use enter for REPLs, agents, prompts, and full-screen apps
|
|
80
82
|
runtime list
|
|
81
83
|
runtime info <id>
|
|
82
84
|
runtime share public <id>
|
|
@@ -86,6 +88,7 @@ runtime run <id> "echo hello" # one-shot foreground command only
|
|
|
86
88
|
runtime run <id> "apt install -y nodejs" --uid 0
|
|
87
89
|
runtime exec <id> -- bash -lc 'for i in 1 2 3; do echo $i; sleep 1; done'
|
|
88
90
|
printf 'hello' | runtime exec <id> --stdin -- cat
|
|
91
|
+
# Use runtime exec for automation and exact exit codes; use runtime enter -- <cmd> for interactive TTY commands.
|
|
89
92
|
runtime files ls <id> /home/ubuntu
|
|
90
93
|
runtime files read <id> /home/ubuntu/app.py --output app.py
|
|
91
94
|
runtime files write <id> /home/ubuntu/app.py --input app.py --mode 0644
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|