runtime-sdk 0.4.49__py3-none-win_arm64.whl
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/__init__.py +18 -0
- runtime_sdk/account.py +243 -0
- runtime_sdk/arguments.py +10 -0
- runtime_sdk/auth.py +175 -0
- runtime_sdk/cli.py +985 -0
- runtime_sdk/client.py +616 -0
- runtime_sdk/completion.py +348 -0
- runtime_sdk/computers.py +578 -0
- runtime_sdk/config.py +201 -0
- runtime_sdk/console.py +118 -0
- runtime_sdk/files.py +233 -0
- runtime_sdk/github.py +229 -0
- runtime_sdk/goals.py +368 -0
- runtime_sdk/output.py +69 -0
- runtime_sdk/providers.py +422 -0
- runtime_sdk/proxy.py +600 -0
- runtime_sdk/runtime-tui.exe +0 -0
- runtime_sdk/secrets.py +155 -0
- runtime_sdk/services.py +173 -0
- runtime_sdk/ssh.py +197 -0
- runtime_sdk/terminal.py +330 -0
- runtime_sdk-0.4.49.dist-info/METADATA +273 -0
- runtime_sdk-0.4.49.dist-info/RECORD +26 -0
- runtime_sdk-0.4.49.dist-info/WHEEL +5 -0
- runtime_sdk-0.4.49.dist-info/entry_points.txt +2 -0
- runtime_sdk-0.4.49.dist-info/top_level.txt +1 -0
runtime_sdk/computers.py
ADDED
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import difflib
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
from typing import Any, Callable
|
|
8
|
+
from urllib.parse import urlparse
|
|
9
|
+
|
|
10
|
+
from runtime_sdk import arguments, output as cli_output, terminal
|
|
11
|
+
from runtime_sdk.auth import authenticated_client
|
|
12
|
+
from runtime_sdk.client import RuntimeAPIError, RuntimeClient
|
|
13
|
+
from runtime_sdk.config import RuntimeConfig
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def close_matches(ref: str, candidates: list[str], limit: int = 3) -> list[str]:
|
|
17
|
+
"""Return up to `limit` candidates that look similar to ref."""
|
|
18
|
+
ref = (ref or "").strip()
|
|
19
|
+
if not ref:
|
|
20
|
+
return []
|
|
21
|
+
pool = [c for c in candidates if c]
|
|
22
|
+
return difflib.get_close_matches(ref, pool, n=limit, cutoff=0.4)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def computer_power_state(c: dict[str, Any]) -> str:
|
|
26
|
+
power = c.get("power_state")
|
|
27
|
+
if power is not None:
|
|
28
|
+
return str(power or "?")
|
|
29
|
+
status = str(c.get("status") or "?")
|
|
30
|
+
if status in ("warm", "cold"):
|
|
31
|
+
return status
|
|
32
|
+
if status == "deleted":
|
|
33
|
+
return "cold"
|
|
34
|
+
return "warm"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def computer_internal_status(c: dict[str, Any]) -> str:
|
|
38
|
+
internal = c.get("internal_status")
|
|
39
|
+
if internal is not None:
|
|
40
|
+
return str(internal or "?")
|
|
41
|
+
return str(c.get("status") or "?")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def computer_status_label(c: dict[str, Any]) -> str:
|
|
45
|
+
power = computer_power_state(c)
|
|
46
|
+
internal = computer_internal_status(c)
|
|
47
|
+
if power == internal:
|
|
48
|
+
return power
|
|
49
|
+
if power == "warm" and internal == "running":
|
|
50
|
+
return power
|
|
51
|
+
if internal in {"creating", "archiving", "restoring", "orphaned"}:
|
|
52
|
+
return internal
|
|
53
|
+
return power
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _computer_matches_ref(c: dict[str, Any], ref: str) -> bool:
|
|
57
|
+
ref = ref.strip()
|
|
58
|
+
if not ref:
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
candidates = {
|
|
62
|
+
str(c.get("id") or "").strip(),
|
|
63
|
+
str(c.get("slug") or "").strip(),
|
|
64
|
+
str(c.get("hostname") or "").strip(),
|
|
65
|
+
str(c.get("public_url") or "").strip(),
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public_url = str(c.get("public_url") or "").strip()
|
|
69
|
+
if public_url:
|
|
70
|
+
parsed = urlparse(public_url)
|
|
71
|
+
if parsed.netloc:
|
|
72
|
+
candidates.add(parsed.netloc)
|
|
73
|
+
first_label = parsed.netloc.split(".", 1)[0].strip()
|
|
74
|
+
if first_label:
|
|
75
|
+
candidates.add(first_label)
|
|
76
|
+
|
|
77
|
+
return ref in {value for value in candidates if value}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def resolve_computer_ref(client: RuntimeClient, ref: str) -> str:
|
|
81
|
+
ref = ref.strip()
|
|
82
|
+
if not ref:
|
|
83
|
+
raise RuntimeAPIError("computer id is required")
|
|
84
|
+
if ref.startswith("cmp_"):
|
|
85
|
+
return ref
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
payload = client.list_computers()
|
|
89
|
+
except RuntimeAPIError:
|
|
90
|
+
return ref
|
|
91
|
+
|
|
92
|
+
computers = payload.get("computers") or []
|
|
93
|
+
if not isinstance(computers, list):
|
|
94
|
+
computers = []
|
|
95
|
+
|
|
96
|
+
matches = [
|
|
97
|
+
c for c in computers if isinstance(c, dict) and _computer_matches_ref(c, ref)
|
|
98
|
+
]
|
|
99
|
+
if len(matches) == 1:
|
|
100
|
+
resolved = str(matches[0].get("id") or "").strip()
|
|
101
|
+
return resolved or ref
|
|
102
|
+
if len(matches) > 1:
|
|
103
|
+
raise RuntimeAPIError(
|
|
104
|
+
f"multiple computers matched {ref!r}; use the exact computer id"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
slugs = [str(c.get("slug") or "").strip() for c in computers if isinstance(c, dict)]
|
|
108
|
+
suggestions = close_matches(ref, [s for s in slugs if s])
|
|
109
|
+
if suggestions:
|
|
110
|
+
hint = ", ".join(repr(s) for s in suggestions)
|
|
111
|
+
raise RuntimeAPIError(f"computer {ref!r} not found. Did you mean {hint}?")
|
|
112
|
+
raise RuntimeAPIError(f"computer {ref!r} not found")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _computer_name(c: dict[str, Any]) -> str:
|
|
116
|
+
return str(c.get("slug") or c.get("id") or "?")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _computer_email_address(c: dict[str, Any]) -> str:
|
|
120
|
+
explicit = str(c.get("email_address") or "").strip()
|
|
121
|
+
if explicit:
|
|
122
|
+
return explicit
|
|
123
|
+
slug = str(c.get("slug") or "").strip()
|
|
124
|
+
public_url = str(c.get("public_url") or "").strip()
|
|
125
|
+
if not slug or not public_url:
|
|
126
|
+
return ""
|
|
127
|
+
host = urlparse(public_url).netloc
|
|
128
|
+
if not host:
|
|
129
|
+
return ""
|
|
130
|
+
suffix = host.removeprefix(slug + ".")
|
|
131
|
+
if suffix == host or not suffix:
|
|
132
|
+
return ""
|
|
133
|
+
return f"{slug}@mail.{suffix}"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def with_computer_email(payload: dict[str, Any]) -> dict[str, Any]:
|
|
137
|
+
if payload.get("email_address"):
|
|
138
|
+
return payload
|
|
139
|
+
email_address = _computer_email_address(payload)
|
|
140
|
+
if not email_address:
|
|
141
|
+
return payload
|
|
142
|
+
enriched = dict(payload)
|
|
143
|
+
enriched["email_address"] = email_address
|
|
144
|
+
return enriched
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _computer_sort_key(c: dict[str, Any]) -> tuple[int, str, str]:
|
|
148
|
+
status = computer_status_label(c)
|
|
149
|
+
priority = 1
|
|
150
|
+
if "warm" in status or "running" in status:
|
|
151
|
+
priority = 0
|
|
152
|
+
elif "cold" in status:
|
|
153
|
+
priority = 1
|
|
154
|
+
elif "restoring" in status or "creating" in status:
|
|
155
|
+
priority = 2
|
|
156
|
+
else:
|
|
157
|
+
priority = 3
|
|
158
|
+
return (priority, _computer_name(c).lower(), str(c.get("id") or ""))
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def pick_computer(
|
|
162
|
+
config: RuntimeConfig,
|
|
163
|
+
prompt: str,
|
|
164
|
+
predicate: Callable[[dict[str, Any]], bool] | None = None,
|
|
165
|
+
) -> dict[str, Any] | None:
|
|
166
|
+
client = authenticated_client(config)
|
|
167
|
+
payload = client.list_computers()
|
|
168
|
+
computers = payload.get("computers") or []
|
|
169
|
+
if not isinstance(computers, list):
|
|
170
|
+
computers = []
|
|
171
|
+
if predicate is not None:
|
|
172
|
+
computers = [c for c in computers if isinstance(c, dict) and predicate(c)]
|
|
173
|
+
computers = sorted(computers, key=_computer_sort_key)
|
|
174
|
+
if not computers:
|
|
175
|
+
print("no matching computers found", file=sys.stderr)
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
for index, computer in enumerate(computers, start=1):
|
|
179
|
+
print(
|
|
180
|
+
f"{index}. {_computer_name(computer)} ({computer_status_label(computer)})"
|
|
181
|
+
)
|
|
182
|
+
selection = cli_output.prompt_text(f"{prompt} [1-{len(computers)}]")
|
|
183
|
+
if selection is None:
|
|
184
|
+
return None
|
|
185
|
+
try:
|
|
186
|
+
index = int(selection) - 1
|
|
187
|
+
except ValueError:
|
|
188
|
+
print("selection must be a number", file=sys.stderr)
|
|
189
|
+
return None
|
|
190
|
+
if index < 0 or index >= len(computers):
|
|
191
|
+
print("selection is out of range", file=sys.stderr)
|
|
192
|
+
return None
|
|
193
|
+
return computers[index]
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def required_computer_id(config: RuntimeConfig, computer_id: str | None) -> str | int:
|
|
197
|
+
if computer_id is not None:
|
|
198
|
+
return computer_id
|
|
199
|
+
if not cli_output.interactive():
|
|
200
|
+
return cli_output.report_error("computer id is required")
|
|
201
|
+
picked = pick_computer(config, "Pick a computer")
|
|
202
|
+
if picked is None:
|
|
203
|
+
return 0
|
|
204
|
+
value = str(picked.get("id") or picked.get("slug") or "").strip()
|
|
205
|
+
if not value:
|
|
206
|
+
return cli_output.report_error("computer is missing an id")
|
|
207
|
+
return value
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def handle_create(
|
|
211
|
+
config: RuntimeConfig,
|
|
212
|
+
name: str | None,
|
|
213
|
+
startup_command: str | None,
|
|
214
|
+
cwd: str | None,
|
|
215
|
+
port: int | None,
|
|
216
|
+
network_mode: str | None = None,
|
|
217
|
+
allowed_hosts: list[str] | None = None,
|
|
218
|
+
) -> int:
|
|
219
|
+
if (err := cli_output.require_auth(config)) is not None:
|
|
220
|
+
return err
|
|
221
|
+
|
|
222
|
+
if name is None and cli_output.interactive():
|
|
223
|
+
name = cli_output.prompt_text("Name your computer")
|
|
224
|
+
|
|
225
|
+
if startup_command is None and port is not None:
|
|
226
|
+
return cli_output.report_error(
|
|
227
|
+
"startup command is required when port is provided"
|
|
228
|
+
)
|
|
229
|
+
if startup_command is not None and port is None:
|
|
230
|
+
return cli_output.report_error(
|
|
231
|
+
"port is required when startup command is provided"
|
|
232
|
+
)
|
|
233
|
+
allowed_hosts = allowed_hosts or []
|
|
234
|
+
network_error = _validate_network_policy(
|
|
235
|
+
network_mode, allowed_hosts, allow_default=True
|
|
236
|
+
)
|
|
237
|
+
if network_error:
|
|
238
|
+
return cli_output.report_error(network_error)
|
|
239
|
+
|
|
240
|
+
client = authenticated_client(config)
|
|
241
|
+
try:
|
|
242
|
+
started_at = time.perf_counter()
|
|
243
|
+
create_args: dict[str, Any] = {
|
|
244
|
+
"slug": name,
|
|
245
|
+
"command": startup_command,
|
|
246
|
+
"cwd": cwd,
|
|
247
|
+
"port": port,
|
|
248
|
+
}
|
|
249
|
+
if network_mode is not None:
|
|
250
|
+
create_args["network_mode"] = network_mode
|
|
251
|
+
create_args["allowed_hosts"] = allowed_hosts
|
|
252
|
+
result = client.create_computer(**create_args)
|
|
253
|
+
result["creation_duration_ms"] = int(
|
|
254
|
+
round((time.perf_counter() - started_at) * 1000)
|
|
255
|
+
)
|
|
256
|
+
except RuntimeAPIError as exc:
|
|
257
|
+
if (
|
|
258
|
+
exc.status_code == 409
|
|
259
|
+
and (
|
|
260
|
+
"slug already in use" in str(exc)
|
|
261
|
+
or "computer name or email address already in use" in str(exc)
|
|
262
|
+
)
|
|
263
|
+
and name
|
|
264
|
+
):
|
|
265
|
+
return cli_output.report_error(
|
|
266
|
+
f"computer name {name!r} is unavailable. Names are public URL slugs and VM email addresses; an active/cold computer or reserved email address already owns it.",
|
|
267
|
+
status_code=exc.status_code,
|
|
268
|
+
)
|
|
269
|
+
raise
|
|
270
|
+
|
|
271
|
+
result = with_computer_email(result)
|
|
272
|
+
code = cli_output.report_success(result)
|
|
273
|
+
if code != 0 or not cli_output.interactive():
|
|
274
|
+
return code
|
|
275
|
+
|
|
276
|
+
computer_id = result.get("id") or result.get("slug")
|
|
277
|
+
if not computer_id:
|
|
278
|
+
return code
|
|
279
|
+
return terminal.enter_computer(config, str(computer_id))
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def handle_network(config: RuntimeConfig, args: argparse.Namespace) -> int:
|
|
283
|
+
if (err := cli_output.require_auth(config)) is not None:
|
|
284
|
+
return err
|
|
285
|
+
client = authenticated_client(config)
|
|
286
|
+
if args.network_action == "default":
|
|
287
|
+
action = args.network_default_action
|
|
288
|
+
if action == "show":
|
|
289
|
+
return cli_output.report_success(
|
|
290
|
+
{"network_policy": client.get_default_network()}
|
|
291
|
+
)
|
|
292
|
+
hosts = args.hosts if action == "allowlist" else []
|
|
293
|
+
return cli_output.report_success(
|
|
294
|
+
{"network_policy": client.set_default_network(action, hosts)}
|
|
295
|
+
)
|
|
296
|
+
if args.network_action == "show":
|
|
297
|
+
computer_id = required_computer_id(config, args.id)
|
|
298
|
+
if isinstance(computer_id, int):
|
|
299
|
+
return computer_id
|
|
300
|
+
computer_id = resolve_computer_ref(client, computer_id)
|
|
301
|
+
return cli_output.report_success(client.get_computer_network(computer_id))
|
|
302
|
+
if args.network_action == "denials":
|
|
303
|
+
computer_id = required_computer_id(config, args.id)
|
|
304
|
+
if isinstance(computer_id, int):
|
|
305
|
+
return computer_id
|
|
306
|
+
computer_id = resolve_computer_ref(client, computer_id)
|
|
307
|
+
return cli_output.report_success(client.list_network_denials(computer_id))
|
|
308
|
+
if args.network_action == "set":
|
|
309
|
+
error = _validate_network_policy(args.mode, args.hosts)
|
|
310
|
+
if error:
|
|
311
|
+
return cli_output.report_error(error)
|
|
312
|
+
computer_id = required_computer_id(config, args.id)
|
|
313
|
+
if isinstance(computer_id, int):
|
|
314
|
+
return computer_id
|
|
315
|
+
computer_id = resolve_computer_ref(client, computer_id)
|
|
316
|
+
return cli_output.report_success(
|
|
317
|
+
client.set_computer_network(computer_id, args.mode, args.hosts)
|
|
318
|
+
)
|
|
319
|
+
return cli_output.report_error("unknown network command")
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _validate_network_policy(
|
|
323
|
+
mode: str | None, hosts: list[str], *, allow_default: bool = False
|
|
324
|
+
) -> str | None:
|
|
325
|
+
if mode is None:
|
|
326
|
+
if hosts:
|
|
327
|
+
return "--allow-host requires --network allowlist"
|
|
328
|
+
return None if allow_default else "network mode is required"
|
|
329
|
+
if mode == "allowlist" and not hosts:
|
|
330
|
+
return "allowlist mode requires at least one host"
|
|
331
|
+
if mode != "allowlist" and hosts:
|
|
332
|
+
return "hosts can only be used with allowlist mode"
|
|
333
|
+
return None
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def handle_info(config: RuntimeConfig, computer_id: str | None) -> int:
|
|
337
|
+
if (err := cli_output.require_auth(config)) is not None:
|
|
338
|
+
return err
|
|
339
|
+
|
|
340
|
+
if computer_id is None:
|
|
341
|
+
if not cli_output.interactive():
|
|
342
|
+
return cli_output.report_error("computer id is required")
|
|
343
|
+
picked = pick_computer(config, "Pick a computer")
|
|
344
|
+
if picked is None:
|
|
345
|
+
return 0
|
|
346
|
+
computer_id = picked.get("id") or picked.get("slug")
|
|
347
|
+
if not computer_id:
|
|
348
|
+
return cli_output.report_error("computer is missing an id")
|
|
349
|
+
|
|
350
|
+
client = authenticated_client(config)
|
|
351
|
+
computer_id = resolve_computer_ref(client, computer_id)
|
|
352
|
+
result = client.get_computer(computer_id)
|
|
353
|
+
return cli_output.report_success(result)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def handle_url(config: RuntimeConfig, computer_id: str | None) -> int:
|
|
357
|
+
if (err := cli_output.require_auth(config)) is not None:
|
|
358
|
+
return err
|
|
359
|
+
|
|
360
|
+
if computer_id is None:
|
|
361
|
+
if not cli_output.interactive():
|
|
362
|
+
return cli_output.report_error("computer id is required")
|
|
363
|
+
picked = pick_computer(config, "Pick a computer")
|
|
364
|
+
if picked is None:
|
|
365
|
+
return 0
|
|
366
|
+
computer_id = picked.get("id") or picked.get("slug")
|
|
367
|
+
if not computer_id:
|
|
368
|
+
return cli_output.report_error("computer is missing an id")
|
|
369
|
+
|
|
370
|
+
client = authenticated_client(config)
|
|
371
|
+
computer_id = resolve_computer_ref(client, computer_id)
|
|
372
|
+
result = client.get_computer_network(computer_id)
|
|
373
|
+
return cli_output.report_success(result)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def handle_share(
|
|
377
|
+
config: RuntimeConfig, visibility: str | None, computer_id: str | None
|
|
378
|
+
) -> int:
|
|
379
|
+
if (err := cli_output.require_auth(config)) is not None:
|
|
380
|
+
return err
|
|
381
|
+
if visibility not in ("public", "private"):
|
|
382
|
+
return cli_output.report_error("share target must be public or private")
|
|
383
|
+
|
|
384
|
+
if computer_id is None:
|
|
385
|
+
if not cli_output.interactive():
|
|
386
|
+
return cli_output.report_error("computer id is required")
|
|
387
|
+
picked = pick_computer(config, "Pick a computer")
|
|
388
|
+
if picked is None:
|
|
389
|
+
return 0
|
|
390
|
+
computer_id = picked.get("id") or picked.get("slug")
|
|
391
|
+
if not computer_id:
|
|
392
|
+
return cli_output.report_error("computer is missing an id")
|
|
393
|
+
|
|
394
|
+
client = authenticated_client(config)
|
|
395
|
+
computer_id = resolve_computer_ref(client, computer_id)
|
|
396
|
+
result = client.set_visibility(computer_id, visibility)
|
|
397
|
+
|
|
398
|
+
return cli_output.report_success(result)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def handle_start(config: RuntimeConfig, computer_id: str | None) -> int:
|
|
402
|
+
if (err := cli_output.require_auth(config)) is not None:
|
|
403
|
+
return err
|
|
404
|
+
|
|
405
|
+
if computer_id is None:
|
|
406
|
+
if not cli_output.interactive():
|
|
407
|
+
return cli_output.report_error("computer id is required")
|
|
408
|
+
picked = pick_computer(
|
|
409
|
+
config,
|
|
410
|
+
"Pick a cold computer to wake",
|
|
411
|
+
lambda c: computer_power_state(c) == "cold",
|
|
412
|
+
)
|
|
413
|
+
if picked is None:
|
|
414
|
+
return 0
|
|
415
|
+
computer_id = picked.get("id") or picked.get("slug")
|
|
416
|
+
if not computer_id:
|
|
417
|
+
return cli_output.report_error("computer is missing an id")
|
|
418
|
+
|
|
419
|
+
client = authenticated_client(config)
|
|
420
|
+
computer_id = resolve_computer_ref(client, computer_id)
|
|
421
|
+
result = client.start_computer(computer_id)
|
|
422
|
+
|
|
423
|
+
return cli_output.report_success(result)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def handle_enter(
|
|
427
|
+
config: RuntimeConfig,
|
|
428
|
+
computer_id: str | None,
|
|
429
|
+
command_parts: list[str] | None = None,
|
|
430
|
+
) -> int:
|
|
431
|
+
if (err := cli_output.require_auth(config)) is not None:
|
|
432
|
+
return err
|
|
433
|
+
if not cli_output.interactive():
|
|
434
|
+
return cli_output.report_error("enter requires an interactive terminal")
|
|
435
|
+
|
|
436
|
+
command = arguments.command_from_remainder(command_parts)
|
|
437
|
+
|
|
438
|
+
if computer_id is None:
|
|
439
|
+
picked = pick_computer(
|
|
440
|
+
config,
|
|
441
|
+
"Pick a computer to enter",
|
|
442
|
+
lambda c: computer_internal_status(c) in ("running", "cold"),
|
|
443
|
+
)
|
|
444
|
+
if picked is None:
|
|
445
|
+
return 0
|
|
446
|
+
computer_id = picked.get("id") or picked.get("slug")
|
|
447
|
+
if not computer_id:
|
|
448
|
+
return cli_output.report_error("computer is missing an id")
|
|
449
|
+
|
|
450
|
+
client = authenticated_client(config)
|
|
451
|
+
computer_id = resolve_computer_ref(client, str(computer_id))
|
|
452
|
+
if command:
|
|
453
|
+
return terminal.enter_computer(config, str(computer_id), command=command)
|
|
454
|
+
return terminal.enter_computer(config, str(computer_id))
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def handle_delete(
|
|
458
|
+
config: RuntimeConfig, computer_id: str | None, *, force: bool = False
|
|
459
|
+
) -> int:
|
|
460
|
+
if (err := cli_output.require_auth(config)) is not None:
|
|
461
|
+
return err
|
|
462
|
+
|
|
463
|
+
if computer_id is None:
|
|
464
|
+
if not cli_output.interactive():
|
|
465
|
+
return cli_output.report_error("computer id is required")
|
|
466
|
+
picked = pick_computer(config, "Pick a computer to delete")
|
|
467
|
+
if picked is None:
|
|
468
|
+
return 0
|
|
469
|
+
computer_id = picked.get("id") or picked.get("slug")
|
|
470
|
+
if not computer_id:
|
|
471
|
+
return cli_output.report_error("computer is missing an id")
|
|
472
|
+
|
|
473
|
+
# In TTY mode, require the user to retype the slug as confirmation
|
|
474
|
+
# unless --force was passed. Non-TTY callers stay script-friendly.
|
|
475
|
+
if cli_output.interactive() and not force:
|
|
476
|
+
if not cli_output.prompt_typed_confirm(
|
|
477
|
+
f"Delete {computer_id}? This permanently destroys filesystem, service, and public URL.",
|
|
478
|
+
str(computer_id),
|
|
479
|
+
):
|
|
480
|
+
print("cancelled")
|
|
481
|
+
return 0
|
|
482
|
+
|
|
483
|
+
client = authenticated_client(config)
|
|
484
|
+
display_ref = computer_id
|
|
485
|
+
computer_id = resolve_computer_ref(client, computer_id)
|
|
486
|
+
client.delete_computer(computer_id)
|
|
487
|
+
|
|
488
|
+
return cli_output.report_success({"message": f"computer {display_ref} deleted"})
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def handle_run(
|
|
492
|
+
config: RuntimeConfig,
|
|
493
|
+
computer_id: str | None,
|
|
494
|
+
command: str | None,
|
|
495
|
+
uid: int | None,
|
|
496
|
+
gid: int | None,
|
|
497
|
+
cwd: str | None,
|
|
498
|
+
shell: str | None,
|
|
499
|
+
) -> int:
|
|
500
|
+
if (err := cli_output.require_auth(config)) is not None:
|
|
501
|
+
return err
|
|
502
|
+
|
|
503
|
+
if computer_id is None:
|
|
504
|
+
if not cli_output.interactive():
|
|
505
|
+
return cli_output.report_error("computer id is required")
|
|
506
|
+
picked = pick_computer(
|
|
507
|
+
config,
|
|
508
|
+
"Pick a computer",
|
|
509
|
+
lambda c: computer_internal_status(c) == "running",
|
|
510
|
+
)
|
|
511
|
+
if picked is None:
|
|
512
|
+
return 0
|
|
513
|
+
computer_id = picked.get("id") or picked.get("slug")
|
|
514
|
+
if not computer_id:
|
|
515
|
+
return cli_output.report_error("computer is missing an id")
|
|
516
|
+
|
|
517
|
+
if command is None:
|
|
518
|
+
if not cli_output.interactive():
|
|
519
|
+
return cli_output.report_error("command is required")
|
|
520
|
+
command = cli_output.prompt_text("Command to run")
|
|
521
|
+
if not command:
|
|
522
|
+
return cli_output.report_error("command is required")
|
|
523
|
+
|
|
524
|
+
client = authenticated_client(config)
|
|
525
|
+
computer_id = resolve_computer_ref(client, computer_id)
|
|
526
|
+
result = client.run_command(
|
|
527
|
+
computer_id, command, uid=uid, gid=gid, cwd=cwd, shell=shell
|
|
528
|
+
)
|
|
529
|
+
return cli_output.report_success(result)
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def handle_publish(
|
|
533
|
+
config: RuntimeConfig,
|
|
534
|
+
computer_id: str | None,
|
|
535
|
+
port: int | None,
|
|
536
|
+
command_parts: list[str] | None = None,
|
|
537
|
+
) -> int:
|
|
538
|
+
if (err := cli_output.require_auth(config)) is not None:
|
|
539
|
+
return err
|
|
540
|
+
|
|
541
|
+
if computer_id is None:
|
|
542
|
+
if not cli_output.interactive():
|
|
543
|
+
return cli_output.report_error("computer id is required")
|
|
544
|
+
picked = pick_computer(
|
|
545
|
+
config,
|
|
546
|
+
"Pick a computer",
|
|
547
|
+
lambda c: computer_internal_status(c) == "running",
|
|
548
|
+
)
|
|
549
|
+
if picked is None:
|
|
550
|
+
return 0
|
|
551
|
+
computer_id = picked.get("id") or picked.get("slug")
|
|
552
|
+
if not computer_id:
|
|
553
|
+
return cli_output.report_error("computer is missing an id")
|
|
554
|
+
|
|
555
|
+
if port is None:
|
|
556
|
+
if not cli_output.interactive():
|
|
557
|
+
return cli_output.report_error("port is required")
|
|
558
|
+
port_text = cli_output.prompt_text("Port to publish", default="3000")
|
|
559
|
+
if not port_text:
|
|
560
|
+
return cli_output.report_error("port is required")
|
|
561
|
+
try:
|
|
562
|
+
port = int(port_text)
|
|
563
|
+
except ValueError:
|
|
564
|
+
return cli_output.report_error("port must be a number")
|
|
565
|
+
|
|
566
|
+
if port <= 0 or port > 65535:
|
|
567
|
+
return cli_output.report_error("port must be between 1 and 65535")
|
|
568
|
+
|
|
569
|
+
command = arguments.command_from_remainder(command_parts)
|
|
570
|
+
if not command:
|
|
571
|
+
return cli_output.report_error(
|
|
572
|
+
f"publish requires -- <command...>, for example: runtime publish {computer_id} {port} -- npm run dev -- --host 0.0.0.0"
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
client = authenticated_client(config)
|
|
576
|
+
computer_id = resolve_computer_ref(client, computer_id)
|
|
577
|
+
result = client.publish_port(computer_id, port, command=command)
|
|
578
|
+
return cli_output.report_success(result)
|