runtime-sdk 0.4.24__tar.gz → 0.4.26__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.24 → runtime_sdk-0.4.26}/PKG-INFO +8 -5
- {runtime_sdk-0.4.24 → runtime_sdk-0.4.26}/README.md +7 -4
- {runtime_sdk-0.4.24 → runtime_sdk-0.4.26}/pyproject.toml +1 -1
- {runtime_sdk-0.4.24 → runtime_sdk-0.4.26}/scripts/runtime_sdk/cli.py +211 -227
- {runtime_sdk-0.4.24 → runtime_sdk-0.4.26}/scripts/runtime_sdk/client.py +10 -29
- {runtime_sdk-0.4.24 → runtime_sdk-0.4.26}/scripts/runtime_sdk.egg-info/PKG-INFO +8 -5
- {runtime_sdk-0.4.24 → runtime_sdk-0.4.26}/scripts/runtime_sdk/__init__.py +0 -0
- {runtime_sdk-0.4.24 → runtime_sdk-0.4.26}/scripts/runtime_sdk/config.py +0 -0
- {runtime_sdk-0.4.24 → runtime_sdk-0.4.26}/scripts/runtime_sdk.egg-info/SOURCES.txt +0 -0
- {runtime_sdk-0.4.24 → runtime_sdk-0.4.26}/scripts/runtime_sdk.egg-info/dependency_links.txt +0 -0
- {runtime_sdk-0.4.24 → runtime_sdk-0.4.26}/scripts/runtime_sdk.egg-info/entry_points.txt +0 -0
- {runtime_sdk-0.4.24 → runtime_sdk-0.4.26}/scripts/runtime_sdk.egg-info/requires.txt +0 -0
- {runtime_sdk-0.4.24 → runtime_sdk-0.4.26}/scripts/runtime_sdk.egg-info/top_level.txt +0 -0
- {runtime_sdk-0.4.24 → runtime_sdk-0.4.26}/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.26
|
|
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
|
|
@@ -66,8 +66,9 @@ runtime login --api-key rt_live_...
|
|
|
66
66
|
runtime api-keys list
|
|
67
67
|
runtime api-keys create "my laptop"
|
|
68
68
|
runtime api-keys revoke 123
|
|
69
|
-
runtime
|
|
70
|
-
runtime
|
|
69
|
+
runtime integrate github
|
|
70
|
+
runtime github list
|
|
71
|
+
runtime github disconnect acme
|
|
71
72
|
|
|
72
73
|
# Computers
|
|
73
74
|
runtime switch --repo owner/repo feature/login # open or create the branch workspace computer
|
|
@@ -170,13 +171,15 @@ that ad-hoc process is not.
|
|
|
170
171
|
between parallel tasks without local worktrees.
|
|
171
172
|
|
|
172
173
|
```bash
|
|
173
|
-
runtime
|
|
174
|
+
runtime integrate github
|
|
175
|
+
runtime github list
|
|
174
176
|
runtime switch --repo owner/repo feature/login
|
|
175
177
|
runtime switch -c feature/search --repo owner/repo --from main
|
|
176
178
|
```
|
|
177
179
|
|
|
178
180
|
Rules:
|
|
179
|
-
- GitHub must already be connected with `runtime
|
|
181
|
+
- GitHub must already be connected with `runtime integrate github`.
|
|
182
|
+
- Run `runtime integrate github` again when you want to add another personal account or org.
|
|
180
183
|
- `runtime switch` opens an existing branch workspace, or creates the computer if
|
|
181
184
|
the branch exists but the workspace does not yet.
|
|
182
185
|
- `runtime switch -c` creates a new branch first, then opens that branch's
|
|
@@ -44,8 +44,9 @@ runtime login --api-key rt_live_...
|
|
|
44
44
|
runtime api-keys list
|
|
45
45
|
runtime api-keys create "my laptop"
|
|
46
46
|
runtime api-keys revoke 123
|
|
47
|
-
runtime
|
|
48
|
-
runtime
|
|
47
|
+
runtime integrate github
|
|
48
|
+
runtime github list
|
|
49
|
+
runtime github disconnect acme
|
|
49
50
|
|
|
50
51
|
# Computers
|
|
51
52
|
runtime switch --repo owner/repo feature/login # open or create the branch workspace computer
|
|
@@ -148,13 +149,15 @@ that ad-hoc process is not.
|
|
|
148
149
|
between parallel tasks without local worktrees.
|
|
149
150
|
|
|
150
151
|
```bash
|
|
151
|
-
runtime
|
|
152
|
+
runtime integrate github
|
|
153
|
+
runtime github list
|
|
152
154
|
runtime switch --repo owner/repo feature/login
|
|
153
155
|
runtime switch -c feature/search --repo owner/repo --from main
|
|
154
156
|
```
|
|
155
157
|
|
|
156
158
|
Rules:
|
|
157
|
-
- GitHub must already be connected with `runtime
|
|
159
|
+
- GitHub must already be connected with `runtime integrate github`.
|
|
160
|
+
- Run `runtime integrate github` again when you want to add another personal account or org.
|
|
158
161
|
- `runtime switch` opens an existing branch workspace, or creates the computer if
|
|
159
162
|
the branch exists but the workspace does not yet.
|
|
160
163
|
- `runtime switch -c` creates a new branch first, then opens that branch's
|
|
@@ -192,14 +192,16 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
192
192
|
secrets_delete = secrets_subparsers.add_parser("delete", aliases=["rm"], help="Delete a secret set")
|
|
193
193
|
secrets_delete.add_argument("slug", nargs="?", default=None, help="Secret set slug")
|
|
194
194
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
195
|
+
github_cmd = subparsers.add_parser("github", help="Manage connected GitHub App installations")
|
|
196
|
+
github_subparsers = github_cmd.add_subparsers(dest="github_action", required=True)
|
|
197
|
+
github_subparsers.add_parser("list", aliases=["ls"], help="List connected GitHub installations")
|
|
198
|
+
github_disconnect = github_subparsers.add_parser("disconnect", aliases=["rm"], help="Disconnect a GitHub installation from Runtime")
|
|
199
|
+
github_disconnect.add_argument("ref", nargs="?", default=None, help="GitHub owner or installation id")
|
|
200
|
+
|
|
201
|
+
integrate_cmd = subparsers.add_parser("integrate", help="Connect the RuntimeVM GitHub App")
|
|
202
|
+
integrate_cmd.add_argument("provider", choices=["github"])
|
|
203
|
+
integrate_cmd.add_argument("--no-open", action="store_true", help="Print the install URL instead of opening a browser")
|
|
204
|
+
integrate_cmd.add_argument("--timeout", type=int, default=180, help="Seconds to wait for the browser install to finish (default: 180)")
|
|
203
205
|
|
|
204
206
|
switch_cmd = subparsers.add_parser(
|
|
205
207
|
"switch",
|
|
@@ -376,8 +378,9 @@ _HELP_SECTIONS: tuple[tuple[str, tuple[tuple[str, str], ...]], ...] = (
|
|
|
376
378
|
"GitHub",
|
|
377
379
|
(
|
|
378
380
|
("switch, checkout [branch]", "Open a GitHub branch workspace in its own computer"),
|
|
379
|
-
("
|
|
380
|
-
("
|
|
381
|
+
("integrate github", "Install the RuntimeVM GitHub App and wait for completion"),
|
|
382
|
+
("github list", "List connected GitHub installations"),
|
|
383
|
+
("github disconnect, rm [owner|id]", "Disconnect one GitHub installation"),
|
|
381
384
|
),
|
|
382
385
|
),
|
|
383
386
|
(
|
|
@@ -407,6 +410,8 @@ _HELP_EXAMPLES: tuple[str, ...] = (
|
|
|
407
410
|
"runtime ls -w # live-updating list",
|
|
408
411
|
"runtime create mybox --command 'npm start' --cwd /app --port 3000",
|
|
409
412
|
"runtime share private mybox # require owner auth",
|
|
413
|
+
"runtime integrate github # open GitHub and wait for the install to finish",
|
|
414
|
+
"runtime github list # show connected GitHub orgs/accounts",
|
|
410
415
|
"runtime proxy start mybox 5432 # forward localhost:5432 → mybox:5432",
|
|
411
416
|
"runtime completion zsh > ~/.zsh/completions/_runtime",
|
|
412
417
|
)
|
|
@@ -506,12 +511,14 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
506
511
|
if args.secrets_action in ("delete", "rm"):
|
|
507
512
|
return handle_secrets_delete(config, args.slug)
|
|
508
513
|
return report_error("unknown secrets command")
|
|
509
|
-
if args.command == "
|
|
510
|
-
if args.
|
|
511
|
-
return
|
|
512
|
-
if args.
|
|
513
|
-
return
|
|
514
|
-
return report_error("unknown
|
|
514
|
+
if args.command == "github":
|
|
515
|
+
if args.github_action in ("list", "ls"):
|
|
516
|
+
return handle_github_list(config)
|
|
517
|
+
if args.github_action in ("disconnect", "rm"):
|
|
518
|
+
return handle_github_disconnect(config, args.ref)
|
|
519
|
+
return report_error("unknown github command")
|
|
520
|
+
if args.command == "integrate":
|
|
521
|
+
return handle_integrate_github(config, args.provider, no_open=args.no_open, timeout_seconds=args.timeout)
|
|
515
522
|
if args.command in ("switch", "checkout"):
|
|
516
523
|
return handle_switch(
|
|
517
524
|
config,
|
|
@@ -889,22 +896,6 @@ def _prompt_typed_confirm(message: str, expected: str) -> bool:
|
|
|
889
896
|
return typed.strip().lower() == expected.strip().lower()
|
|
890
897
|
|
|
891
898
|
|
|
892
|
-
def _display_width(value: Any, minimum: int, maximum: int) -> int:
|
|
893
|
-
text = str(value or "")
|
|
894
|
-
return max(minimum, min(len(text), maximum))
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
def _fit_cell(value: Any, width: int) -> str:
|
|
898
|
-
text = str(value or "")
|
|
899
|
-
if width <= 0:
|
|
900
|
-
return ""
|
|
901
|
-
if len(text) > width:
|
|
902
|
-
if width == 1:
|
|
903
|
-
return "…"
|
|
904
|
-
text = text[: width - 1] + "…"
|
|
905
|
-
return text.ljust(width)
|
|
906
|
-
|
|
907
|
-
|
|
908
899
|
def _computer_power_state(c: dict[str, Any]) -> str:
|
|
909
900
|
power = c.get("power_state")
|
|
910
901
|
if power is not None:
|
|
@@ -1123,157 +1114,97 @@ def _computer_sort_key(c: dict[str, Any]) -> tuple[int, str, str]:
|
|
|
1123
1114
|
|
|
1124
1115
|
|
|
1125
1116
|
|
|
1126
|
-
def
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
url_budget = max(16, term_width - reserved)
|
|
1139
|
-
url_width = max(
|
|
1140
|
-
len("url"),
|
|
1141
|
-
min(max(len(_computer_url_label(c)) for c in computers), url_budget),
|
|
1142
|
-
)
|
|
1143
|
-
return {
|
|
1144
|
-
"slug": slug_width,
|
|
1145
|
-
"status": status_width,
|
|
1146
|
-
"public_url": url_width,
|
|
1147
|
-
"created_at": created_width,
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
def _format_computer_row(c: dict[str, Any], widths: dict[str, int]) -> str:
|
|
1152
|
-
return " │ ".join(
|
|
1153
|
-
[
|
|
1154
|
-
_fit_cell(_computer_name(c), widths["slug"]),
|
|
1155
|
-
_fit_cell(_computer_status_label(c), widths["status"]),
|
|
1156
|
-
_fit_cell(_computer_url_label(c), widths["public_url"]),
|
|
1157
|
-
_fit_cell(_computer_created_label(c), widths["created_at"]),
|
|
1158
|
-
]
|
|
1159
|
-
)
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
def _computer_table_prompt(prompt: str, computers: list[dict[str, Any]]) -> tuple[str, dict[str, int]]:
|
|
1163
|
-
widths = _computer_table_layout(computers)
|
|
1164
|
-
header = " │ ".join(
|
|
1165
|
-
[
|
|
1166
|
-
_fit_cell("name", widths["slug"]),
|
|
1167
|
-
_fit_cell("state", widths["status"]),
|
|
1168
|
-
_fit_cell("url", widths["public_url"]),
|
|
1169
|
-
_fit_cell("age", widths["created_at"]),
|
|
1170
|
-
]
|
|
1171
|
-
)
|
|
1172
|
-
divider = "─" * len(header)
|
|
1173
|
-
return f"{prompt}\n{header}\n{divider}", widths
|
|
1117
|
+
def _picker_status_color(status: str) -> str:
|
|
1118
|
+
if "cold" in status:
|
|
1119
|
+
return "fg:#60a5fa" # cyan-blue
|
|
1120
|
+
if "warm" in status or "running" in status:
|
|
1121
|
+
return "fg:#22c55e" # green
|
|
1122
|
+
if "archiving" in status or "restoring" in status:
|
|
1123
|
+
return "fg:#eab308" # yellow
|
|
1124
|
+
if "creating" in status:
|
|
1125
|
+
return "fg:#a855f7" # magenta
|
|
1126
|
+
if "orphaned" in status:
|
|
1127
|
+
return "fg:#ef4444" # red
|
|
1128
|
+
return "fg:#e5e7eb"
|
|
1174
1129
|
|
|
1175
1130
|
|
|
1131
|
+
def _picker_widths(computers: list[dict[str, Any]]) -> dict[str, int]:
|
|
1132
|
+
"""Compute aligned column widths for the interactive picker rows.
|
|
1176
1133
|
|
|
1177
|
-
|
|
1178
|
-
|
|
1134
|
+
We budget total width to leave room for questionary's pointer + indicator.
|
|
1135
|
+
"""
|
|
1136
|
+
term_width = shutil.get_terminal_size((100, 20)).columns
|
|
1137
|
+
name_w = max(len("name"), max((len(_computer_name(c)) for c in computers), default=0))
|
|
1138
|
+
name_w = min(name_w, 24)
|
|
1139
|
+
status_w = max(len("state"), max((len(_computer_status_label(c)) for c in computers), default=0))
|
|
1140
|
+
status_w = min(status_w, 14)
|
|
1141
|
+
age_w = max(len("age"), max((len(_computer_created_label(c)) for c in computers), default=0))
|
|
1142
|
+
age_w = min(age_w, 6)
|
|
1143
|
+
# pointer + indicator + 2 gaps * 3 + status dot (2 chars) + padding
|
|
1144
|
+
reserved = 6 + name_w + status_w + age_w + 2 * 3 + 2
|
|
1145
|
+
url_w = max(len("url"), min(max((len(_computer_url_label(c)) for c in computers), default=16), term_width - reserved))
|
|
1146
|
+
url_w = max(10, url_w)
|
|
1147
|
+
return {"name": name_w, "status": status_w, "url": url_w, "age": age_w}
|
|
1148
|
+
|
|
1149
|
+
|
|
1150
|
+
def _computer_choice_title(c: dict[str, Any], widths: dict[str, int]) -> list[tuple[str, str]]:
|
|
1151
|
+
"""Return a styled list-of-tuples title so the picker rows look tabular.
|
|
1152
|
+
|
|
1153
|
+
Columns: name · state (colored dot + label) · url · age.
|
|
1154
|
+
"""
|
|
1155
|
+
name = _computer_name(c)
|
|
1179
1156
|
status = _computer_status_label(c)
|
|
1180
|
-
url = _computer_url_label(c)
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
body = Text()
|
|
1200
|
-
body.append(f"total {len(computers)}", style="bold white")
|
|
1201
|
-
if counts:
|
|
1202
|
-
body.append(" ")
|
|
1203
|
-
first = True
|
|
1204
|
-
status_styles = {
|
|
1205
|
-
"warm": "bold green",
|
|
1206
|
-
"cold": "bold cyan",
|
|
1207
|
-
"running": "bold green",
|
|
1208
|
-
"archiving": "bold yellow",
|
|
1209
|
-
"restoring": "bold yellow",
|
|
1210
|
-
"creating": "bold magenta",
|
|
1211
|
-
"orphaned": "bold red",
|
|
1212
|
-
}
|
|
1213
|
-
for status in sorted(counts):
|
|
1214
|
-
if not first:
|
|
1215
|
-
body.append(" • ", style="dim")
|
|
1216
|
-
first = False
|
|
1217
|
-
status_style = "bold white"
|
|
1218
|
-
for token, style in status_styles.items():
|
|
1219
|
-
if token in status:
|
|
1220
|
-
status_style = style
|
|
1221
|
-
break
|
|
1222
|
-
body.append(status, style=status_style)
|
|
1223
|
-
body.append(f" {counts[status]}", style="white")
|
|
1224
|
-
|
|
1225
|
-
_UI.console().print(
|
|
1226
|
-
Panel(body, title="runtime", border_style="blue", expand=False, padding=(0, 1))
|
|
1227
|
-
)
|
|
1228
|
-
|
|
1157
|
+
url = _computer_url_label(c) or "—"
|
|
1158
|
+
age = _computer_created_label(c) or ""
|
|
1159
|
+
color = _picker_status_color(status)
|
|
1160
|
+
dot = _computer_status_dot(status)
|
|
1161
|
+
# Keep each column within its budget; truncate with ellipsis on overflow.
|
|
1162
|
+
def fit(text: str, width: int, align: str = "<") -> str:
|
|
1163
|
+
text = str(text or "")
|
|
1164
|
+
if len(text) > width:
|
|
1165
|
+
return text[: max(1, width - 1)] + "…"
|
|
1166
|
+
return format(text, f"{align}{width}")
|
|
1167
|
+
return [
|
|
1168
|
+
("", fit(name, widths["name"]) + " "),
|
|
1169
|
+
(color, f"{dot} "),
|
|
1170
|
+
(color, fit(status, widths["status"])),
|
|
1171
|
+
("", " "),
|
|
1172
|
+
("fg:#cbd5e1", fit(url, widths["url"])),
|
|
1173
|
+
("", " "),
|
|
1174
|
+
("fg:#6b7280", fit(age, widths["age"], ">")),
|
|
1175
|
+
]
|
|
1229
1176
|
|
|
1230
1177
|
|
|
1231
|
-
def _render_computer_table(computers: list[dict[str, Any]], *, title: str | None = None) -> None:
|
|
1232
|
-
mods = _UI.rich()
|
|
1233
|
-
Table, box = mods["Table"], mods["box"]
|
|
1234
|
-
table = Table(
|
|
1235
|
-
title=title,
|
|
1236
|
-
box=box.MINIMAL_DOUBLE_HEAD,
|
|
1237
|
-
expand=True,
|
|
1238
|
-
show_lines=False,
|
|
1239
|
-
header_style="bold #7dd3fc",
|
|
1240
|
-
title_style="bold white",
|
|
1241
|
-
pad_edge=False,
|
|
1242
|
-
collapse_padding=True,
|
|
1243
|
-
row_styles=["", "dim"],
|
|
1244
|
-
)
|
|
1245
|
-
table.add_column("Name", style="bold white", no_wrap=True, ratio=2)
|
|
1246
|
-
table.add_column("State", no_wrap=True, ratio=2)
|
|
1247
|
-
table.add_column("URL", style="white", overflow="fold", ratio=4)
|
|
1248
|
-
table.add_column("Age", style="dim", no_wrap=True, justify="right", ratio=1)
|
|
1249
|
-
|
|
1250
|
-
for computer in sorted(computers, key=_computer_sort_key):
|
|
1251
|
-
status = _computer_status_label(computer)
|
|
1252
|
-
status_style = _computer_status_style(status)
|
|
1253
|
-
table.add_row(
|
|
1254
|
-
_computer_name(computer),
|
|
1255
|
-
f"[{status_style}]{_computer_status_dot(status)} {status}[/{status_style}]",
|
|
1256
|
-
_computer_url_label(computer),
|
|
1257
|
-
_computer_created_label(computer),
|
|
1258
|
-
)
|
|
1259
1178
|
|
|
1260
|
-
_UI.console().print(table)
|
|
1261
1179
|
|
|
1262
1180
|
|
|
1263
1181
|
def _select_prompt(message: str, choices: list[Any], *, instruction: str | None = None) -> Any:
|
|
1264
1182
|
# questionary rejects use_jk_keys together with use_search_filter because
|
|
1265
1183
|
# j/k would be swallowed by the search prefix. We pick search over vim keys.
|
|
1266
|
-
|
|
1184
|
+
question = _UI.q().select(
|
|
1267
1185
|
message,
|
|
1268
1186
|
choices=choices,
|
|
1269
1187
|
qmark="●",
|
|
1270
1188
|
pointer="❯",
|
|
1271
|
-
instruction=instruction or "↑↓
|
|
1189
|
+
instruction=instruction or "↑↓ · type to search · enter select · esc back",
|
|
1272
1190
|
use_indicator=True,
|
|
1273
1191
|
use_jk_keys=False,
|
|
1274
1192
|
use_search_filter=True,
|
|
1275
1193
|
style=_UI.style(),
|
|
1276
1194
|
)
|
|
1195
|
+
# Bind ESC explicitly — questionary's select prompt does not bind it by
|
|
1196
|
+
# default. With eager=True the binding fires before prompt_toolkit's
|
|
1197
|
+
# meta-key timeout. .unsafe_ask() returns the `result` we pass to exit().
|
|
1198
|
+
with contextlib.suppress(Exception):
|
|
1199
|
+
from prompt_toolkit.keys import Keys
|
|
1200
|
+
|
|
1201
|
+
bindings = question.application.key_bindings
|
|
1202
|
+
|
|
1203
|
+
@bindings.add(Keys.Escape, eager=True)
|
|
1204
|
+
def _escape(event: Any) -> None:
|
|
1205
|
+
event.app.exit(result=None)
|
|
1206
|
+
|
|
1207
|
+
return question
|
|
1277
1208
|
|
|
1278
1209
|
|
|
1279
1210
|
def _pick_computer(
|
|
@@ -1283,8 +1214,8 @@ def _pick_computer(
|
|
|
1283
1214
|
) -> dict[str, Any] | None:
|
|
1284
1215
|
"""Fetch computers and let the user arrow-key through them.
|
|
1285
1216
|
|
|
1286
|
-
Returns the picked computer dict, or None
|
|
1287
|
-
|
|
1217
|
+
Returns the picked computer dict, or None on ESC / "← cancel".
|
|
1218
|
+
Ctrl-C propagates as KeyboardInterrupt.
|
|
1288
1219
|
"""
|
|
1289
1220
|
client = RuntimeClient(base_url=config.base_url, api_key=config.api_key)
|
|
1290
1221
|
payload = client.list_computers()
|
|
@@ -1299,8 +1230,7 @@ def _pick_computer(
|
|
|
1299
1230
|
return None
|
|
1300
1231
|
|
|
1301
1232
|
questionary = _UI.q()
|
|
1302
|
-
widths =
|
|
1303
|
-
_render_computer_table(computers)
|
|
1233
|
+
widths = _picker_widths(computers)
|
|
1304
1234
|
choices = [
|
|
1305
1235
|
questionary.Choice(title=_computer_choice_title(c, widths), value=c) for c in computers
|
|
1306
1236
|
]
|
|
@@ -1770,30 +1700,31 @@ def handle_api_keys_revoke(config: RuntimeConfig, key_id: str | None) -> int:
|
|
|
1770
1700
|
|
|
1771
1701
|
|
|
1772
1702
|
|
|
1773
|
-
def
|
|
1703
|
+
def _render_github_installations_table(payload: dict[str, Any]) -> None:
|
|
1774
1704
|
mods = _UI.rich()
|
|
1775
1705
|
Table = mods["Table"]
|
|
1776
|
-
|
|
1777
|
-
if not isinstance(
|
|
1778
|
-
_UI.console().print("[yellow]no
|
|
1706
|
+
installations = payload.get("installations") or []
|
|
1707
|
+
if not isinstance(installations, list) or not installations:
|
|
1708
|
+
_UI.console().print("[yellow]no GitHub installations found[/yellow]")
|
|
1779
1709
|
return
|
|
1780
1710
|
|
|
1781
|
-
table = Table(title="
|
|
1782
|
-
table.add_column("
|
|
1783
|
-
table.add_column("Provider", style="white")
|
|
1711
|
+
table = Table(title="GitHub", expand=True, box=mods["box"].SIMPLE_HEAVY)
|
|
1712
|
+
table.add_column("ID", style="bold white")
|
|
1784
1713
|
table.add_column("Owner", style="white")
|
|
1785
|
-
table.add_column("
|
|
1786
|
-
table.add_column("
|
|
1714
|
+
table.add_column("Type", style="white")
|
|
1715
|
+
table.add_column("Repos", style="white")
|
|
1716
|
+
table.add_column("Status", style="white")
|
|
1787
1717
|
|
|
1788
|
-
for entry in
|
|
1718
|
+
for entry in installations:
|
|
1789
1719
|
if not isinstance(entry, dict):
|
|
1790
1720
|
continue
|
|
1721
|
+
status = "suspended" if entry.get("suspended") else "connected"
|
|
1791
1722
|
table.add_row(
|
|
1792
|
-
str(entry.get("
|
|
1793
|
-
str(entry.get("
|
|
1794
|
-
str(entry.get("
|
|
1795
|
-
str(entry.get("
|
|
1796
|
-
|
|
1723
|
+
str(entry.get("id") or "—"),
|
|
1724
|
+
str(entry.get("github_account_login") or "—"),
|
|
1725
|
+
str(entry.get("github_account_type") or "—"),
|
|
1726
|
+
str(entry.get("repository_selection") or "—"),
|
|
1727
|
+
status,
|
|
1797
1728
|
)
|
|
1798
1729
|
_UI.console().print(table)
|
|
1799
1730
|
|
|
@@ -1931,21 +1862,68 @@ def handle_secrets_delete(config: RuntimeConfig, slug: str | None) -> int:
|
|
|
1931
1862
|
|
|
1932
1863
|
|
|
1933
1864
|
|
|
1934
|
-
def
|
|
1865
|
+
def handle_github_list(config: RuntimeConfig) -> int:
|
|
1935
1866
|
if (err := _require_api_key(config)) is not None:
|
|
1936
1867
|
return err
|
|
1937
1868
|
client = RuntimeClient(base_url=config.base_url, api_key=config.api_key)
|
|
1938
|
-
return report_success(client.
|
|
1869
|
+
return report_success(client.list_github_installations(), _render_github_installations_table)
|
|
1870
|
+
|
|
1939
1871
|
|
|
1940
1872
|
|
|
1873
|
+
def handle_github_disconnect(config: RuntimeConfig, ref: str | None) -> int:
|
|
1874
|
+
if (err := _require_api_key(config)) is not None:
|
|
1875
|
+
return err
|
|
1876
|
+
resolved_ref = str(ref or "").strip()
|
|
1877
|
+
if not resolved_ref:
|
|
1878
|
+
if not _interactive():
|
|
1879
|
+
return report_error("github owner or installation id is required")
|
|
1880
|
+
resolved_ref = _prompt_text("GitHub owner or installation id")
|
|
1881
|
+
if not resolved_ref:
|
|
1882
|
+
return report_error("github owner or installation id is required")
|
|
1883
|
+
|
|
1884
|
+
client = RuntimeClient(base_url=config.base_url, api_key=config.api_key)
|
|
1885
|
+
installation_id: int | None = None
|
|
1886
|
+
owner = resolved_ref
|
|
1941
1887
|
|
|
1942
|
-
|
|
1888
|
+
try:
|
|
1889
|
+
installation_id = int(resolved_ref)
|
|
1890
|
+
except ValueError:
|
|
1891
|
+
payload = client.list_github_installations()
|
|
1892
|
+
installations = payload.get("installations") or []
|
|
1893
|
+
target_owner = resolved_ref.casefold()
|
|
1894
|
+
match = next(
|
|
1895
|
+
(
|
|
1896
|
+
entry
|
|
1897
|
+
for entry in installations
|
|
1898
|
+
if isinstance(entry, dict) and str(entry.get("github_account_login") or "").strip().casefold() == target_owner
|
|
1899
|
+
),
|
|
1900
|
+
None,
|
|
1901
|
+
)
|
|
1902
|
+
if match is None:
|
|
1903
|
+
return report_error(f"github installation not found: {resolved_ref}")
|
|
1904
|
+
try:
|
|
1905
|
+
installation_id = int(match.get("id"))
|
|
1906
|
+
except (TypeError, ValueError):
|
|
1907
|
+
return report_error(f"github installation is missing an id: {resolved_ref}")
|
|
1908
|
+
owner = str(match.get("github_account_login") or resolved_ref)
|
|
1909
|
+
|
|
1910
|
+
if installation_id is None or installation_id <= 0:
|
|
1911
|
+
return report_error("installation id must be a positive number")
|
|
1912
|
+
|
|
1913
|
+
client.disconnect_github_installation(installation_id)
|
|
1914
|
+
return report_success(
|
|
1915
|
+
{"id": installation_id, "owner": owner, "deleted": True},
|
|
1916
|
+
lambda payload: _UI.console().print(f"[green]✓[/green] disconnected [bold]{payload['owner']}[/bold]"),
|
|
1917
|
+
)
|
|
1918
|
+
|
|
1919
|
+
|
|
1920
|
+
|
|
1921
|
+
def handle_integrate_github(
|
|
1943
1922
|
config: RuntimeConfig,
|
|
1944
1923
|
provider: str,
|
|
1945
|
-
slug: str | None,
|
|
1946
|
-
owner: str | None,
|
|
1947
1924
|
*,
|
|
1948
1925
|
no_open: bool = False,
|
|
1926
|
+
timeout_seconds: int = 180,
|
|
1949
1927
|
) -> int:
|
|
1950
1928
|
if (err := _require_api_key(config)) is not None:
|
|
1951
1929
|
return err
|
|
@@ -1953,11 +1931,11 @@ def handle_integrations_connect(
|
|
|
1953
1931
|
return report_error(f"unsupported integration provider: {provider}")
|
|
1954
1932
|
|
|
1955
1933
|
client = RuntimeClient(base_url=config.base_url, api_key=config.api_key)
|
|
1956
|
-
payload = _with_spinner("getting GitHub install link…",
|
|
1934
|
+
payload = _with_spinner("getting GitHub install link…", client.start_github_connect)
|
|
1957
1935
|
install_url = str(payload.get("install_url") or "").strip()
|
|
1958
|
-
|
|
1959
|
-
if not install_url:
|
|
1960
|
-
return report_error("server did not return a GitHub install
|
|
1936
|
+
session_id = int(payload.get("session_id") or 0)
|
|
1937
|
+
if not install_url or session_id <= 0:
|
|
1938
|
+
return report_error("server did not return a GitHub install session")
|
|
1961
1939
|
|
|
1962
1940
|
opened = False
|
|
1963
1941
|
if not no_open:
|
|
@@ -1967,37 +1945,26 @@ def handle_integrations_connect(
|
|
|
1967
1945
|
if _interactive():
|
|
1968
1946
|
action = "opened in your browser" if opened else "copy this URL into your browser"
|
|
1969
1947
|
_UI.console().print(f"[cyan]→[/cyan] {action}: [link={install_url}]{install_url}[/link]")
|
|
1970
|
-
_UI.console().print("[dim]
|
|
1971
|
-
input("Press Enter after the GitHub App is installed... ")
|
|
1972
|
-
if not owner:
|
|
1973
|
-
owner = _prompt_text("GitHub owner (username or org)")
|
|
1974
|
-
elif not owner:
|
|
1975
|
-
return report_error("--owner is required without a TTY")
|
|
1976
|
-
|
|
1977
|
-
resolved_owner = str(owner or "").strip()
|
|
1978
|
-
if not resolved_owner:
|
|
1979
|
-
return report_error("GitHub owner is required")
|
|
1980
|
-
|
|
1981
|
-
integration = _with_spinner(
|
|
1982
|
-
"connecting GitHub…",
|
|
1983
|
-
lambda: client.create_integration(
|
|
1984
|
-
slug=resolved_slug,
|
|
1985
|
-
provider="github",
|
|
1986
|
-
transport="scoped_credential",
|
|
1987
|
-
backend="native",
|
|
1988
|
-
credential_source="external_account",
|
|
1989
|
-
credential_ref=resolved_owner,
|
|
1990
|
-
config={},
|
|
1991
|
-
),
|
|
1992
|
-
)
|
|
1948
|
+
_UI.console().print("[dim]Choose the GitHub account/org and repo access in GitHub. Waiting for completion…[/dim]")
|
|
1993
1949
|
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
)
|
|
1950
|
+
deadline = time.time() + max(1, int(timeout_seconds))
|
|
1951
|
+
last_status = "pending"
|
|
1952
|
+
while time.time() < deadline:
|
|
1953
|
+
session = client.get_github_connect_session(session_id)
|
|
1954
|
+
status = str(session.get("status") or "pending")
|
|
1955
|
+
last_status = status
|
|
1956
|
+
if status == "completed":
|
|
1957
|
+
installation = session.get("installation") or {}
|
|
1958
|
+
owner = str(installation.get("github_account_login") or "github")
|
|
1959
|
+
return report_success(
|
|
1960
|
+
{"installation": installation, "install_url": install_url, "session_id": session_id},
|
|
1961
|
+
lambda result: _UI.console().print(f"[green]✓[/green] connected [bold]{(result.get('installation') or {}).get('github_account_login', owner)}[/bold]"),
|
|
1962
|
+
)
|
|
1963
|
+
if status in {"failed", "expired"}:
|
|
1964
|
+
return report_error(str(session.get("error") or f"github connect {status}"))
|
|
1965
|
+
time.sleep(2)
|
|
1999
1966
|
|
|
2000
|
-
return
|
|
1967
|
+
return report_error(f"timed out waiting for GitHub install to finish (last status: {last_status})")
|
|
2001
1968
|
|
|
2002
1969
|
|
|
2003
1970
|
|
|
@@ -2977,7 +2944,11 @@ def _send_terminal_resize(ws: Any) -> None:
|
|
|
2977
2944
|
|
|
2978
2945
|
|
|
2979
2946
|
def _interactive_list(config: RuntimeConfig) -> int:
|
|
2980
|
-
"""
|
|
2947
|
+
"""Interactive top-level menu: picker is the display, no separate table.
|
|
2948
|
+
|
|
2949
|
+
ESC / Ctrl+C / "✕ quit" exit. "+ new computer" creates one. "↻ refresh"
|
|
2950
|
+
re-fetches. Picking a VM drops into its action submenu.
|
|
2951
|
+
"""
|
|
2981
2952
|
client = RuntimeClient(base_url=config.base_url, api_key=config.api_key)
|
|
2982
2953
|
questionary = _UI.q()
|
|
2983
2954
|
|
|
@@ -2992,9 +2963,19 @@ def _interactive_list(config: RuntimeConfig) -> int:
|
|
|
2992
2963
|
return handle_create(config, None, None, None, None)
|
|
2993
2964
|
|
|
2994
2965
|
computers = sorted(computers, key=_computer_sort_key)
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2966
|
+
widths = _picker_widths(computers)
|
|
2967
|
+
|
|
2968
|
+
# Title summarises counts so the picker alone carries enough context.
|
|
2969
|
+
total = len(computers)
|
|
2970
|
+
warm = sum(1 for c in computers if "warm" in _computer_status_label(c) or "running" in _computer_status_label(c))
|
|
2971
|
+
cold = sum(1 for c in computers if "cold" in _computer_status_label(c))
|
|
2972
|
+
bits = [f"{total} total"]
|
|
2973
|
+
if warm:
|
|
2974
|
+
bits.append(f"{warm} warm")
|
|
2975
|
+
if cold:
|
|
2976
|
+
bits.append(f"{cold} cold")
|
|
2977
|
+
summary = " · ".join(bits)
|
|
2978
|
+
|
|
2998
2979
|
choices: list[Any] = [
|
|
2999
2980
|
questionary.Choice(title=_computer_choice_title(c, widths), value=c)
|
|
3000
2981
|
for c in computers
|
|
@@ -3004,9 +2985,9 @@ def _interactive_list(config: RuntimeConfig) -> int:
|
|
|
3004
2985
|
choices.append(questionary.Choice(title="↻ refresh", value="__refresh__"))
|
|
3005
2986
|
choices.append(questionary.Choice(title="✕ quit", value="__quit__"))
|
|
3006
2987
|
|
|
3007
|
-
picked = _select_prompt("Pick a computer", choices).unsafe_ask()
|
|
2988
|
+
picked = _select_prompt(f"Pick a computer ({summary})", choices).unsafe_ask()
|
|
3008
2989
|
|
|
3009
|
-
if picked == "__quit__":
|
|
2990
|
+
if picked is None or picked == "__quit__":
|
|
3010
2991
|
return 0
|
|
3011
2992
|
if picked == "__refresh__":
|
|
3012
2993
|
continue
|
|
@@ -3270,7 +3251,7 @@ _runtime_complete() {
|
|
|
3270
3251
|
COMPREPLY=()
|
|
3271
3252
|
cur="${COMP_WORDS[COMP_CWORD]}"
|
|
3272
3253
|
if [[ ${COMP_CWORD} -eq 1 ]]; then
|
|
3273
|
-
COMPREPLY=( $(compgen -W "create list ls info url start enter run publish delete rm share startup service proxy switch checkout
|
|
3254
|
+
COMPREPLY=( $(compgen -W "create list ls info url start enter run publish delete rm share startup service proxy switch checkout github integrate secrets api-keys signup verify login whoami logout completion help" -- "$cur") )
|
|
3274
3255
|
return
|
|
3275
3256
|
fi
|
|
3276
3257
|
sub="${COMP_WORDS[1]}"
|
|
@@ -3319,7 +3300,8 @@ _runtime() {
|
|
|
3319
3300
|
'proxy:Port proxies'
|
|
3320
3301
|
'switch:Open a GitHub branch workspace'
|
|
3321
3302
|
'checkout:Open a GitHub branch workspace'
|
|
3322
|
-
'
|
|
3303
|
+
'github:GitHub connections'
|
|
3304
|
+
'integrate:Connect GitHub'
|
|
3323
3305
|
'secrets:Secret sets'
|
|
3324
3306
|
'api-keys:API keys'
|
|
3325
3307
|
'signup:Send verification code'
|
|
@@ -3384,6 +3366,8 @@ complete -c runtime -n '__fish_use_subcommand' -a share -d 'Set visibility'
|
|
|
3384
3366
|
complete -c runtime -n '__fish_use_subcommand' -a startup -d 'Startup config'
|
|
3385
3367
|
complete -c runtime -n '__fish_use_subcommand' -a service -d 'Service config'
|
|
3386
3368
|
complete -c runtime -n '__fish_use_subcommand' -a proxy -d 'Port proxies'
|
|
3369
|
+
complete -c runtime -n '__fish_use_subcommand' -a github -d 'GitHub connections'
|
|
3370
|
+
complete -c runtime -n '__fish_use_subcommand' -a integrate -d 'Connect GitHub'
|
|
3387
3371
|
complete -c runtime -n '__fish_use_subcommand' -a help -d 'Extended help'
|
|
3388
3372
|
complete -c runtime -n '__fish_use_subcommand' -a completion -d 'Completion'
|
|
3389
3373
|
|
|
@@ -52,38 +52,19 @@ class RuntimeClient:
|
|
|
52
52
|
def revoke_api_key(self, key_id: int) -> dict[str, Any]:
|
|
53
53
|
return self._request("POST", f"/api/api-keys/{key_id}/revoke", auth_required=True)
|
|
54
54
|
|
|
55
|
-
# ---
|
|
55
|
+
# --- GitHub App ---
|
|
56
56
|
|
|
57
|
-
def
|
|
58
|
-
return self._request("
|
|
57
|
+
def start_github_connect(self) -> dict[str, Any]:
|
|
58
|
+
return self._request("POST", "/api/github/connect", auth_required=True)
|
|
59
59
|
|
|
60
|
-
def
|
|
61
|
-
|
|
62
|
-
if slug:
|
|
63
|
-
body["slug"] = slug
|
|
64
|
-
return self._request("POST", "/api/integrations/github/connect", json=body, auth_required=True)
|
|
60
|
+
def get_github_connect_session(self, session_id: int) -> dict[str, Any]:
|
|
61
|
+
return self._request("GET", f"/api/github/connect/{session_id}", auth_required=True)
|
|
65
62
|
|
|
66
|
-
def
|
|
67
|
-
self,
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
transport: str,
|
|
72
|
-
backend: str,
|
|
73
|
-
credential_source: str,
|
|
74
|
-
credential_ref: str,
|
|
75
|
-
config: dict[str, Any] | None = None,
|
|
76
|
-
) -> dict[str, Any]:
|
|
77
|
-
body: dict[str, Any] = {
|
|
78
|
-
"slug": slug,
|
|
79
|
-
"provider": provider,
|
|
80
|
-
"transport": transport,
|
|
81
|
-
"backend": backend,
|
|
82
|
-
"credential_source": credential_source,
|
|
83
|
-
"credential_ref": credential_ref,
|
|
84
|
-
"config": config or {},
|
|
85
|
-
}
|
|
86
|
-
return self._request("POST", "/api/integrations", json=body, auth_required=True)
|
|
63
|
+
def list_github_installations(self) -> dict[str, Any]:
|
|
64
|
+
return self._request("GET", "/api/github/installations", auth_required=True)
|
|
65
|
+
|
|
66
|
+
def disconnect_github_installation(self, installation_id: int) -> dict[str, Any]:
|
|
67
|
+
return self._request("DELETE", f"/api/github/installations/{installation_id}", auth_required=True)
|
|
87
68
|
|
|
88
69
|
def list_secret_sets(self) -> dict[str, Any]:
|
|
89
70
|
return self._request("GET", "/api/secrets", auth_required=True)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: runtime-sdk
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.26
|
|
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
|
|
@@ -66,8 +66,9 @@ runtime login --api-key rt_live_...
|
|
|
66
66
|
runtime api-keys list
|
|
67
67
|
runtime api-keys create "my laptop"
|
|
68
68
|
runtime api-keys revoke 123
|
|
69
|
-
runtime
|
|
70
|
-
runtime
|
|
69
|
+
runtime integrate github
|
|
70
|
+
runtime github list
|
|
71
|
+
runtime github disconnect acme
|
|
71
72
|
|
|
72
73
|
# Computers
|
|
73
74
|
runtime switch --repo owner/repo feature/login # open or create the branch workspace computer
|
|
@@ -170,13 +171,15 @@ that ad-hoc process is not.
|
|
|
170
171
|
between parallel tasks without local worktrees.
|
|
171
172
|
|
|
172
173
|
```bash
|
|
173
|
-
runtime
|
|
174
|
+
runtime integrate github
|
|
175
|
+
runtime github list
|
|
174
176
|
runtime switch --repo owner/repo feature/login
|
|
175
177
|
runtime switch -c feature/search --repo owner/repo --from main
|
|
176
178
|
```
|
|
177
179
|
|
|
178
180
|
Rules:
|
|
179
|
-
- GitHub must already be connected with `runtime
|
|
181
|
+
- GitHub must already be connected with `runtime integrate github`.
|
|
182
|
+
- Run `runtime integrate github` again when you want to add another personal account or org.
|
|
180
183
|
- `runtime switch` opens an existing branch workspace, or creates the computer if
|
|
181
184
|
the branch exists but the workspace does not yet.
|
|
182
185
|
- `runtime switch -c` creates a new branch first, then opens that branch's
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|