runtime-sdk 0.4.10__tar.gz → 0.4.12__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.10 → runtime_sdk-0.4.12}/PKG-INFO +33 -12
- {runtime_sdk-0.4.10 → runtime_sdk-0.4.12}/README.md +32 -11
- {runtime_sdk-0.4.10 → runtime_sdk-0.4.12}/pyproject.toml +1 -1
- {runtime_sdk-0.4.10 → runtime_sdk-0.4.12}/scripts/runtime_sdk/cli.py +342 -11
- {runtime_sdk-0.4.10 → runtime_sdk-0.4.12}/scripts/runtime_sdk/client.py +34 -0
- {runtime_sdk-0.4.10 → runtime_sdk-0.4.12}/scripts/runtime_sdk.egg-info/PKG-INFO +33 -12
- {runtime_sdk-0.4.10 → runtime_sdk-0.4.12}/scripts/runtime_sdk/__init__.py +0 -0
- {runtime_sdk-0.4.10 → runtime_sdk-0.4.12}/scripts/runtime_sdk/config.py +0 -0
- {runtime_sdk-0.4.10 → runtime_sdk-0.4.12}/scripts/runtime_sdk.egg-info/SOURCES.txt +0 -0
- {runtime_sdk-0.4.10 → runtime_sdk-0.4.12}/scripts/runtime_sdk.egg-info/dependency_links.txt +0 -0
- {runtime_sdk-0.4.10 → runtime_sdk-0.4.12}/scripts/runtime_sdk.egg-info/entry_points.txt +0 -0
- {runtime_sdk-0.4.10 → runtime_sdk-0.4.12}/scripts/runtime_sdk.egg-info/requires.txt +0 -0
- {runtime_sdk-0.4.10 → runtime_sdk-0.4.12}/scripts/runtime_sdk.egg-info/top_level.txt +0 -0
- {runtime_sdk-0.4.10 → runtime_sdk-0.4.12}/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.12
|
|
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
|
|
@@ -52,18 +52,32 @@ runtime verify 123456
|
|
|
52
52
|
runtime whoami
|
|
53
53
|
runtime logout
|
|
54
54
|
runtime login --api-key rt_live_...
|
|
55
|
+
runtime api-keys list
|
|
56
|
+
runtime api-keys create "my laptop"
|
|
57
|
+
runtime api-keys revoke 123
|
|
55
58
|
|
|
56
59
|
# Computers
|
|
57
|
-
runtime create #
|
|
60
|
+
runtime create # creates a computer with the starter app already published
|
|
58
61
|
runtime create myapp --command "python3 app.py" --cwd /home/ubuntu --port 3000
|
|
59
62
|
runtime enter <name-or-id> # accepts slug/name like test, or the computer id
|
|
60
63
|
runtime list
|
|
61
64
|
runtime info <id>
|
|
62
65
|
runtime start <id>
|
|
63
|
-
runtime run <id> "echo hello"
|
|
66
|
+
runtime run <id> "echo hello" # one-shot foreground command only
|
|
64
67
|
runtime run <id> "apt install -y nodejs" --uid 0
|
|
65
|
-
runtime
|
|
68
|
+
runtime startup show <id>
|
|
69
|
+
runtime startup set <id> --command "python3 app.py" --cwd /home/ubuntu --port 3000
|
|
70
|
+
runtime startup clear <id> # low-level durable service config
|
|
71
|
+
runtime service show <id> # user-facing alias for the durable published app
|
|
72
|
+
runtime service clear <id>
|
|
73
|
+
runtime publish <id> 3000 # promote the running app on port 3000 to the public durable app
|
|
66
74
|
runtime delete <id>
|
|
75
|
+
|
|
76
|
+
# Inside a running computer, the helper installed by Runtime can manage the
|
|
77
|
+
# durable app without leaving the sandbox:
|
|
78
|
+
# runtime-env publish 3000
|
|
79
|
+
# runtime-env service show
|
|
80
|
+
# runtime-env service clear
|
|
67
81
|
```
|
|
68
82
|
|
|
69
83
|
## Python
|
|
@@ -73,12 +87,12 @@ from runtime_sdk import RuntimeClient
|
|
|
73
87
|
|
|
74
88
|
client = RuntimeClient(base_url="https://api.runruntime.dev", api_key="rt_live_...")
|
|
75
89
|
|
|
76
|
-
# Create a computer
|
|
90
|
+
# Create a computer. New computers start with the starter app already published.
|
|
77
91
|
computer = client.create_computer()
|
|
78
92
|
print(computer["public_url"]) # https://goldbird.runruntime.dev
|
|
79
93
|
|
|
80
|
-
# Or create one with
|
|
81
|
-
# That
|
|
94
|
+
# Or create one with an explicit durable app command.
|
|
95
|
+
# That saved service is replayed after cold restore / start.
|
|
82
96
|
app = client.create_computer(
|
|
83
97
|
slug="myapp",
|
|
84
98
|
command="python3 app.py",
|
|
@@ -93,7 +107,8 @@ print(result["stdout"])
|
|
|
93
107
|
# Wake a cold computer explicitly
|
|
94
108
|
client.start_computer(app["id"])
|
|
95
109
|
|
|
96
|
-
#
|
|
110
|
+
# Promote the running app on a local port to the durable public app.
|
|
111
|
+
# Runtime inspects the listening process and saves its command + cwd when possible.
|
|
97
112
|
client.publish_port(computer["id"], 3000)
|
|
98
113
|
|
|
99
114
|
# List, info, delete
|
|
@@ -120,10 +135,16 @@ make smoke
|
|
|
120
135
|
Use `make deploy` instead of `make sync` when migrations, env
|
|
121
136
|
files, Caddy, or systemd units changed.
|
|
122
137
|
|
|
123
|
-
Cold restore and
|
|
124
|
-
computers
|
|
125
|
-
|
|
126
|
-
|
|
138
|
+
Cold restore and explicit `runtime start` replay the saved published app command.
|
|
139
|
+
New computers seed that durable app from the starter workspace. Later,
|
|
140
|
+
`runtime publish <id> <port>` can promote a running listener into the saved
|
|
141
|
+
public app definition by inspecting the live process. Inside a running computer,
|
|
142
|
+
`runtime-env publish <port>` does the same thing using a computer-scoped token
|
|
143
|
+
installed by Runtime. `runtime service show|clear` and `runtime-env service
|
|
144
|
+
show|clear` expose that same durable app state directly. The low-level
|
|
145
|
+
`runtime startup ...` commands still map to the same durable state. A one-off
|
|
146
|
+
`runtime run` stays one-shot: the filesystem is restored after going cold, but
|
|
147
|
+
that ad-hoc process is not.
|
|
127
148
|
|
|
128
149
|
Run the SDK unit tests through the backend project environment:
|
|
129
150
|
|
|
@@ -33,18 +33,32 @@ runtime verify 123456
|
|
|
33
33
|
runtime whoami
|
|
34
34
|
runtime logout
|
|
35
35
|
runtime login --api-key rt_live_...
|
|
36
|
+
runtime api-keys list
|
|
37
|
+
runtime api-keys create "my laptop"
|
|
38
|
+
runtime api-keys revoke 123
|
|
36
39
|
|
|
37
40
|
# Computers
|
|
38
|
-
runtime create #
|
|
41
|
+
runtime create # creates a computer with the starter app already published
|
|
39
42
|
runtime create myapp --command "python3 app.py" --cwd /home/ubuntu --port 3000
|
|
40
43
|
runtime enter <name-or-id> # accepts slug/name like test, or the computer id
|
|
41
44
|
runtime list
|
|
42
45
|
runtime info <id>
|
|
43
46
|
runtime start <id>
|
|
44
|
-
runtime run <id> "echo hello"
|
|
47
|
+
runtime run <id> "echo hello" # one-shot foreground command only
|
|
45
48
|
runtime run <id> "apt install -y nodejs" --uid 0
|
|
46
|
-
runtime
|
|
49
|
+
runtime startup show <id>
|
|
50
|
+
runtime startup set <id> --command "python3 app.py" --cwd /home/ubuntu --port 3000
|
|
51
|
+
runtime startup clear <id> # low-level durable service config
|
|
52
|
+
runtime service show <id> # user-facing alias for the durable published app
|
|
53
|
+
runtime service clear <id>
|
|
54
|
+
runtime publish <id> 3000 # promote the running app on port 3000 to the public durable app
|
|
47
55
|
runtime delete <id>
|
|
56
|
+
|
|
57
|
+
# Inside a running computer, the helper installed by Runtime can manage the
|
|
58
|
+
# durable app without leaving the sandbox:
|
|
59
|
+
# runtime-env publish 3000
|
|
60
|
+
# runtime-env service show
|
|
61
|
+
# runtime-env service clear
|
|
48
62
|
```
|
|
49
63
|
|
|
50
64
|
## Python
|
|
@@ -54,12 +68,12 @@ from runtime_sdk import RuntimeClient
|
|
|
54
68
|
|
|
55
69
|
client = RuntimeClient(base_url="https://api.runruntime.dev", api_key="rt_live_...")
|
|
56
70
|
|
|
57
|
-
# Create a computer
|
|
71
|
+
# Create a computer. New computers start with the starter app already published.
|
|
58
72
|
computer = client.create_computer()
|
|
59
73
|
print(computer["public_url"]) # https://goldbird.runruntime.dev
|
|
60
74
|
|
|
61
|
-
# Or create one with
|
|
62
|
-
# That
|
|
75
|
+
# Or create one with an explicit durable app command.
|
|
76
|
+
# That saved service is replayed after cold restore / start.
|
|
63
77
|
app = client.create_computer(
|
|
64
78
|
slug="myapp",
|
|
65
79
|
command="python3 app.py",
|
|
@@ -74,7 +88,8 @@ print(result["stdout"])
|
|
|
74
88
|
# Wake a cold computer explicitly
|
|
75
89
|
client.start_computer(app["id"])
|
|
76
90
|
|
|
77
|
-
#
|
|
91
|
+
# Promote the running app on a local port to the durable public app.
|
|
92
|
+
# Runtime inspects the listening process and saves its command + cwd when possible.
|
|
78
93
|
client.publish_port(computer["id"], 3000)
|
|
79
94
|
|
|
80
95
|
# List, info, delete
|
|
@@ -101,10 +116,16 @@ make smoke
|
|
|
101
116
|
Use `make deploy` instead of `make sync` when migrations, env
|
|
102
117
|
files, Caddy, or systemd units changed.
|
|
103
118
|
|
|
104
|
-
Cold restore and
|
|
105
|
-
computers
|
|
106
|
-
|
|
107
|
-
|
|
119
|
+
Cold restore and explicit `runtime start` replay the saved published app command.
|
|
120
|
+
New computers seed that durable app from the starter workspace. Later,
|
|
121
|
+
`runtime publish <id> <port>` can promote a running listener into the saved
|
|
122
|
+
public app definition by inspecting the live process. Inside a running computer,
|
|
123
|
+
`runtime-env publish <port>` does the same thing using a computer-scoped token
|
|
124
|
+
installed by Runtime. `runtime service show|clear` and `runtime-env service
|
|
125
|
+
show|clear` expose that same durable app state directly. The low-level
|
|
126
|
+
`runtime startup ...` commands still map to the same durable state. A one-off
|
|
127
|
+
`runtime run` stays one-shot: the filesystem is restored after going cold, but
|
|
128
|
+
that ad-hoc process is not.
|
|
108
129
|
|
|
109
130
|
Run the SDK unit tests through the backend project environment:
|
|
110
131
|
|
|
@@ -165,16 +165,24 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
165
165
|
subparsers.add_parser("whoami")
|
|
166
166
|
subparsers.add_parser("logout")
|
|
167
167
|
|
|
168
|
-
|
|
168
|
+
api_keys_cmd = subparsers.add_parser("api-keys", help="Manage Runtime API keys")
|
|
169
|
+
api_keys_subparsers = api_keys_cmd.add_subparsers(dest="api_keys_action", required=True)
|
|
170
|
+
api_keys_subparsers.add_parser("list", aliases=["ls"], help="List API keys")
|
|
171
|
+
api_keys_create = api_keys_subparsers.add_parser("create", help="Create a new API key")
|
|
172
|
+
api_keys_create.add_argument("label", nargs="?", default=None, help="Human-friendly label for the new API key")
|
|
173
|
+
api_keys_revoke = api_keys_subparsers.add_parser("revoke", help="Revoke an API key")
|
|
174
|
+
api_keys_revoke.add_argument("id", nargs="?", default=None, help="API key id")
|
|
175
|
+
|
|
176
|
+
create_cmd = subparsers.add_parser("create", help="Create a new computer with the starter app published by default")
|
|
169
177
|
create_cmd.add_argument(
|
|
170
178
|
"name",
|
|
171
179
|
nargs="?",
|
|
172
180
|
default=None,
|
|
173
181
|
help="Subdomain name (e.g. redsox → redsox.runruntime.dev). Random if skipped.",
|
|
174
182
|
)
|
|
175
|
-
create_cmd.add_argument("--command", dest="startup_command", help="
|
|
176
|
-
create_cmd.add_argument("--cwd", help="Working directory for the startup command")
|
|
177
|
-
create_cmd.add_argument("--port", type=int, help="App port to publish
|
|
183
|
+
create_cmd.add_argument("--command", dest="startup_command", help="Durable startup command to save on create")
|
|
184
|
+
create_cmd.add_argument("--cwd", help="Working directory for the durable startup command")
|
|
185
|
+
create_cmd.add_argument("--port", type=int, help="App port to publish for durable startup")
|
|
178
186
|
subparsers.add_parser("list", aliases=["ls"], help="List computers")
|
|
179
187
|
|
|
180
188
|
enter_cmd = subparsers.add_parser("enter", help="Enter a computer's console")
|
|
@@ -192,7 +200,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
192
200
|
delete_cmd = subparsers.add_parser("delete", aliases=["rm"], help="Delete a computer")
|
|
193
201
|
delete_cmd.add_argument("id", nargs="?", default=None, help="Computer ID (hostname)")
|
|
194
202
|
|
|
195
|
-
run_cmd = subparsers.add_parser("run", help="Run a command in a computer")
|
|
203
|
+
run_cmd = subparsers.add_parser("run", help="Run a one-shot command in a computer")
|
|
196
204
|
run_cmd.add_argument("id", nargs="?", default=None, help="Computer ID (hostname)")
|
|
197
205
|
run_cmd.add_argument(
|
|
198
206
|
"run_command",
|
|
@@ -207,11 +215,30 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
207
215
|
run_cmd.add_argument("--shell", help="Shell to use")
|
|
208
216
|
|
|
209
217
|
publish_cmd = subparsers.add_parser(
|
|
210
|
-
"publish", help="
|
|
218
|
+
"publish", help="Promote the running app on a local port into the durable public app"
|
|
211
219
|
)
|
|
212
220
|
publish_cmd.add_argument("id", nargs="?", default=None, help="Computer ID (hostname)")
|
|
213
221
|
publish_cmd.add_argument("port", nargs="?", type=int, default=None, help="Local app port")
|
|
214
222
|
|
|
223
|
+
startup_cmd = subparsers.add_parser("startup", help="Manage durable startup config")
|
|
224
|
+
startup_subparsers = startup_cmd.add_subparsers(dest="startup_action", required=True)
|
|
225
|
+
startup_show = startup_subparsers.add_parser("show", help="Show durable startup config")
|
|
226
|
+
startup_show.add_argument("id", nargs="?", default=None, help="Computer ID (hostname)")
|
|
227
|
+
startup_set = startup_subparsers.add_parser("set", help="Set durable startup config")
|
|
228
|
+
startup_set.add_argument("id", nargs="?", default=None, help="Computer ID (hostname)")
|
|
229
|
+
startup_set.add_argument("--command", dest="startup_value_command", required=True, help="Durable startup command")
|
|
230
|
+
startup_set.add_argument("--cwd", help="Working directory for the durable startup command")
|
|
231
|
+
startup_set.add_argument("--port", required=True, type=int, help="App port to publish for durable startup")
|
|
232
|
+
startup_clear = startup_subparsers.add_parser("clear", help="Clear durable startup config")
|
|
233
|
+
startup_clear.add_argument("id", nargs="?", default=None, help="Computer ID (hostname)")
|
|
234
|
+
|
|
235
|
+
service_cmd = subparsers.add_parser("service", help="Manage the durable published app service")
|
|
236
|
+
service_subparsers = service_cmd.add_subparsers(dest="service_action", required=True)
|
|
237
|
+
service_show = service_subparsers.add_parser("show", help="Show the durable published app service")
|
|
238
|
+
service_show.add_argument("id", nargs="?", default=None, help="Computer ID (hostname)")
|
|
239
|
+
service_clear = service_subparsers.add_parser("clear", help="Clear the durable published app service")
|
|
240
|
+
service_clear.add_argument("id", nargs="?", default=None, help="Computer ID (hostname)")
|
|
241
|
+
|
|
215
242
|
proxy_cmd = subparsers.add_parser("proxy", help="Manage background local port proxies")
|
|
216
243
|
proxy_subparsers = proxy_cmd.add_subparsers(dest="proxy_command", required=True)
|
|
217
244
|
proxy_start = proxy_subparsers.add_parser("start", help="Start one or more background local port proxies")
|
|
@@ -261,6 +288,14 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
261
288
|
return handle_whoami(config)
|
|
262
289
|
if args.command == "logout":
|
|
263
290
|
return handle_logout(config)
|
|
291
|
+
if args.command == "api-keys":
|
|
292
|
+
if args.api_keys_action in ("list", "ls"):
|
|
293
|
+
return handle_api_keys_list(config)
|
|
294
|
+
if args.api_keys_action == "create":
|
|
295
|
+
return handle_api_keys_create(config, args.label)
|
|
296
|
+
if args.api_keys_action == "revoke":
|
|
297
|
+
return handle_api_keys_revoke(config, args.id)
|
|
298
|
+
return report_error("unknown api-keys command")
|
|
264
299
|
if args.command == "create":
|
|
265
300
|
return handle_create(
|
|
266
301
|
config,
|
|
@@ -287,6 +322,20 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
287
322
|
)
|
|
288
323
|
if args.command == "publish":
|
|
289
324
|
return handle_publish(config, args.id, args.port)
|
|
325
|
+
if args.command == "startup":
|
|
326
|
+
if args.startup_action == "show":
|
|
327
|
+
return handle_startup_show(config, args.id)
|
|
328
|
+
if args.startup_action == "set":
|
|
329
|
+
return handle_startup_set(config, args.id, args.startup_value_command, args.cwd, args.port)
|
|
330
|
+
if args.startup_action == "clear":
|
|
331
|
+
return handle_startup_clear(config, args.id)
|
|
332
|
+
return report_error("unknown startup command")
|
|
333
|
+
if args.command == "service":
|
|
334
|
+
if args.service_action == "show":
|
|
335
|
+
return handle_service_show(config, args.id)
|
|
336
|
+
if args.service_action == "clear":
|
|
337
|
+
return handle_service_clear(config, args.id)
|
|
338
|
+
return report_error("unknown service command")
|
|
290
339
|
if args.command == "proxy":
|
|
291
340
|
if args.proxy_command == "start":
|
|
292
341
|
return handle_proxy_start(config, args.id, args.ports)
|
|
@@ -510,7 +559,7 @@ def _spawn_proxy_agent(base_url: str, api_key: str, computer_id: str, display_na
|
|
|
510
559
|
finally:
|
|
511
560
|
log_file.close()
|
|
512
561
|
|
|
513
|
-
deadline = time.time() + 4.0
|
|
562
|
+
deadline = time.time() + max(4.0, DEFAULT_WARMUP_TIMEOUT + 2.0)
|
|
514
563
|
state_path = _proxy_state_path(local_port)
|
|
515
564
|
while time.time() < deadline:
|
|
516
565
|
if state_path.exists():
|
|
@@ -664,7 +713,7 @@ def _resolve_computer_ref(client: RuntimeClient, ref: str) -> str:
|
|
|
664
713
|
return resolved or ref
|
|
665
714
|
if len(matches) > 1:
|
|
666
715
|
raise RuntimeAPIError(f"multiple computers matched {ref!r}; use the exact computer id")
|
|
667
|
-
|
|
716
|
+
raise RuntimeAPIError(f"computer {ref!r} not found")
|
|
668
717
|
|
|
669
718
|
|
|
670
719
|
|
|
@@ -682,8 +731,8 @@ def _computer_url_label(c: dict[str, Any]) -> str:
|
|
|
682
731
|
|
|
683
732
|
|
|
684
733
|
|
|
685
|
-
def
|
|
686
|
-
raw = str(
|
|
734
|
+
def _relative_time_label(value: Any) -> str:
|
|
735
|
+
raw = str(value or "").strip()
|
|
687
736
|
if not raw:
|
|
688
737
|
return ""
|
|
689
738
|
try:
|
|
@@ -705,6 +754,11 @@ def _computer_created_label(c: dict[str, Any]) -> str:
|
|
|
705
754
|
|
|
706
755
|
|
|
707
756
|
|
|
757
|
+
def _computer_created_label(c: dict[str, Any]) -> str:
|
|
758
|
+
return _relative_time_label(c.get("created_at"))
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
|
|
708
762
|
def _computer_status_style(status: str) -> str:
|
|
709
763
|
if "cold" in status:
|
|
710
764
|
return "bold cyan"
|
|
@@ -999,6 +1053,27 @@ def _render_network_panel(payload: dict[str, Any], *, title: str = "url") -> Non
|
|
|
999
1053
|
_UI.console().print(Panel(body, title=title, border_style="cyan", expand=False, padding=(0, 1)))
|
|
1000
1054
|
|
|
1001
1055
|
|
|
1056
|
+
def _render_startup_panel(payload: dict[str, Any], *, title: str = "startup") -> None:
|
|
1057
|
+
mods = _UI.rich()
|
|
1058
|
+
Panel, Text = mods["Panel"], mods["Text"]
|
|
1059
|
+
body = Text()
|
|
1060
|
+
configured = bool(payload.get("configured"))
|
|
1061
|
+
rows: list[tuple[str, str, str | None]] = [("configured", "yes" if configured else "no", "bold white" if configured else "dim")]
|
|
1062
|
+
if configured:
|
|
1063
|
+
rows.extend(
|
|
1064
|
+
[
|
|
1065
|
+
("command", str(payload.get("command") or "—"), "white"),
|
|
1066
|
+
("cwd", str(payload.get("cwd") or "/home/ubuntu"), "dim"),
|
|
1067
|
+
("port", str(payload.get("port") or "—"), "bold white"),
|
|
1068
|
+
]
|
|
1069
|
+
)
|
|
1070
|
+
for label, value, style in rows:
|
|
1071
|
+
body.append(f"{label:<10}", style="dim")
|
|
1072
|
+
body.append(value, style=style or "white")
|
|
1073
|
+
body.append("\n")
|
|
1074
|
+
_UI.console().print(Panel(body, title=title, border_style="cyan", expand=False, padding=(0, 1)))
|
|
1075
|
+
|
|
1076
|
+
|
|
1002
1077
|
def _render_whoami_panel(payload: dict[str, Any]) -> None:
|
|
1003
1078
|
mods = _UI.rich()
|
|
1004
1079
|
Panel, Text = mods["Panel"], mods["Text"]
|
|
@@ -1018,6 +1093,84 @@ def _render_whoami_panel(payload: dict[str, Any]) -> None:
|
|
|
1018
1093
|
_UI.console().print(Panel(body, title="whoami", border_style="cyan", expand=False))
|
|
1019
1094
|
|
|
1020
1095
|
|
|
1096
|
+
|
|
1097
|
+
def _api_key_status_label(key: dict[str, Any]) -> str:
|
|
1098
|
+
if key.get("revoked_at"):
|
|
1099
|
+
return "revoked"
|
|
1100
|
+
if key.get("last_used_at"):
|
|
1101
|
+
return "used"
|
|
1102
|
+
return "unused"
|
|
1103
|
+
|
|
1104
|
+
|
|
1105
|
+
|
|
1106
|
+
def _api_key_status_style(status: str) -> str:
|
|
1107
|
+
if status == "revoked":
|
|
1108
|
+
return "bold red"
|
|
1109
|
+
if status == "used":
|
|
1110
|
+
return "bold green"
|
|
1111
|
+
return "yellow"
|
|
1112
|
+
|
|
1113
|
+
|
|
1114
|
+
|
|
1115
|
+
def _render_api_keys_table(payload: dict[str, Any]) -> None:
|
|
1116
|
+
mods = _UI.rich()
|
|
1117
|
+
Table = mods["Table"]
|
|
1118
|
+
api_keys = payload.get("api_keys") or []
|
|
1119
|
+
if not isinstance(api_keys, list) or not api_keys:
|
|
1120
|
+
_UI.console().print("[yellow]no api keys found[/yellow]")
|
|
1121
|
+
return
|
|
1122
|
+
|
|
1123
|
+
table = Table(title="API keys", expand=True, box=mods["box"].SIMPLE_HEAVY)
|
|
1124
|
+
table.add_column("ID", style="dim", no_wrap=True)
|
|
1125
|
+
table.add_column("Label", style="bold white")
|
|
1126
|
+
table.add_column("Prefix", style="white", no_wrap=True)
|
|
1127
|
+
table.add_column("Created", style="dim", no_wrap=True, justify="right")
|
|
1128
|
+
table.add_column("Last used", style="dim", no_wrap=True, justify="right")
|
|
1129
|
+
table.add_column("Status", no_wrap=True)
|
|
1130
|
+
|
|
1131
|
+
for entry in api_keys:
|
|
1132
|
+
if not isinstance(entry, dict):
|
|
1133
|
+
continue
|
|
1134
|
+
status = _api_key_status_label(entry)
|
|
1135
|
+
style = _api_key_status_style(status)
|
|
1136
|
+
table.add_row(
|
|
1137
|
+
str(entry.get("id") or "—"),
|
|
1138
|
+
str(entry.get("label") or "—"),
|
|
1139
|
+
str(entry.get("key_prefix") or "—"),
|
|
1140
|
+
_relative_time_label(entry.get("created_at")) or "—",
|
|
1141
|
+
_relative_time_label(entry.get("last_used_at")) or "—",
|
|
1142
|
+
f"[{style}]{status}[/{style}]",
|
|
1143
|
+
)
|
|
1144
|
+
|
|
1145
|
+
_UI.console().print(table)
|
|
1146
|
+
|
|
1147
|
+
|
|
1148
|
+
|
|
1149
|
+
def _render_api_key_created(payload: dict[str, Any]) -> None:
|
|
1150
|
+
mods = _UI.rich()
|
|
1151
|
+
Panel, Text = mods["Panel"], mods["Text"]
|
|
1152
|
+
key = payload.get("key") or {}
|
|
1153
|
+
secret = str(payload.get("api_key") or "")
|
|
1154
|
+
body = Text()
|
|
1155
|
+
rows = [
|
|
1156
|
+
("id", key.get("id")),
|
|
1157
|
+
("label", key.get("label")),
|
|
1158
|
+
("prefix", key.get("key_prefix")),
|
|
1159
|
+
]
|
|
1160
|
+
for label, value in rows:
|
|
1161
|
+
if value is None:
|
|
1162
|
+
continue
|
|
1163
|
+
body.append(f"{label:<12}", style="dim")
|
|
1164
|
+
body.append(f"{value}\n")
|
|
1165
|
+
if secret:
|
|
1166
|
+
body.append("save now ", style="bold yellow")
|
|
1167
|
+
body.append("this secret is only shown once\n", style="yellow")
|
|
1168
|
+
body.append("api key ", style="dim")
|
|
1169
|
+
body.append(secret, style="bold white")
|
|
1170
|
+
body.append("\n")
|
|
1171
|
+
_UI.console().print(Panel(body, title="api key created", border_style="cyan", expand=False))
|
|
1172
|
+
|
|
1173
|
+
|
|
1021
1174
|
# --------------------------------------------------------------------------- #
|
|
1022
1175
|
# Command handlers #
|
|
1023
1176
|
# --------------------------------------------------------------------------- #
|
|
@@ -1166,6 +1319,56 @@ def handle_logout(config: RuntimeConfig) -> int:
|
|
|
1166
1319
|
return report_success({"message": message}, render)
|
|
1167
1320
|
|
|
1168
1321
|
|
|
1322
|
+
|
|
1323
|
+
def handle_api_keys_list(config: RuntimeConfig) -> int:
|
|
1324
|
+
if (err := _require_api_key(config)) is not None:
|
|
1325
|
+
return err
|
|
1326
|
+
client = RuntimeClient(base_url=config.base_url, api_key=config.api_key)
|
|
1327
|
+
return report_success(client.list_api_keys(), _render_api_keys_table)
|
|
1328
|
+
|
|
1329
|
+
|
|
1330
|
+
|
|
1331
|
+
def handle_api_keys_create(config: RuntimeConfig, label: str | None) -> int:
|
|
1332
|
+
if (err := _require_api_key(config)) is not None:
|
|
1333
|
+
return err
|
|
1334
|
+
if label is None:
|
|
1335
|
+
if not _interactive():
|
|
1336
|
+
return report_error("label is required")
|
|
1337
|
+
label = _prompt_text("API key label")
|
|
1338
|
+
if not label:
|
|
1339
|
+
return report_error("label is required")
|
|
1340
|
+
|
|
1341
|
+
client = RuntimeClient(base_url=config.base_url, api_key=config.api_key)
|
|
1342
|
+
result = client.create_api_key(label)
|
|
1343
|
+
return report_success(result, _render_api_key_created)
|
|
1344
|
+
|
|
1345
|
+
|
|
1346
|
+
|
|
1347
|
+
def handle_api_keys_revoke(config: RuntimeConfig, key_id: str | None) -> int:
|
|
1348
|
+
if (err := _require_api_key(config)) is not None:
|
|
1349
|
+
return err
|
|
1350
|
+
if key_id is None:
|
|
1351
|
+
if not _interactive():
|
|
1352
|
+
return report_error("api key id is required")
|
|
1353
|
+
key_id = _prompt_text("API key id")
|
|
1354
|
+
if not key_id:
|
|
1355
|
+
return report_error("api key id is required")
|
|
1356
|
+
|
|
1357
|
+
try:
|
|
1358
|
+
resolved_key_id = int(str(key_id).strip())
|
|
1359
|
+
except ValueError:
|
|
1360
|
+
return report_error("api key id must be a number")
|
|
1361
|
+
|
|
1362
|
+
client = RuntimeClient(base_url=config.base_url, api_key=config.api_key)
|
|
1363
|
+
client.revoke_api_key(resolved_key_id)
|
|
1364
|
+
|
|
1365
|
+
def render(_: dict[str, Any]) -> None:
|
|
1366
|
+
_UI.console().print(f"[green]✓[/green] api key {resolved_key_id} revoked")
|
|
1367
|
+
|
|
1368
|
+
return report_success({"id": resolved_key_id, "message": f"api key {resolved_key_id} revoked"}, render)
|
|
1369
|
+
|
|
1370
|
+
|
|
1371
|
+
|
|
1169
1372
|
def handle_create(
|
|
1170
1373
|
config: RuntimeConfig,
|
|
1171
1374
|
name: str | None,
|
|
@@ -1260,6 +1463,111 @@ def handle_url(config: RuntimeConfig, computer_id: str | None) -> int:
|
|
|
1260
1463
|
return report_success(result, lambda p: _render_network_panel(p, title="url"))
|
|
1261
1464
|
|
|
1262
1465
|
|
|
1466
|
+
def handle_startup_show(config: RuntimeConfig, computer_id: str | None) -> int:
|
|
1467
|
+
if (err := _require_api_key(config)) is not None:
|
|
1468
|
+
return err
|
|
1469
|
+
|
|
1470
|
+
if computer_id is None:
|
|
1471
|
+
if not _interactive():
|
|
1472
|
+
return report_error("computer id is required")
|
|
1473
|
+
picked = _pick_computer(config, "Pick a computer")
|
|
1474
|
+
if picked is None:
|
|
1475
|
+
return 0
|
|
1476
|
+
computer_id = picked.get("id") or picked.get("slug")
|
|
1477
|
+
if not computer_id:
|
|
1478
|
+
return report_error("computer is missing an id")
|
|
1479
|
+
|
|
1480
|
+
client = RuntimeClient(base_url=config.base_url, api_key=config.api_key)
|
|
1481
|
+
computer_id = _resolve_computer_ref(client, computer_id)
|
|
1482
|
+
result = client.get_startup_config(computer_id)
|
|
1483
|
+
return report_success(result, lambda p: _render_startup_panel(p, title="startup"))
|
|
1484
|
+
|
|
1485
|
+
|
|
1486
|
+
def handle_startup_set(
|
|
1487
|
+
config: RuntimeConfig,
|
|
1488
|
+
computer_id: str | None,
|
|
1489
|
+
command: str | None,
|
|
1490
|
+
cwd: str | None,
|
|
1491
|
+
port: int | None,
|
|
1492
|
+
) -> int:
|
|
1493
|
+
if (err := _require_api_key(config)) is not None:
|
|
1494
|
+
return err
|
|
1495
|
+
if not computer_id:
|
|
1496
|
+
return report_error("computer id is required")
|
|
1497
|
+
if not command:
|
|
1498
|
+
return report_error("startup command is required")
|
|
1499
|
+
if port is None:
|
|
1500
|
+
return report_error("port is required")
|
|
1501
|
+
if port <= 0 or port > 65535:
|
|
1502
|
+
return report_error("port must be between 1 and 65535")
|
|
1503
|
+
|
|
1504
|
+
client = RuntimeClient(base_url=config.base_url, api_key=config.api_key)
|
|
1505
|
+
computer_id = _resolve_computer_ref(client, computer_id)
|
|
1506
|
+
result = _with_spinner(
|
|
1507
|
+
f"saving startup config for {computer_id}…",
|
|
1508
|
+
lambda: client.set_startup_config(computer_id, command=command, cwd=cwd, port=port),
|
|
1509
|
+
)
|
|
1510
|
+
return report_success(result, lambda p: _render_startup_panel(p, title="startup saved"))
|
|
1511
|
+
|
|
1512
|
+
|
|
1513
|
+
def handle_startup_clear(config: RuntimeConfig, computer_id: str | None) -> int:
|
|
1514
|
+
if (err := _require_api_key(config)) is not None:
|
|
1515
|
+
return err
|
|
1516
|
+
if not computer_id:
|
|
1517
|
+
return report_error("computer id is required")
|
|
1518
|
+
|
|
1519
|
+
client = RuntimeClient(base_url=config.base_url, api_key=config.api_key)
|
|
1520
|
+
computer_id = _resolve_computer_ref(client, computer_id)
|
|
1521
|
+
result = _with_spinner(
|
|
1522
|
+
f"clearing startup config for {computer_id}…",
|
|
1523
|
+
lambda: client.clear_startup_config(computer_id),
|
|
1524
|
+
)
|
|
1525
|
+
|
|
1526
|
+
def render(_: dict[str, Any]) -> None:
|
|
1527
|
+
_UI.console().print(f"[green]✓[/green] cleared durable startup config for [bold]{computer_id}[/bold]")
|
|
1528
|
+
|
|
1529
|
+
return report_success(result or {"configured": False}, render)
|
|
1530
|
+
|
|
1531
|
+
|
|
1532
|
+
def handle_service_show(config: RuntimeConfig, computer_id: str | None) -> int:
|
|
1533
|
+
if (err := _require_api_key(config)) is not None:
|
|
1534
|
+
return err
|
|
1535
|
+
|
|
1536
|
+
if computer_id is None:
|
|
1537
|
+
if not _interactive():
|
|
1538
|
+
return report_error("computer id is required")
|
|
1539
|
+
picked = _pick_computer(config, "Pick a computer")
|
|
1540
|
+
if picked is None:
|
|
1541
|
+
return 0
|
|
1542
|
+
computer_id = picked.get("id") or picked.get("slug")
|
|
1543
|
+
if not computer_id:
|
|
1544
|
+
return report_error("computer is missing an id")
|
|
1545
|
+
|
|
1546
|
+
client = RuntimeClient(base_url=config.base_url, api_key=config.api_key)
|
|
1547
|
+
computer_id = _resolve_computer_ref(client, computer_id)
|
|
1548
|
+
result = client.get_service_config(computer_id)
|
|
1549
|
+
return report_success(result, lambda p: _render_startup_panel(p, title="service"))
|
|
1550
|
+
|
|
1551
|
+
|
|
1552
|
+
def handle_service_clear(config: RuntimeConfig, computer_id: str | None) -> int:
|
|
1553
|
+
if (err := _require_api_key(config)) is not None:
|
|
1554
|
+
return err
|
|
1555
|
+
if not computer_id:
|
|
1556
|
+
return report_error("computer id is required")
|
|
1557
|
+
|
|
1558
|
+
client = RuntimeClient(base_url=config.base_url, api_key=config.api_key)
|
|
1559
|
+
computer_id = _resolve_computer_ref(client, computer_id)
|
|
1560
|
+
result = _with_spinner(
|
|
1561
|
+
f"clearing service for {computer_id}…",
|
|
1562
|
+
lambda: client.clear_service_config(computer_id),
|
|
1563
|
+
)
|
|
1564
|
+
|
|
1565
|
+
def render(_: dict[str, Any]) -> None:
|
|
1566
|
+
_UI.console().print(f"[green]✓[/green] cleared durable service for [bold]{computer_id}[/bold]")
|
|
1567
|
+
|
|
1568
|
+
return report_success(result or {"configured": False}, render)
|
|
1569
|
+
|
|
1570
|
+
|
|
1263
1571
|
def handle_start(config: RuntimeConfig, computer_id: str | None) -> int:
|
|
1264
1572
|
if (err := _require_api_key(config)) is not None:
|
|
1265
1573
|
return err
|
|
@@ -1433,7 +1741,7 @@ def handle_publish(config: RuntimeConfig, computer_id: str | None, port: int | N
|
|
|
1433
1741
|
|
|
1434
1742
|
def render(payload: dict[str, Any]) -> None:
|
|
1435
1743
|
_UI.console().print(
|
|
1436
|
-
f"[green]✓[/green] public url for [bold]{computer_id}[/bold]
|
|
1744
|
+
f"[green]✓[/green] public url for [bold]{computer_id}[/bold] now points at durable app port [bold]{payload.get('published_port') or port}[/bold]"
|
|
1437
1745
|
)
|
|
1438
1746
|
url = payload.get("public_url")
|
|
1439
1747
|
if url:
|
|
@@ -1474,6 +1782,10 @@ def handle_proxy_start(config: RuntimeConfig, computer_id: str | None, specs: li
|
|
|
1474
1782
|
if started_ports:
|
|
1475
1783
|
_stop_proxy_ports(started_ports, stop_all=False)
|
|
1476
1784
|
return report_error(str(exc), status_code=exc.status_code)
|
|
1785
|
+
except Exception as exc:
|
|
1786
|
+
if started_ports:
|
|
1787
|
+
_stop_proxy_ports(started_ports, stop_all=False)
|
|
1788
|
+
return report_error(str(exc))
|
|
1477
1789
|
started_ports.append(local_port)
|
|
1478
1790
|
started.append({"computer_id": resolved_id, "computer": computer_id, "local_port": local_port, "remote_port": remote_port})
|
|
1479
1791
|
|
|
@@ -1526,6 +1838,17 @@ def handle_proxy_stop(config: RuntimeConfig, local_ports: list[int] | None, *, s
|
|
|
1526
1838
|
return report_success({"proxies": stopped}, render if _interactive() else None)
|
|
1527
1839
|
|
|
1528
1840
|
|
|
1841
|
+
def _proxy_preflight(base_url: str, api_key: str, computer_id: str, remote_port: int) -> None:
|
|
1842
|
+
client = RuntimeClient(base_url=base_url, api_key=api_key)
|
|
1843
|
+
ticket_payload = client.create_proxy_ticket(computer_id, remote_port)
|
|
1844
|
+
ws_url = _resolve_ws_url(base_url, ticket_payload.get("ws_url"))
|
|
1845
|
+
if not ws_url:
|
|
1846
|
+
raise RuntimeAPIError("proxy endpoint did not return a websocket url")
|
|
1847
|
+
with websocket_connect(ws_url, open_timeout=DEFAULT_WARMUP_TIMEOUT, close_timeout=1, max_size=None):
|
|
1848
|
+
return
|
|
1849
|
+
|
|
1850
|
+
|
|
1851
|
+
|
|
1529
1852
|
def run_proxy_agent(base_url: str, api_key: str, computer_id: str, display_name: str, local_port: int, remote_port: int) -> int:
|
|
1530
1853
|
if not api_key:
|
|
1531
1854
|
print("proxy agent missing api key", file=sys.stderr)
|
|
@@ -1541,6 +1864,14 @@ def run_proxy_agent(base_url: str, api_key: str, computer_id: str, display_name:
|
|
|
1541
1864
|
print(f"proxy listen failed on localhost:{local_port}: {exc}", file=sys.stderr)
|
|
1542
1865
|
return 1
|
|
1543
1866
|
|
|
1867
|
+
try:
|
|
1868
|
+
_proxy_preflight(base_url, api_key, computer_id, remote_port)
|
|
1869
|
+
except Exception as exc:
|
|
1870
|
+
with contextlib.suppress(OSError):
|
|
1871
|
+
listener.close()
|
|
1872
|
+
print(f"proxy preflight failed for localhost:{local_port} -> {computer_id}:{remote_port}: {exc}", file=sys.stderr)
|
|
1873
|
+
return 1
|
|
1874
|
+
|
|
1544
1875
|
proxy_dir = _proxy_dir()
|
|
1545
1876
|
proxy_dir.mkdir(parents=True, exist_ok=True)
|
|
1546
1877
|
state = {
|
|
@@ -43,6 +43,15 @@ class RuntimeClient:
|
|
|
43
43
|
def logout(self) -> dict[str, Any]:
|
|
44
44
|
return self._request("POST", "/api/auth/logout", auth_required=True)
|
|
45
45
|
|
|
46
|
+
def list_api_keys(self) -> dict[str, Any]:
|
|
47
|
+
return self._request("GET", "/api/api-keys", auth_required=True)
|
|
48
|
+
|
|
49
|
+
def create_api_key(self, label: str) -> dict[str, Any]:
|
|
50
|
+
return self._request("POST", "/api/api-keys", json={"label": label}, auth_required=True)
|
|
51
|
+
|
|
52
|
+
def revoke_api_key(self, key_id: int) -> dict[str, Any]:
|
|
53
|
+
return self._request("POST", f"/api/api-keys/{key_id}/revoke", auth_required=True)
|
|
54
|
+
|
|
46
55
|
# --- Computers ---
|
|
47
56
|
|
|
48
57
|
def create_computer(
|
|
@@ -82,6 +91,31 @@ class RuntimeClient:
|
|
|
82
91
|
def get_computer_network(self, computer_id: str) -> dict[str, Any]:
|
|
83
92
|
return self._request("GET", f"/api/computers/{computer_id}/network", auth_required=True)
|
|
84
93
|
|
|
94
|
+
def get_startup_config(self, computer_id: str) -> dict[str, Any]:
|
|
95
|
+
return self._request("GET", f"/api/computers/{computer_id}/startup", auth_required=True)
|
|
96
|
+
|
|
97
|
+
def get_service_config(self, computer_id: str) -> dict[str, Any]:
|
|
98
|
+
return self.get_startup_config(computer_id)
|
|
99
|
+
|
|
100
|
+
def set_startup_config(
|
|
101
|
+
self,
|
|
102
|
+
computer_id: str,
|
|
103
|
+
*,
|
|
104
|
+
command: str,
|
|
105
|
+
cwd: str | None = None,
|
|
106
|
+
port: int,
|
|
107
|
+
) -> dict[str, Any]:
|
|
108
|
+
body: dict[str, Any] = {"command": command, "port": port}
|
|
109
|
+
if cwd is not None:
|
|
110
|
+
body["cwd"] = cwd
|
|
111
|
+
return self._request("PUT", f"/api/computers/{computer_id}/startup", json=body, auth_required=True)
|
|
112
|
+
|
|
113
|
+
def clear_startup_config(self, computer_id: str) -> dict[str, Any]:
|
|
114
|
+
return self._request("DELETE", f"/api/computers/{computer_id}/startup", auth_required=True)
|
|
115
|
+
|
|
116
|
+
def clear_service_config(self, computer_id: str) -> dict[str, Any]:
|
|
117
|
+
return self.clear_startup_config(computer_id)
|
|
118
|
+
|
|
85
119
|
def start_computer(self, computer_id: str) -> dict[str, Any]:
|
|
86
120
|
return self._request(
|
|
87
121
|
"POST",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: runtime-sdk
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.12
|
|
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
|
|
@@ -52,18 +52,32 @@ runtime verify 123456
|
|
|
52
52
|
runtime whoami
|
|
53
53
|
runtime logout
|
|
54
54
|
runtime login --api-key rt_live_...
|
|
55
|
+
runtime api-keys list
|
|
56
|
+
runtime api-keys create "my laptop"
|
|
57
|
+
runtime api-keys revoke 123
|
|
55
58
|
|
|
56
59
|
# Computers
|
|
57
|
-
runtime create #
|
|
60
|
+
runtime create # creates a computer with the starter app already published
|
|
58
61
|
runtime create myapp --command "python3 app.py" --cwd /home/ubuntu --port 3000
|
|
59
62
|
runtime enter <name-or-id> # accepts slug/name like test, or the computer id
|
|
60
63
|
runtime list
|
|
61
64
|
runtime info <id>
|
|
62
65
|
runtime start <id>
|
|
63
|
-
runtime run <id> "echo hello"
|
|
66
|
+
runtime run <id> "echo hello" # one-shot foreground command only
|
|
64
67
|
runtime run <id> "apt install -y nodejs" --uid 0
|
|
65
|
-
runtime
|
|
68
|
+
runtime startup show <id>
|
|
69
|
+
runtime startup set <id> --command "python3 app.py" --cwd /home/ubuntu --port 3000
|
|
70
|
+
runtime startup clear <id> # low-level durable service config
|
|
71
|
+
runtime service show <id> # user-facing alias for the durable published app
|
|
72
|
+
runtime service clear <id>
|
|
73
|
+
runtime publish <id> 3000 # promote the running app on port 3000 to the public durable app
|
|
66
74
|
runtime delete <id>
|
|
75
|
+
|
|
76
|
+
# Inside a running computer, the helper installed by Runtime can manage the
|
|
77
|
+
# durable app without leaving the sandbox:
|
|
78
|
+
# runtime-env publish 3000
|
|
79
|
+
# runtime-env service show
|
|
80
|
+
# runtime-env service clear
|
|
67
81
|
```
|
|
68
82
|
|
|
69
83
|
## Python
|
|
@@ -73,12 +87,12 @@ from runtime_sdk import RuntimeClient
|
|
|
73
87
|
|
|
74
88
|
client = RuntimeClient(base_url="https://api.runruntime.dev", api_key="rt_live_...")
|
|
75
89
|
|
|
76
|
-
# Create a computer
|
|
90
|
+
# Create a computer. New computers start with the starter app already published.
|
|
77
91
|
computer = client.create_computer()
|
|
78
92
|
print(computer["public_url"]) # https://goldbird.runruntime.dev
|
|
79
93
|
|
|
80
|
-
# Or create one with
|
|
81
|
-
# That
|
|
94
|
+
# Or create one with an explicit durable app command.
|
|
95
|
+
# That saved service is replayed after cold restore / start.
|
|
82
96
|
app = client.create_computer(
|
|
83
97
|
slug="myapp",
|
|
84
98
|
command="python3 app.py",
|
|
@@ -93,7 +107,8 @@ print(result["stdout"])
|
|
|
93
107
|
# Wake a cold computer explicitly
|
|
94
108
|
client.start_computer(app["id"])
|
|
95
109
|
|
|
96
|
-
#
|
|
110
|
+
# Promote the running app on a local port to the durable public app.
|
|
111
|
+
# Runtime inspects the listening process and saves its command + cwd when possible.
|
|
97
112
|
client.publish_port(computer["id"], 3000)
|
|
98
113
|
|
|
99
114
|
# List, info, delete
|
|
@@ -120,10 +135,16 @@ make smoke
|
|
|
120
135
|
Use `make deploy` instead of `make sync` when migrations, env
|
|
121
136
|
files, Caddy, or systemd units changed.
|
|
122
137
|
|
|
123
|
-
Cold restore and
|
|
124
|
-
computers
|
|
125
|
-
|
|
126
|
-
|
|
138
|
+
Cold restore and explicit `runtime start` replay the saved published app command.
|
|
139
|
+
New computers seed that durable app from the starter workspace. Later,
|
|
140
|
+
`runtime publish <id> <port>` can promote a running listener into the saved
|
|
141
|
+
public app definition by inspecting the live process. Inside a running computer,
|
|
142
|
+
`runtime-env publish <port>` does the same thing using a computer-scoped token
|
|
143
|
+
installed by Runtime. `runtime service show|clear` and `runtime-env service
|
|
144
|
+
show|clear` expose that same durable app state directly. The low-level
|
|
145
|
+
`runtime startup ...` commands still map to the same durable state. A one-off
|
|
146
|
+
`runtime run` stays one-shot: the filesystem is restored after going cold, but
|
|
147
|
+
that ad-hoc process is not.
|
|
127
148
|
|
|
128
149
|
Run the SDK unit tests through the backend project environment:
|
|
129
150
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|