runtime-sdk 0.4.20__tar.gz → 0.4.22__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.20 → runtime_sdk-0.4.22}/PKG-INFO +3 -1
- {runtime_sdk-0.4.20 → runtime_sdk-0.4.22}/README.md +2 -0
- {runtime_sdk-0.4.20 → runtime_sdk-0.4.22}/pyproject.toml +1 -1
- {runtime_sdk-0.4.20 → runtime_sdk-0.4.22}/scripts/runtime_sdk/cli.py +595 -27
- {runtime_sdk-0.4.20 → runtime_sdk-0.4.22}/scripts/runtime_sdk/client.py +8 -0
- {runtime_sdk-0.4.20 → runtime_sdk-0.4.22}/scripts/runtime_sdk.egg-info/PKG-INFO +3 -1
- {runtime_sdk-0.4.20 → runtime_sdk-0.4.22}/scripts/runtime_sdk/__init__.py +0 -0
- {runtime_sdk-0.4.20 → runtime_sdk-0.4.22}/scripts/runtime_sdk/config.py +0 -0
- {runtime_sdk-0.4.20 → runtime_sdk-0.4.22}/scripts/runtime_sdk.egg-info/SOURCES.txt +0 -0
- {runtime_sdk-0.4.20 → runtime_sdk-0.4.22}/scripts/runtime_sdk.egg-info/dependency_links.txt +0 -0
- {runtime_sdk-0.4.20 → runtime_sdk-0.4.22}/scripts/runtime_sdk.egg-info/entry_points.txt +0 -0
- {runtime_sdk-0.4.20 → runtime_sdk-0.4.22}/scripts/runtime_sdk.egg-info/requires.txt +0 -0
- {runtime_sdk-0.4.20 → runtime_sdk-0.4.22}/scripts/runtime_sdk.egg-info/top_level.txt +0 -0
- {runtime_sdk-0.4.20 → runtime_sdk-0.4.22}/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.22
|
|
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
|
|
@@ -78,6 +78,8 @@ runtime create myapp --command "python3 app.py" --cwd /home/ubuntu --port 3000
|
|
|
78
78
|
runtime enter <name-or-id> # accepts slug/name like test, or the computer id
|
|
79
79
|
runtime list
|
|
80
80
|
runtime info <id>
|
|
81
|
+
runtime share public <id>
|
|
82
|
+
runtime share private <id>
|
|
81
83
|
runtime start <id>
|
|
82
84
|
runtime run <id> "echo hello" # one-shot foreground command only
|
|
83
85
|
runtime run <id> "apt install -y nodejs" --uid 0
|
|
@@ -56,6 +56,8 @@ runtime create myapp --command "python3 app.py" --cwd /home/ubuntu --port 3000
|
|
|
56
56
|
runtime enter <name-or-id> # accepts slug/name like test, or the computer id
|
|
57
57
|
runtime list
|
|
58
58
|
runtime info <id>
|
|
59
|
+
runtime share public <id>
|
|
60
|
+
runtime share private <id>
|
|
59
61
|
runtime start <id>
|
|
60
62
|
runtime run <id> "echo hello" # one-shot foreground command only
|
|
61
63
|
runtime run <id> "apt install -y nodejs" --uid 0
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
4
|
import contextlib
|
|
5
|
+
import difflib
|
|
5
6
|
import importlib
|
|
6
7
|
import json
|
|
7
8
|
import os
|
|
@@ -132,7 +133,10 @@ class _UI:
|
|
|
132
133
|
|
|
133
134
|
|
|
134
135
|
def build_parser() -> argparse.ArgumentParser:
|
|
135
|
-
parser = argparse.ArgumentParser(
|
|
136
|
+
parser = argparse.ArgumentParser(
|
|
137
|
+
prog="runtime",
|
|
138
|
+
description="Manage RuntimeVM computers — run code, publish apps, share URLs.",
|
|
139
|
+
)
|
|
136
140
|
parser.add_argument("--base-url", help="Override the Runtime API base URL")
|
|
137
141
|
parser.add_argument("--version", action="version", version=f"runtime-sdk {__version__}")
|
|
138
142
|
|
|
@@ -217,7 +221,17 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
217
221
|
create_cmd.add_argument("--command", dest="startup_command", help="Durable startup command to save on create")
|
|
218
222
|
create_cmd.add_argument("--cwd", help="Working directory for the durable startup command")
|
|
219
223
|
create_cmd.add_argument("--port", type=int, help="App port to publish for durable startup")
|
|
220
|
-
subparsers.add_parser("list", aliases=["ls"], help="List computers")
|
|
224
|
+
list_cmd = subparsers.add_parser("list", aliases=["ls"], help="List computers")
|
|
225
|
+
list_cmd.add_argument(
|
|
226
|
+
"-w",
|
|
227
|
+
"--watch",
|
|
228
|
+
nargs="?",
|
|
229
|
+
const=2,
|
|
230
|
+
type=int,
|
|
231
|
+
default=None,
|
|
232
|
+
metavar="SECONDS",
|
|
233
|
+
help="Re-render the table every N seconds (default 2)",
|
|
234
|
+
)
|
|
221
235
|
|
|
222
236
|
enter_cmd = subparsers.add_parser("enter", help="Enter a computer's console")
|
|
223
237
|
enter_cmd.add_argument("id", nargs="?", default=None, help="Computer ID (hostname)")
|
|
@@ -228,11 +242,24 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
228
242
|
url_cmd = subparsers.add_parser("url", help="Show a computer's public URL and live published port")
|
|
229
243
|
url_cmd.add_argument("id", nargs="?", default=None, help="Computer ID (hostname)")
|
|
230
244
|
|
|
245
|
+
share_cmd = subparsers.add_parser("share", help="Set whether a computer is public or private")
|
|
246
|
+
share_subparsers = share_cmd.add_subparsers(dest="share_action", required=True)
|
|
247
|
+
share_public = share_subparsers.add_parser("public", help="Make a computer publicly accessible")
|
|
248
|
+
share_public.add_argument("id", nargs="?", default=None, help="Computer ID (hostname)")
|
|
249
|
+
share_private = share_subparsers.add_parser("private", help="Require owner auth before browser access")
|
|
250
|
+
share_private.add_argument("id", nargs="?", default=None, help="Computer ID (hostname)")
|
|
251
|
+
|
|
231
252
|
start_cmd = subparsers.add_parser("start", help="Wake a cold computer")
|
|
232
253
|
start_cmd.add_argument("id", nargs="?", default=None, help="Computer ID (hostname)")
|
|
233
254
|
|
|
234
255
|
delete_cmd = subparsers.add_parser("delete", aliases=["rm"], help="Delete a computer")
|
|
235
256
|
delete_cmd.add_argument("id", nargs="?", default=None, help="Computer ID (hostname)")
|
|
257
|
+
delete_cmd.add_argument(
|
|
258
|
+
"--force",
|
|
259
|
+
"-y",
|
|
260
|
+
action="store_true",
|
|
261
|
+
help="Skip the typed-name confirmation prompt",
|
|
262
|
+
)
|
|
236
263
|
|
|
237
264
|
run_cmd = subparsers.add_parser("run", help="Run a one-shot command in a computer")
|
|
238
265
|
run_cmd.add_argument("id", nargs="?", default=None, help="Computer ID (hostname)")
|
|
@@ -290,9 +317,129 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
290
317
|
proxy_agent.add_argument("--local-port", type=int, required=True)
|
|
291
318
|
proxy_agent.add_argument("--remote-port", type=int, required=True)
|
|
292
319
|
|
|
320
|
+
help_cmd = subparsers.add_parser("help", help="Show extended help for a topic")
|
|
321
|
+
help_cmd.add_argument("topic", nargs="?", default=None, help="Topic to show (computer, share, account, all)")
|
|
322
|
+
|
|
323
|
+
completion_cmd = subparsers.add_parser(
|
|
324
|
+
"completion", help="Print shell completion script (bash, zsh, fish)"
|
|
325
|
+
)
|
|
326
|
+
completion_cmd.add_argument("shell", choices=["bash", "zsh", "fish"], help="Target shell")
|
|
327
|
+
|
|
328
|
+
parser.format_help = lambda: _format_top_help() # type: ignore[assignment]
|
|
293
329
|
return parser
|
|
294
330
|
|
|
295
331
|
|
|
332
|
+
_HELP_SECTIONS: tuple[tuple[str, tuple[tuple[str, str], ...]], ...] = (
|
|
333
|
+
(
|
|
334
|
+
"Computers",
|
|
335
|
+
(
|
|
336
|
+
("create [name]", "Create a new computer (starter app published by default)"),
|
|
337
|
+
("list, ls", "List computers (-w watches, refreshes every 2s)"),
|
|
338
|
+
("info [id]", "Show computer details"),
|
|
339
|
+
("url [id]", "Show a computer's public URL and live published port"),
|
|
340
|
+
("start [id]", "Wake a cold computer"),
|
|
341
|
+
("enter [id]", "Enter a computer's console (TTY)"),
|
|
342
|
+
("run [id] <cmd>", "Run a one-shot command"),
|
|
343
|
+
("publish [id] [port]", "Promote a running app on a port to the durable public URL"),
|
|
344
|
+
("delete, rm [id]", "Delete a computer (typed-name confirm; --force / -y to skip)"),
|
|
345
|
+
),
|
|
346
|
+
),
|
|
347
|
+
(
|
|
348
|
+
"Sharing",
|
|
349
|
+
(
|
|
350
|
+
("share public [id]", "Make a computer publicly accessible"),
|
|
351
|
+
("share private [id]", "Require owner auth before browser access"),
|
|
352
|
+
),
|
|
353
|
+
),
|
|
354
|
+
(
|
|
355
|
+
"Startup & service",
|
|
356
|
+
(
|
|
357
|
+
("startup show [id]", "Show durable startup config"),
|
|
358
|
+
("startup set [id] --command … --port …", "Save durable startup command"),
|
|
359
|
+
("startup clear [id]", "Clear durable startup config"),
|
|
360
|
+
("service show [id]", "Show the durable published app service"),
|
|
361
|
+
("service clear [id]", "Clear the durable published app service"),
|
|
362
|
+
),
|
|
363
|
+
),
|
|
364
|
+
(
|
|
365
|
+
"Proxy",
|
|
366
|
+
(
|
|
367
|
+
("proxy start <id> <ports…>", "Forward local → remote ports (e.g. 5432 or 15432:5432)"),
|
|
368
|
+
("proxy ls", "List running proxies"),
|
|
369
|
+
("proxy stop [ports…] [--all]", "Stop proxies (by local port or all)"),
|
|
370
|
+
),
|
|
371
|
+
),
|
|
372
|
+
(
|
|
373
|
+
"GitHub",
|
|
374
|
+
(
|
|
375
|
+
("switch, checkout [branch]", "Open a GitHub branch workspace in its own computer"),
|
|
376
|
+
("integrations list", "List account-level integrations"),
|
|
377
|
+
("integrations connect github", "Install the RuntimeVM GitHub App"),
|
|
378
|
+
),
|
|
379
|
+
),
|
|
380
|
+
(
|
|
381
|
+
"Account",
|
|
382
|
+
(
|
|
383
|
+
("signup [email]", "Send an email verification code"),
|
|
384
|
+
("verify [code]", "Verify an emailed code and save a fresh API key"),
|
|
385
|
+
("login [email|api-key]", "Email login or import an API key"),
|
|
386
|
+
("whoami", "Show current account"),
|
|
387
|
+
("logout", "Revoke API key and remove local config"),
|
|
388
|
+
("api-keys list/create/revoke", "Manage Runtime API keys"),
|
|
389
|
+
("secrets list/show/set/import/rm", "Manage account-level secret sets"),
|
|
390
|
+
),
|
|
391
|
+
),
|
|
392
|
+
(
|
|
393
|
+
"Shell",
|
|
394
|
+
(
|
|
395
|
+
("help [topic]", "Show extended help — topics: computer, share, account, all"),
|
|
396
|
+
("completion <bash|zsh|fish>", "Print shell completion script"),
|
|
397
|
+
),
|
|
398
|
+
),
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
_HELP_EXAMPLES: tuple[str, ...] = (
|
|
403
|
+
"runtime # interactive menu (create if piped)",
|
|
404
|
+
"runtime ls -w # live-updating list",
|
|
405
|
+
"runtime create mybox --command 'npm start' --cwd /app --port 3000",
|
|
406
|
+
"runtime share private mybox # require owner auth",
|
|
407
|
+
"runtime proxy start mybox 5432 # forward localhost:5432 → mybox:5432",
|
|
408
|
+
"runtime completion zsh > ~/.zsh/completions/_runtime",
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _format_top_help() -> str:
|
|
413
|
+
lines: list[str] = []
|
|
414
|
+
lines.append("runtime — manage RuntimeVM computers")
|
|
415
|
+
lines.append("")
|
|
416
|
+
lines.append("usage: runtime [--base-url URL] [--version] [-h] <command> [args…]")
|
|
417
|
+
lines.append("")
|
|
418
|
+
lines.append("Global options:")
|
|
419
|
+
lines.append(" --base-url URL Override the Runtime API base URL")
|
|
420
|
+
lines.append(" --version Show the CLI version")
|
|
421
|
+
lines.append(" -h, --help Show this help")
|
|
422
|
+
lines.append("")
|
|
423
|
+
left_width = 0
|
|
424
|
+
for _, rows in _HELP_SECTIONS:
|
|
425
|
+
for name, _ in rows:
|
|
426
|
+
left_width = max(left_width, len(name))
|
|
427
|
+
left_width = min(left_width, 38)
|
|
428
|
+
for title, rows in _HELP_SECTIONS:
|
|
429
|
+
lines.append(f"{title}:")
|
|
430
|
+
for name, desc in rows:
|
|
431
|
+
padded = name if len(name) >= left_width else name + " " * (left_width - len(name))
|
|
432
|
+
lines.append(f" {padded} {desc}")
|
|
433
|
+
lines.append("")
|
|
434
|
+
lines.append("Examples:")
|
|
435
|
+
for example in _HELP_EXAMPLES:
|
|
436
|
+
lines.append(f" {example}")
|
|
437
|
+
lines.append("")
|
|
438
|
+
lines.append("Run `runtime <command> --help` for flags and examples on one command.")
|
|
439
|
+
lines.append("Run `runtime help <topic>` for long-form docs.")
|
|
440
|
+
return "\n".join(lines) + "\n"
|
|
441
|
+
|
|
442
|
+
|
|
296
443
|
def _windows_unsupported() -> bool:
|
|
297
444
|
return sys.platform == "win32"
|
|
298
445
|
|
|
@@ -314,10 +461,15 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
314
461
|
else:
|
|
315
462
|
config.base_url = config.configured_base_url or DEFAULT_BASE_URL
|
|
316
463
|
|
|
317
|
-
# Bare `runtime` →
|
|
464
|
+
# Bare `runtime` →
|
|
465
|
+
# TTY + no api key → onboarding
|
|
466
|
+
# TTY + api key → interactive menu (list + actions)
|
|
467
|
+
# non-TTY → create (preserves script-friendly default)
|
|
318
468
|
if args.command is None:
|
|
319
|
-
if _interactive()
|
|
320
|
-
|
|
469
|
+
if _interactive():
|
|
470
|
+
if not config.api_key:
|
|
471
|
+
return handle_onboarding(config)
|
|
472
|
+
return handle_list(config)
|
|
321
473
|
args.command = "create"
|
|
322
474
|
args.name = None
|
|
323
475
|
|
|
@@ -374,17 +526,19 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
374
526
|
getattr(args, "port", None),
|
|
375
527
|
)
|
|
376
528
|
if args.command in ("list", "ls"):
|
|
377
|
-
return handle_list(config)
|
|
529
|
+
return handle_list(config, watch=getattr(args, "watch", None))
|
|
378
530
|
if args.command == "info":
|
|
379
531
|
return handle_info(config, args.id)
|
|
380
532
|
if args.command == "url":
|
|
381
533
|
return handle_url(config, args.id)
|
|
534
|
+
if args.command == "share":
|
|
535
|
+
return handle_share(config, args.share_action, args.id)
|
|
382
536
|
if args.command == "start":
|
|
383
537
|
return handle_start(config, args.id)
|
|
384
538
|
if args.command == "enter":
|
|
385
539
|
return handle_enter(config, args.id)
|
|
386
540
|
if args.command in ("delete", "rm"):
|
|
387
|
-
return handle_delete(config, args.id)
|
|
541
|
+
return handle_delete(config, args.id, force=getattr(args, "force", False))
|
|
388
542
|
if args.command == "run":
|
|
389
543
|
return handle_run(
|
|
390
544
|
config, args.id, args.run_command, args.uid, args.gid, args.cwd, args.shell
|
|
@@ -413,6 +567,10 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
413
567
|
if args.proxy_command == "stop":
|
|
414
568
|
return handle_proxy_stop(config, args.local_ports, stop_all=args.all)
|
|
415
569
|
return report_error("unknown proxy command")
|
|
570
|
+
if args.command == "help":
|
|
571
|
+
return handle_help(args.topic)
|
|
572
|
+
if args.command == "completion":
|
|
573
|
+
return handle_completion(args.shell)
|
|
416
574
|
if args.command == "_proxy_agent":
|
|
417
575
|
return run_proxy_agent(
|
|
418
576
|
args.base_url,
|
|
@@ -687,6 +845,45 @@ def _prompt_confirm(message: str, *, default: bool = False) -> bool:
|
|
|
687
845
|
return bool(_UI.q().confirm(message, default=default).unsafe_ask())
|
|
688
846
|
|
|
689
847
|
|
|
848
|
+
def _copy_to_clipboard(text: str) -> bool:
|
|
849
|
+
"""Copy text to the OS clipboard. Returns True on success."""
|
|
850
|
+
text = str(text or "")
|
|
851
|
+
if not text:
|
|
852
|
+
return False
|
|
853
|
+
candidates = [
|
|
854
|
+
["pbcopy"],
|
|
855
|
+
["wl-copy"],
|
|
856
|
+
["xclip", "-selection", "clipboard"],
|
|
857
|
+
["xsel", "--clipboard", "--input"],
|
|
858
|
+
]
|
|
859
|
+
if sys.platform == "win32":
|
|
860
|
+
candidates.insert(0, ["clip"])
|
|
861
|
+
for cmd in candidates:
|
|
862
|
+
if not shutil.which(cmd[0]):
|
|
863
|
+
continue
|
|
864
|
+
try:
|
|
865
|
+
subprocess.run(cmd, input=text.encode(), check=True)
|
|
866
|
+
return True
|
|
867
|
+
except (OSError, subprocess.SubprocessError):
|
|
868
|
+
continue
|
|
869
|
+
return False
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
def _close_matches(ref: str, candidates: list[str], limit: int = 3) -> list[str]:
|
|
873
|
+
"""Return up to `limit` candidates that look similar to ref."""
|
|
874
|
+
ref = (ref or "").strip()
|
|
875
|
+
if not ref:
|
|
876
|
+
return []
|
|
877
|
+
pool = [c for c in candidates if c]
|
|
878
|
+
return difflib.get_close_matches(ref, pool, n=limit, cutoff=0.4)
|
|
879
|
+
|
|
880
|
+
|
|
881
|
+
def _prompt_typed_confirm(message: str, expected: str) -> bool:
|
|
882
|
+
"""Ask the user to retype the expected token (case-insensitive). Empty = cancel."""
|
|
883
|
+
typed = _prompt_text(f"{message} (type {expected!r} to confirm)") or ""
|
|
884
|
+
return typed.strip().lower() == expected.strip().lower()
|
|
885
|
+
|
|
886
|
+
|
|
690
887
|
def _display_width(value: Any, minimum: int, maximum: int) -> int:
|
|
691
888
|
text = str(value or "")
|
|
692
889
|
return max(minimum, min(len(text), maximum))
|
|
@@ -782,6 +979,12 @@ def _resolve_computer_ref(client: RuntimeClient, ref: str) -> str:
|
|
|
782
979
|
return resolved or ref
|
|
783
980
|
if len(matches) > 1:
|
|
784
981
|
raise RuntimeAPIError(f"multiple computers matched {ref!r}; use the exact computer id")
|
|
982
|
+
|
|
983
|
+
slugs = [str(c.get("slug") or "").strip() for c in computers if isinstance(c, dict)]
|
|
984
|
+
suggestions = _close_matches(ref, [s for s in slugs if s])
|
|
985
|
+
if suggestions:
|
|
986
|
+
hint = ", ".join(repr(s) for s in suggestions)
|
|
987
|
+
raise RuntimeAPIError(f"computer {ref!r} not found. Did you mean {hint}?")
|
|
785
988
|
raise RuntimeAPIError(f"computer {ref!r} not found")
|
|
786
989
|
|
|
787
990
|
|
|
@@ -1052,14 +1255,16 @@ def _render_computer_table(computers: list[dict[str, Any]], *, title: str | None
|
|
|
1052
1255
|
_UI.console().print(table)
|
|
1053
1256
|
|
|
1054
1257
|
|
|
1055
|
-
def _select_prompt(message: str, choices: list[Any]) -> Any:
|
|
1258
|
+
def _select_prompt(message: str, choices: list[Any], *, instruction: str | None = None) -> Any:
|
|
1056
1259
|
return _UI.q().select(
|
|
1057
1260
|
message,
|
|
1058
1261
|
choices=choices,
|
|
1059
1262
|
qmark="●",
|
|
1060
1263
|
pointer="❯",
|
|
1061
|
-
instruction=
|
|
1264
|
+
instruction=instruction or "↑↓ navigate · / search · enter select · esc cancel",
|
|
1062
1265
|
use_indicator=True,
|
|
1266
|
+
use_jk_keys=True,
|
|
1267
|
+
use_search_filter=True,
|
|
1063
1268
|
style=_UI.style(),
|
|
1064
1269
|
)
|
|
1065
1270
|
|
|
@@ -1111,6 +1316,7 @@ def _render_computer_panel(c: dict[str, Any], *, title: str = "computer") -> Non
|
|
|
1111
1316
|
rows: list[tuple[str, str, str | None]] = [
|
|
1112
1317
|
("name", _computer_name(c), "bold white"),
|
|
1113
1318
|
("state", f"{_computer_status_dot(status)} {status}", status_style),
|
|
1319
|
+
("share", str(c.get("visibility") or "public"), "bold white"),
|
|
1114
1320
|
("url", _computer_url_label(c) or "—", "white"),
|
|
1115
1321
|
("age", _computer_created_label(c) or "—", "dim"),
|
|
1116
1322
|
("id", str(c.get("id") or "—"), "dim"),
|
|
@@ -1147,6 +1353,7 @@ def _render_network_panel(payload: dict[str, Any], *, title: str = "url") -> Non
|
|
|
1147
1353
|
rows: list[tuple[str, str, str | None]] = [
|
|
1148
1354
|
("url", str(payload.get("public_url") or "—"), "cyan"),
|
|
1149
1355
|
("state", f"{_computer_status_dot(status)} {status}", _computer_status_style(status)),
|
|
1356
|
+
("share", str(payload.get("visibility") or "public"), "bold white"),
|
|
1150
1357
|
]
|
|
1151
1358
|
if published_port:
|
|
1152
1359
|
rows.append(("port", str(published_port), "bold white"))
|
|
@@ -1906,10 +2113,16 @@ def handle_create(
|
|
|
1906
2113
|
return _enter_computer(config, str(computer_id))
|
|
1907
2114
|
|
|
1908
2115
|
|
|
1909
|
-
def handle_list(config: RuntimeConfig) -> int:
|
|
2116
|
+
def handle_list(config: RuntimeConfig, *, watch: int | None = None) -> int:
|
|
1910
2117
|
if (err := _require_api_key(config)) is not None:
|
|
1911
2118
|
return err
|
|
1912
2119
|
|
|
2120
|
+
if watch is not None:
|
|
2121
|
+
if not _interactive():
|
|
2122
|
+
return report_error("--watch requires an interactive terminal")
|
|
2123
|
+
interval = max(1, int(watch))
|
|
2124
|
+
return _watch_list(config, interval)
|
|
2125
|
+
|
|
1913
2126
|
if _interactive():
|
|
1914
2127
|
return _interactive_list(config)
|
|
1915
2128
|
|
|
@@ -1917,6 +2130,59 @@ def handle_list(config: RuntimeConfig) -> int:
|
|
|
1917
2130
|
return report_success(client.list_computers())
|
|
1918
2131
|
|
|
1919
2132
|
|
|
2133
|
+
def _watch_list(config: RuntimeConfig, interval: int) -> int:
|
|
2134
|
+
from rich.live import Live
|
|
2135
|
+
|
|
2136
|
+
client = RuntimeClient(base_url=config.base_url, api_key=config.api_key)
|
|
2137
|
+
console = _UI.console()
|
|
2138
|
+
console.print(f"[dim]watching every {interval}s · press Ctrl+C to exit[/dim]")
|
|
2139
|
+
|
|
2140
|
+
def render_table() -> Any:
|
|
2141
|
+
mods = _UI.rich()
|
|
2142
|
+
Table, box = mods["Table"], mods["box"]
|
|
2143
|
+
table = Table(
|
|
2144
|
+
title=f"computers · refresh {interval}s · {datetime.now().strftime('%H:%M:%S')}",
|
|
2145
|
+
box=box.MINIMAL_DOUBLE_HEAD,
|
|
2146
|
+
expand=True,
|
|
2147
|
+
header_style="bold #7dd3fc",
|
|
2148
|
+
title_style="bold white",
|
|
2149
|
+
row_styles=["", "dim"],
|
|
2150
|
+
)
|
|
2151
|
+
table.add_column("Name", style="bold white", no_wrap=True, ratio=2)
|
|
2152
|
+
table.add_column("State", no_wrap=True, ratio=2)
|
|
2153
|
+
table.add_column("URL", overflow="fold", ratio=4)
|
|
2154
|
+
table.add_column("Age", style="dim", justify="right", ratio=1)
|
|
2155
|
+
try:
|
|
2156
|
+
payload = client.list_computers()
|
|
2157
|
+
except RuntimeAPIError as exc:
|
|
2158
|
+
table.add_row(f"[red]error: {exc}[/red]", "", "", "")
|
|
2159
|
+
return table
|
|
2160
|
+
computers = payload.get("computers") or []
|
|
2161
|
+
if not isinstance(computers, list):
|
|
2162
|
+
computers = []
|
|
2163
|
+
for computer in sorted(computers, key=_computer_sort_key):
|
|
2164
|
+
status = _computer_status_label(computer)
|
|
2165
|
+
style = _computer_status_style(status)
|
|
2166
|
+
table.add_row(
|
|
2167
|
+
_computer_name(computer),
|
|
2168
|
+
f"[{style}]{_computer_status_dot(status)} {status}[/{style}]",
|
|
2169
|
+
_computer_url_label(computer),
|
|
2170
|
+
_computer_created_label(computer),
|
|
2171
|
+
)
|
|
2172
|
+
if not computers:
|
|
2173
|
+
table.add_row("[dim]no computers yet[/dim]", "", "", "")
|
|
2174
|
+
return table
|
|
2175
|
+
|
|
2176
|
+
try:
|
|
2177
|
+
with Live(render_table(), console=console, refresh_per_second=4, screen=False) as live:
|
|
2178
|
+
while True:
|
|
2179
|
+
time.sleep(interval)
|
|
2180
|
+
live.update(render_table())
|
|
2181
|
+
except KeyboardInterrupt:
|
|
2182
|
+
console.print("[dim]watch stopped[/dim]")
|
|
2183
|
+
return 0
|
|
2184
|
+
|
|
2185
|
+
|
|
1920
2186
|
def handle_info(config: RuntimeConfig, computer_id: str | None) -> int:
|
|
1921
2187
|
if (err := _require_api_key(config)) is not None:
|
|
1922
2188
|
return err
|
|
@@ -1957,6 +2223,36 @@ def handle_url(config: RuntimeConfig, computer_id: str | None) -> int:
|
|
|
1957
2223
|
return report_success(result, lambda p: _render_network_panel(p, title="url"))
|
|
1958
2224
|
|
|
1959
2225
|
|
|
2226
|
+
def handle_share(config: RuntimeConfig, visibility: str | None, computer_id: str | None) -> int:
|
|
2227
|
+
if (err := _require_api_key(config)) is not None:
|
|
2228
|
+
return err
|
|
2229
|
+
if visibility not in ("public", "private"):
|
|
2230
|
+
return report_error("share target must be public or private")
|
|
2231
|
+
|
|
2232
|
+
if computer_id is None:
|
|
2233
|
+
if not _interactive():
|
|
2234
|
+
return report_error("computer id is required")
|
|
2235
|
+
picked = _pick_computer(config, "Pick a computer")
|
|
2236
|
+
if picked is None:
|
|
2237
|
+
return 0
|
|
2238
|
+
computer_id = picked.get("id") or picked.get("slug")
|
|
2239
|
+
if not computer_id:
|
|
2240
|
+
return report_error("computer is missing an id")
|
|
2241
|
+
|
|
2242
|
+
client = RuntimeClient(base_url=config.base_url, api_key=config.api_key)
|
|
2243
|
+
computer_id = _resolve_computer_ref(client, computer_id)
|
|
2244
|
+
result = _with_spinner(
|
|
2245
|
+
f"setting {computer_id} to {visibility}…",
|
|
2246
|
+
lambda: client.set_visibility(computer_id, visibility),
|
|
2247
|
+
)
|
|
2248
|
+
|
|
2249
|
+
def render(payload: dict[str, Any]) -> None:
|
|
2250
|
+
_render_computer_panel(payload, title="share")
|
|
2251
|
+
_UI.console().print(f"[green]✓[/green] [bold]{computer_id}[/bold] is now [bold]{visibility}[/bold]")
|
|
2252
|
+
|
|
2253
|
+
return report_success(result, render)
|
|
2254
|
+
|
|
2255
|
+
|
|
1960
2256
|
def handle_startup_show(config: RuntimeConfig, computer_id: str | None) -> int:
|
|
1961
2257
|
if (err := _require_api_key(config)) is not None:
|
|
1962
2258
|
return err
|
|
@@ -2116,11 +2412,10 @@ def handle_enter(config: RuntimeConfig, computer_id: str | None) -> int:
|
|
|
2116
2412
|
return _enter_computer(config, str(computer_id))
|
|
2117
2413
|
|
|
2118
2414
|
|
|
2119
|
-
def handle_delete(config: RuntimeConfig, computer_id: str | None) -> int:
|
|
2415
|
+
def handle_delete(config: RuntimeConfig, computer_id: str | None, *, force: bool = False) -> int:
|
|
2120
2416
|
if (err := _require_api_key(config)) is not None:
|
|
2121
2417
|
return err
|
|
2122
2418
|
|
|
2123
|
-
interactive_pick = False
|
|
2124
2419
|
if computer_id is None:
|
|
2125
2420
|
if not _interactive():
|
|
2126
2421
|
return report_error("computer id is required")
|
|
@@ -2130,11 +2425,13 @@ def handle_delete(config: RuntimeConfig, computer_id: str | None) -> int:
|
|
|
2130
2425
|
computer_id = picked.get("id") or picked.get("slug")
|
|
2131
2426
|
if not computer_id:
|
|
2132
2427
|
return report_error("computer is missing an id")
|
|
2133
|
-
interactive_pick = True
|
|
2134
2428
|
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2429
|
+
# In TTY mode, require the user to retype the slug as confirmation
|
|
2430
|
+
# unless --force was passed. Non-TTY callers stay script-friendly.
|
|
2431
|
+
if _interactive() and not force:
|
|
2432
|
+
if not _prompt_typed_confirm(
|
|
2433
|
+
f"Delete {computer_id}? This permanently destroys filesystem, service, and public URL.",
|
|
2434
|
+
str(computer_id),
|
|
2138
2435
|
):
|
|
2139
2436
|
_UI.console().print("[dim]cancelled[/dim]")
|
|
2140
2437
|
return 0
|
|
@@ -2684,18 +2981,19 @@ def _interactive_list(config: RuntimeConfig) -> int:
|
|
|
2684
2981
|
computers = []
|
|
2685
2982
|
|
|
2686
2983
|
if not computers:
|
|
2687
|
-
_UI.console().print("[dim]no computers yet.
|
|
2688
|
-
return
|
|
2984
|
+
_UI.console().print("[dim]no computers yet. creating your first one…[/dim]")
|
|
2985
|
+
return handle_create(config, None, None, None, None)
|
|
2689
2986
|
|
|
2690
2987
|
computers = sorted(computers, key=_computer_sort_key)
|
|
2691
2988
|
_render_computer_summary(computers)
|
|
2692
2989
|
widths = _computer_table_layout(computers)
|
|
2693
2990
|
_render_computer_table(computers, title="computers")
|
|
2694
|
-
choices = [
|
|
2991
|
+
choices: list[Any] = [
|
|
2695
2992
|
questionary.Choice(title=_computer_choice_title(c, widths), value=c)
|
|
2696
2993
|
for c in computers
|
|
2697
2994
|
]
|
|
2698
2995
|
choices.append(questionary.Separator())
|
|
2996
|
+
choices.append(questionary.Choice(title="+ new computer", value="__new__"))
|
|
2699
2997
|
choices.append(questionary.Choice(title="↻ refresh", value="__refresh__"))
|
|
2700
2998
|
choices.append(questionary.Choice(title="✕ quit", value="__quit__"))
|
|
2701
2999
|
|
|
@@ -2705,6 +3003,9 @@ def _interactive_list(config: RuntimeConfig) -> int:
|
|
|
2705
3003
|
return 0
|
|
2706
3004
|
if picked == "__refresh__":
|
|
2707
3005
|
continue
|
|
3006
|
+
if picked == "__new__":
|
|
3007
|
+
handle_create(config, None, None, None, None)
|
|
3008
|
+
continue
|
|
2708
3009
|
if not isinstance(picked, dict):
|
|
2709
3010
|
continue
|
|
2710
3011
|
|
|
@@ -2725,7 +3026,10 @@ def _vm_detail_menu(client: RuntimeClient, vm: dict[str, Any]) -> str:
|
|
|
2725
3026
|
|
|
2726
3027
|
power_state = _computer_power_state(vm)
|
|
2727
3028
|
internal_status = _computer_internal_status(vm)
|
|
2728
|
-
|
|
3029
|
+
visibility = str(vm.get("visibility") or "public")
|
|
3030
|
+
flip_visibility = "private" if visibility == "public" else "public"
|
|
3031
|
+
|
|
3032
|
+
choices: list[Any] = []
|
|
2729
3033
|
if internal_status in ("running", "cold"):
|
|
2730
3034
|
choices.append(questionary.Choice(title="⌨ enter console", value="enter"))
|
|
2731
3035
|
if internal_status == "running":
|
|
@@ -2735,10 +3039,13 @@ def _vm_detail_menu(client: RuntimeClient, vm: dict[str, Any]) -> str:
|
|
|
2735
3039
|
choices.append(questionary.Choice(title="☀ start / wake", value="start"))
|
|
2736
3040
|
choices.extend(
|
|
2737
3041
|
[
|
|
3042
|
+
questionary.Choice(title="⧉ copy public url", value="copy-url"),
|
|
3043
|
+
questionary.Choice(title="↗ open in browser", value="open-url"),
|
|
3044
|
+
questionary.Choice(title=f"⚑ make {flip_visibility}", value=f"share:{flip_visibility}"),
|
|
3045
|
+
questionary.Choice(title="⚙ startup config…", value="startup"),
|
|
2738
3046
|
questionary.Choice(title="ℹ refresh info", value="info"),
|
|
2739
|
-
questionary.Choice(title="⧉ copy public url", value="url"),
|
|
2740
|
-
questionary.Choice(title="✕ delete", value="delete"),
|
|
2741
3047
|
questionary.Separator(),
|
|
3048
|
+
questionary.Choice(title="✕ delete", value="delete"),
|
|
2742
3049
|
questionary.Choice(title="← back", value="__back__"),
|
|
2743
3050
|
questionary.Choice(title="✕ quit", value="__quit__"),
|
|
2744
3051
|
]
|
|
@@ -2798,17 +3105,47 @@ def _vm_detail_menu(client: RuntimeClient, vm: dict[str, Any]) -> str:
|
|
|
2798
3105
|
vm = _with_spinner("fetching…", lambda: client.get_computer(vm_id))
|
|
2799
3106
|
continue
|
|
2800
3107
|
|
|
2801
|
-
if action == "url":
|
|
2802
|
-
url = vm.get("public_url") or ""
|
|
3108
|
+
if action == "copy-url":
|
|
3109
|
+
url = str(vm.get("public_url") or "").strip()
|
|
2803
3110
|
if not url:
|
|
2804
3111
|
_UI.console().print("[yellow]no public url on this computer[/yellow]")
|
|
2805
3112
|
continue
|
|
2806
|
-
|
|
3113
|
+
if _copy_to_clipboard(url):
|
|
3114
|
+
_UI.console().print(f"[green]✓[/green] copied [cyan]{url}[/cyan]")
|
|
3115
|
+
else:
|
|
3116
|
+
_UI.console().print(f"[yellow]no clipboard tool found[/yellow] [cyan]{url}[/cyan]")
|
|
3117
|
+
continue
|
|
3118
|
+
|
|
3119
|
+
if action == "open-url":
|
|
3120
|
+
url = str(vm.get("public_url") or "").strip()
|
|
3121
|
+
if not url:
|
|
3122
|
+
_UI.console().print("[yellow]no public url on this computer[/yellow]")
|
|
3123
|
+
continue
|
|
3124
|
+
with contextlib.suppress(Exception):
|
|
3125
|
+
webbrowser.open(url)
|
|
3126
|
+
_UI.console().print(f"[green]→[/green] opened [cyan]{url}[/cyan]")
|
|
3127
|
+
continue
|
|
3128
|
+
|
|
3129
|
+
if action.startswith("share:"):
|
|
3130
|
+
target = action.split(":", 1)[1]
|
|
3131
|
+
vm = _with_spinner(
|
|
3132
|
+
f"setting {slug} to {target}…",
|
|
3133
|
+
lambda: client.set_visibility(str(vm_id), target),
|
|
3134
|
+
)
|
|
3135
|
+
_UI.console().print(
|
|
3136
|
+
f"[green]✓[/green] [bold]{slug}[/bold] is now [bold]{target}[/bold]"
|
|
3137
|
+
)
|
|
3138
|
+
continue
|
|
3139
|
+
|
|
3140
|
+
if action == "startup":
|
|
3141
|
+
_vm_startup_submenu(client, vm_id, slug)
|
|
3142
|
+
vm = _with_spinner("fetching…", lambda: client.get_computer(vm_id))
|
|
2807
3143
|
continue
|
|
2808
3144
|
|
|
2809
3145
|
if action == "delete":
|
|
2810
|
-
if not
|
|
2811
|
-
f"Delete {slug}? This
|
|
3146
|
+
if not _prompt_typed_confirm(
|
|
3147
|
+
f"Delete {slug}? This permanently destroys filesystem, service, and public URL.",
|
|
3148
|
+
str(slug),
|
|
2812
3149
|
):
|
|
2813
3150
|
_UI.console().print("[dim]cancelled[/dim]")
|
|
2814
3151
|
continue
|
|
@@ -2817,5 +3154,236 @@ def _vm_detail_menu(client: RuntimeClient, vm: dict[str, Any]) -> str:
|
|
|
2817
3154
|
return "__back__"
|
|
2818
3155
|
|
|
2819
3156
|
|
|
3157
|
+
def _vm_startup_submenu(client: RuntimeClient, vm_id: str, slug: str) -> None:
|
|
3158
|
+
"""Inner submenu for durable startup config."""
|
|
3159
|
+
questionary = _UI.q()
|
|
3160
|
+
while True:
|
|
3161
|
+
current = _with_spinner("fetching startup…", lambda: client.get_startup_config(vm_id))
|
|
3162
|
+
_render_startup_panel(current, title=f"startup: {slug}")
|
|
3163
|
+
configured = bool(current.get("configured"))
|
|
3164
|
+
choices: list[Any] = [
|
|
3165
|
+
questionary.Choice(title="✎ set / replace", value="set"),
|
|
3166
|
+
]
|
|
3167
|
+
if configured:
|
|
3168
|
+
choices.append(questionary.Choice(title="✕ clear", value="clear"))
|
|
3169
|
+
choices.extend(
|
|
3170
|
+
[
|
|
3171
|
+
questionary.Separator(),
|
|
3172
|
+
questionary.Choice(title="← back", value="__back__"),
|
|
3173
|
+
]
|
|
3174
|
+
)
|
|
3175
|
+
action = _select_prompt("startup config", choices).unsafe_ask()
|
|
3176
|
+
if action in (None, "__back__"):
|
|
3177
|
+
return
|
|
3178
|
+
if action == "set":
|
|
3179
|
+
command = _prompt_text("Startup command", default=str(current.get("command") or ""))
|
|
3180
|
+
if not command:
|
|
3181
|
+
_UI.console().print("[dim]cancelled[/dim]")
|
|
3182
|
+
continue
|
|
3183
|
+
port_text = _prompt_text("App port", default=str(current.get("port") or "3000"))
|
|
3184
|
+
if not port_text:
|
|
3185
|
+
_UI.console().print("[dim]cancelled[/dim]")
|
|
3186
|
+
continue
|
|
3187
|
+
try:
|
|
3188
|
+
port = int(port_text)
|
|
3189
|
+
except ValueError:
|
|
3190
|
+
_UI.err().print("[red]port must be a number[/red]")
|
|
3191
|
+
continue
|
|
3192
|
+
if port <= 0 or port > 65535:
|
|
3193
|
+
_UI.err().print("[red]port must be between 1 and 65535[/red]")
|
|
3194
|
+
continue
|
|
3195
|
+
cwd = _prompt_text("Working directory (optional)", default=str(current.get("cwd") or ""))
|
|
3196
|
+
_with_spinner(
|
|
3197
|
+
"saving startup config…",
|
|
3198
|
+
lambda: client.set_startup_config(vm_id, command=command, cwd=cwd or None, port=port),
|
|
3199
|
+
)
|
|
3200
|
+
_UI.console().print(f"[green]✓[/green] startup saved for [bold]{slug}[/bold]")
|
|
3201
|
+
continue
|
|
3202
|
+
if action == "clear":
|
|
3203
|
+
if not _prompt_confirm(f"Clear startup config for {slug}?", default=False):
|
|
3204
|
+
_UI.console().print("[dim]cancelled[/dim]")
|
|
3205
|
+
continue
|
|
3206
|
+
_with_spinner(
|
|
3207
|
+
"clearing startup config…",
|
|
3208
|
+
lambda: client.clear_startup_config(vm_id),
|
|
3209
|
+
)
|
|
3210
|
+
_UI.console().print(f"[green]✓[/green] cleared startup config for [bold]{slug}[/bold]")
|
|
3211
|
+
continue
|
|
3212
|
+
|
|
3213
|
+
|
|
3214
|
+
# --------------------------------------------------------------------------- #
|
|
3215
|
+
# Help browser + shell completion #
|
|
3216
|
+
# --------------------------------------------------------------------------- #
|
|
3217
|
+
|
|
3218
|
+
|
|
3219
|
+
_HELP_TOPICS: dict[str, tuple[str, ...]] = {
|
|
3220
|
+
"computer": ("Computers",),
|
|
3221
|
+
"computers": ("Computers",),
|
|
3222
|
+
"share": ("Sharing",),
|
|
3223
|
+
"sharing": ("Sharing",),
|
|
3224
|
+
"startup": ("Startup & service",),
|
|
3225
|
+
"service": ("Startup & service",),
|
|
3226
|
+
"proxy": ("Proxy",),
|
|
3227
|
+
"github": ("GitHub",),
|
|
3228
|
+
"account": ("Account",),
|
|
3229
|
+
"shell": ("Shell",),
|
|
3230
|
+
"all": tuple(title for title, _ in _HELP_SECTIONS),
|
|
3231
|
+
}
|
|
3232
|
+
|
|
3233
|
+
|
|
3234
|
+
def handle_help(topic: str | None) -> int:
|
|
3235
|
+
if topic is None:
|
|
3236
|
+
sys.stdout.write(_format_top_help())
|
|
3237
|
+
return 0
|
|
3238
|
+
key = topic.strip().lower()
|
|
3239
|
+
if key not in _HELP_TOPICS:
|
|
3240
|
+
matches = _close_matches(key, list(_HELP_TOPICS))
|
|
3241
|
+
if matches:
|
|
3242
|
+
return report_error(f"unknown help topic {topic!r}. Did you mean {', '.join(repr(m) for m in matches)}?")
|
|
3243
|
+
return report_error(f"unknown help topic {topic!r}. Try: {', '.join(sorted(_HELP_TOPICS))}")
|
|
3244
|
+
|
|
3245
|
+
wanted = set(_HELP_TOPICS[key])
|
|
3246
|
+
lines: list[str] = []
|
|
3247
|
+
for title, rows in _HELP_SECTIONS:
|
|
3248
|
+
if title not in wanted:
|
|
3249
|
+
continue
|
|
3250
|
+
lines.append(f"{title}:")
|
|
3251
|
+
left = max((len(name) for name, _ in rows), default=0)
|
|
3252
|
+
for name, desc in rows:
|
|
3253
|
+
padded = name + " " * max(0, left - len(name))
|
|
3254
|
+
lines.append(f" {padded} {desc}")
|
|
3255
|
+
lines.append("")
|
|
3256
|
+
sys.stdout.write("\n".join(lines) + "\n")
|
|
3257
|
+
return 0
|
|
3258
|
+
|
|
3259
|
+
|
|
3260
|
+
_COMPLETION_BASH = r"""# bash completion for runtime
|
|
3261
|
+
_runtime_complete() {
|
|
3262
|
+
local cur sub
|
|
3263
|
+
COMPREPLY=()
|
|
3264
|
+
cur="${COMP_WORDS[COMP_CWORD]}"
|
|
3265
|
+
if [[ ${COMP_CWORD} -eq 1 ]]; then
|
|
3266
|
+
COMPREPLY=( $(compgen -W "create list ls info url start enter run publish delete rm share startup service proxy switch checkout integrations secrets api-keys signup verify login whoami logout completion help" -- "$cur") )
|
|
3267
|
+
return
|
|
3268
|
+
fi
|
|
3269
|
+
sub="${COMP_WORDS[1]}"
|
|
3270
|
+
case "$sub" in
|
|
3271
|
+
info|url|start|enter|run|publish|delete|rm|share|startup|service)
|
|
3272
|
+
local ids
|
|
3273
|
+
ids=$(runtime ls 2>/dev/null | python3 -c 'import sys,json;print("\n".join(c["slug"] for c in json.load(sys.stdin).get("computers",[]) if c.get("slug")))' 2>/dev/null)
|
|
3274
|
+
COMPREPLY=( $(compgen -W "$ids" -- "$cur") )
|
|
3275
|
+
;;
|
|
3276
|
+
completion)
|
|
3277
|
+
COMPREPLY=( $(compgen -W "bash zsh fish" -- "$cur") )
|
|
3278
|
+
;;
|
|
3279
|
+
share)
|
|
3280
|
+
if [[ ${COMP_CWORD} -eq 2 ]]; then
|
|
3281
|
+
COMPREPLY=( $(compgen -W "public private" -- "$cur") )
|
|
3282
|
+
fi
|
|
3283
|
+
;;
|
|
3284
|
+
esac
|
|
3285
|
+
}
|
|
3286
|
+
complete -F _runtime_complete runtime
|
|
3287
|
+
"""
|
|
3288
|
+
|
|
3289
|
+
|
|
3290
|
+
_COMPLETION_ZSH = r"""#compdef runtime
|
|
3291
|
+
_runtime() {
|
|
3292
|
+
local -a _runtime_commands
|
|
3293
|
+
_runtime_commands=(
|
|
3294
|
+
'create:Create a new computer'
|
|
3295
|
+
'list:List computers'
|
|
3296
|
+
'ls:List computers'
|
|
3297
|
+
'info:Show details'
|
|
3298
|
+
'url:Show public URL'
|
|
3299
|
+
'start:Wake a cold computer'
|
|
3300
|
+
'enter:Enter console'
|
|
3301
|
+
'run:Run a command'
|
|
3302
|
+
'publish:Publish a port'
|
|
3303
|
+
'delete:Delete a computer'
|
|
3304
|
+
'rm:Delete a computer'
|
|
3305
|
+
'share:Set visibility'
|
|
3306
|
+
'startup:Startup config'
|
|
3307
|
+
'service:Service config'
|
|
3308
|
+
'proxy:Port proxies'
|
|
3309
|
+
'switch:Open a GitHub branch workspace'
|
|
3310
|
+
'checkout:Open a GitHub branch workspace'
|
|
3311
|
+
'integrations:Integrations'
|
|
3312
|
+
'secrets:Secret sets'
|
|
3313
|
+
'api-keys:API keys'
|
|
3314
|
+
'signup:Send verification code'
|
|
3315
|
+
'verify:Verify code'
|
|
3316
|
+
'login:Login'
|
|
3317
|
+
'whoami:Show account'
|
|
3318
|
+
'logout:Log out'
|
|
3319
|
+
'completion:Shell completion'
|
|
3320
|
+
'help:Extended help'
|
|
3321
|
+
)
|
|
3322
|
+
local state
|
|
3323
|
+
_arguments -C '1: :->cmd' '*:: :->args'
|
|
3324
|
+
case $state in
|
|
3325
|
+
cmd)
|
|
3326
|
+
_describe -t commands 'runtime command' _runtime_commands
|
|
3327
|
+
;;
|
|
3328
|
+
args)
|
|
3329
|
+
case ${words[1]} in
|
|
3330
|
+
info|url|start|enter|run|publish|delete|rm|startup|service)
|
|
3331
|
+
local -a ids
|
|
3332
|
+
ids=(${(f)"$(runtime ls 2>/dev/null | python3 -c 'import sys,json;print(\"\\n\".join(c[\"slug\"] for c in json.load(sys.stdin).get(\"computers\",[]) if c.get(\"slug\")))' 2>/dev/null)"})
|
|
3333
|
+
_describe -t slugs 'computer' ids
|
|
3334
|
+
;;
|
|
3335
|
+
share)
|
|
3336
|
+
_values 'visibility' public private
|
|
3337
|
+
;;
|
|
3338
|
+
completion)
|
|
3339
|
+
_values 'shell' bash zsh fish
|
|
3340
|
+
;;
|
|
3341
|
+
esac
|
|
3342
|
+
;;
|
|
3343
|
+
esac
|
|
3344
|
+
}
|
|
3345
|
+
_runtime "$@"
|
|
3346
|
+
"""
|
|
3347
|
+
|
|
3348
|
+
|
|
3349
|
+
_COMPLETION_FISH = r"""# fish completion for runtime
|
|
3350
|
+
function __runtime_slugs
|
|
3351
|
+
runtime ls 2>/dev/null | python3 -c 'import sys,json;print("\n".join(c["slug"] for c in json.load(sys.stdin).get("computers",[]) if c.get("slug")))' 2>/dev/null
|
|
3352
|
+
end
|
|
3353
|
+
|
|
3354
|
+
complete -c runtime -f
|
|
3355
|
+
complete -c runtime -n '__fish_use_subcommand' -a create -d 'Create'
|
|
3356
|
+
complete -c runtime -n '__fish_use_subcommand' -a list -d 'List'
|
|
3357
|
+
complete -c runtime -n '__fish_use_subcommand' -a ls -d 'List'
|
|
3358
|
+
complete -c runtime -n '__fish_use_subcommand' -a info -d 'Show details'
|
|
3359
|
+
complete -c runtime -n '__fish_use_subcommand' -a url -d 'Show URL'
|
|
3360
|
+
complete -c runtime -n '__fish_use_subcommand' -a start -d 'Wake'
|
|
3361
|
+
complete -c runtime -n '__fish_use_subcommand' -a enter -d 'Console'
|
|
3362
|
+
complete -c runtime -n '__fish_use_subcommand' -a run -d 'Run command'
|
|
3363
|
+
complete -c runtime -n '__fish_use_subcommand' -a publish -d 'Publish port'
|
|
3364
|
+
complete -c runtime -n '__fish_use_subcommand' -a delete -d 'Delete'
|
|
3365
|
+
complete -c runtime -n '__fish_use_subcommand' -a rm -d 'Delete'
|
|
3366
|
+
complete -c runtime -n '__fish_use_subcommand' -a share -d 'Set visibility'
|
|
3367
|
+
complete -c runtime -n '__fish_use_subcommand' -a startup -d 'Startup config'
|
|
3368
|
+
complete -c runtime -n '__fish_use_subcommand' -a service -d 'Service config'
|
|
3369
|
+
complete -c runtime -n '__fish_use_subcommand' -a proxy -d 'Port proxies'
|
|
3370
|
+
complete -c runtime -n '__fish_use_subcommand' -a help -d 'Extended help'
|
|
3371
|
+
complete -c runtime -n '__fish_use_subcommand' -a completion -d 'Completion'
|
|
3372
|
+
|
|
3373
|
+
complete -c runtime -n '__fish_seen_subcommand_from info url start enter run publish delete rm startup service' -a '(__runtime_slugs)'
|
|
3374
|
+
complete -c runtime -n '__fish_seen_subcommand_from completion' -a 'bash zsh fish'
|
|
3375
|
+
complete -c runtime -n '__fish_seen_subcommand_from share' -a 'public private'
|
|
3376
|
+
"""
|
|
3377
|
+
|
|
3378
|
+
|
|
3379
|
+
def handle_completion(shell: str) -> int:
|
|
3380
|
+
scripts = {"bash": _COMPLETION_BASH, "zsh": _COMPLETION_ZSH, "fish": _COMPLETION_FISH}
|
|
3381
|
+
script = scripts.get(shell)
|
|
3382
|
+
if script is None:
|
|
3383
|
+
return report_error(f"unsupported shell: {shell}")
|
|
3384
|
+
sys.stdout.write(script)
|
|
3385
|
+
return 0
|
|
3386
|
+
|
|
3387
|
+
|
|
2820
3388
|
if __name__ == "__main__":
|
|
2821
3389
|
raise SystemExit(main())
|
|
@@ -155,6 +155,14 @@ class RuntimeClient:
|
|
|
155
155
|
def get_computer_network(self, computer_id: str) -> dict[str, Any]:
|
|
156
156
|
return self._request("GET", f"/api/computers/{computer_id}/network", auth_required=True)
|
|
157
157
|
|
|
158
|
+
def set_visibility(self, computer_id: str, visibility: str) -> dict[str, Any]:
|
|
159
|
+
return self._request(
|
|
160
|
+
"PUT",
|
|
161
|
+
f"/api/computers/{computer_id}/visibility",
|
|
162
|
+
json={"visibility": visibility},
|
|
163
|
+
auth_required=True,
|
|
164
|
+
)
|
|
165
|
+
|
|
158
166
|
def get_startup_config(self, computer_id: str) -> dict[str, Any]:
|
|
159
167
|
return self._request("GET", f"/api/computers/{computer_id}/startup", auth_required=True)
|
|
160
168
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: runtime-sdk
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.22
|
|
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
|
|
@@ -78,6 +78,8 @@ runtime create myapp --command "python3 app.py" --cwd /home/ubuntu --port 3000
|
|
|
78
78
|
runtime enter <name-or-id> # accepts slug/name like test, or the computer id
|
|
79
79
|
runtime list
|
|
80
80
|
runtime info <id>
|
|
81
|
+
runtime share public <id>
|
|
82
|
+
runtime share private <id>
|
|
81
83
|
runtime start <id>
|
|
82
84
|
runtime run <id> "echo hello" # one-shot foreground command only
|
|
83
85
|
runtime run <id> "apt install -y nodejs" --uid 0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|