runtime-sdk 0.4.18__tar.gz → 0.4.20__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: runtime-sdk
3
- Version: 0.4.18
3
+ Version: 0.4.20
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,13 @@ 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 integrations list
70
+ runtime integrations connect github
69
71
 
70
72
  # Computers
73
+ runtime switch --repo owner/repo feature/login # open or create the branch workspace computer
74
+ runtime switch -c feature/login --repo owner/repo # create a new branch workspace from the repo default branch
75
+ runtime checkout --repo owner/repo feature/login # alias for switch
71
76
  runtime create # creates a computer with the starter app already published
72
77
  runtime create myapp --command "python3 app.py" --cwd /home/ubuntu --port 3000
73
78
  runtime enter <name-or-id> # accepts slug/name like test, or the computer id
@@ -157,6 +162,28 @@ show|clear` expose that same durable app state directly. The low-level
157
162
  `runtime run` stays one-shot: the filesystem is restored after going cold, but
158
163
  that ad-hoc process is not.
159
164
 
165
+ ## GitHub branch workspaces
166
+
167
+ `runtime switch` gives a repo branch its own Runtime computer so you can jump
168
+ between parallel tasks without local worktrees.
169
+
170
+ ```bash
171
+ runtime integrations connect github
172
+ runtime switch --repo owner/repo feature/login
173
+ runtime switch -c feature/search --repo owner/repo --from main
174
+ ```
175
+
176
+ Rules:
177
+ - GitHub must already be connected with `runtime integrations connect github`.
178
+ - `runtime switch` opens an existing branch workspace, or creates the computer if
179
+ the branch exists but the workspace does not yet.
180
+ - `runtime switch -c` creates a new branch first, then opens that branch's
181
+ workspace.
182
+ - Each `repo + branch` maps to one dedicated computer.
183
+ - The repo is cloned into `/home/ubuntu/<repo>` and new shells in that computer
184
+ start there automatically.
185
+ - `runtime checkout ...` is an alias for `runtime switch ...`.
186
+
160
187
  Run the SDK unit tests through the backend project environment:
161
188
 
162
189
  ```bash
@@ -44,8 +44,13 @@ 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 integrations list
48
+ runtime integrations connect github
47
49
 
48
50
  # Computers
51
+ runtime switch --repo owner/repo feature/login # open or create the branch workspace computer
52
+ runtime switch -c feature/login --repo owner/repo # create a new branch workspace from the repo default branch
53
+ runtime checkout --repo owner/repo feature/login # alias for switch
49
54
  runtime create # creates a computer with the starter app already published
50
55
  runtime create myapp --command "python3 app.py" --cwd /home/ubuntu --port 3000
51
56
  runtime enter <name-or-id> # accepts slug/name like test, or the computer id
@@ -135,6 +140,28 @@ show|clear` expose that same durable app state directly. The low-level
135
140
  `runtime run` stays one-shot: the filesystem is restored after going cold, but
136
141
  that ad-hoc process is not.
137
142
 
143
+ ## GitHub branch workspaces
144
+
145
+ `runtime switch` gives a repo branch its own Runtime computer so you can jump
146
+ between parallel tasks without local worktrees.
147
+
148
+ ```bash
149
+ runtime integrations connect github
150
+ runtime switch --repo owner/repo feature/login
151
+ runtime switch -c feature/search --repo owner/repo --from main
152
+ ```
153
+
154
+ Rules:
155
+ - GitHub must already be connected with `runtime integrations connect github`.
156
+ - `runtime switch` opens an existing branch workspace, or creates the computer if
157
+ the branch exists but the workspace does not yet.
158
+ - `runtime switch -c` creates a new branch first, then opens that branch's
159
+ workspace.
160
+ - Each `repo + branch` maps to one dedicated computer.
161
+ - The repo is cloned into `/home/ubuntu/<repo>` and new shells in that computer
162
+ start there automatically.
163
+ - `runtime checkout ...` is an alias for `runtime switch ...`.
164
+
138
165
  Run the SDK unit tests through the backend project environment:
139
166
 
140
167
  ```bash
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "runtime-sdk"
3
- version = "0.4.18"
3
+ version = "0.4.20"
4
4
  description = "Runtime Python SDK and CLI"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -5,6 +5,7 @@ import contextlib
5
5
  import importlib
6
6
  import json
7
7
  import os
8
+ import re
8
9
  import select
9
10
  import shlex
10
11
  import shutil
@@ -189,12 +190,23 @@ def build_parser() -> argparse.ArgumentParser:
189
190
 
190
191
  integrations_cmd = subparsers.add_parser("integrations", help="Connect account-level integrations")
191
192
  integrations_subparsers = integrations_cmd.add_subparsers(dest="integrations_action", required=True)
193
+ integrations_subparsers.add_parser("list", aliases=["ls"], help="List integrations")
192
194
  integrations_connect = integrations_subparsers.add_parser("connect", help="Connect an integration")
193
195
  integrations_connect.add_argument("provider", choices=["github"])
194
196
  integrations_connect.add_argument("--slug", default="github", help="Integration slug to create (default: github)")
195
197
  integrations_connect.add_argument("--owner", default=None, help="GitHub username or org where the app is installed")
196
198
  integrations_connect.add_argument("--no-open", action="store_true", help="Print the install URL instead of opening a browser")
197
199
 
200
+ switch_cmd = subparsers.add_parser(
201
+ "switch",
202
+ aliases=["checkout"],
203
+ help="Open a GitHub branch workspace in its own computer",
204
+ )
205
+ switch_cmd.add_argument("branch", nargs="?", default=None, help="Git branch to open")
206
+ switch_cmd.add_argument("--repo", default=None, help="GitHub repository in owner/name format")
207
+ switch_cmd.add_argument("-c", "--create", action="store_true", help="Create a new branch before opening it")
208
+ switch_cmd.add_argument("--from", dest="base_branch", default=None, help="Base branch for -c (defaults to the repo default branch)")
209
+
198
210
  create_cmd = subparsers.add_parser("create", help="Create a new computer with the starter app published by default")
199
211
  create_cmd.add_argument(
200
212
  "name",
@@ -340,9 +352,19 @@ def main(argv: list[str] | None = None) -> int:
340
352
  return handle_secrets_delete(config, args.slug)
341
353
  return report_error("unknown secrets command")
342
354
  if args.command == "integrations":
355
+ if args.integrations_action in ("list", "ls"):
356
+ return handle_integrations_list(config)
343
357
  if args.integrations_action == "connect":
344
358
  return handle_integrations_connect(config, args.provider, args.slug, args.owner, no_open=args.no_open)
345
359
  return report_error("unknown integrations command")
360
+ if args.command in ("switch", "checkout"):
361
+ return handle_switch(
362
+ config,
363
+ getattr(args, "repo", None),
364
+ getattr(args, "branch", None),
365
+ create_branch=getattr(args, "create", False),
366
+ base_branch=getattr(args, "base_branch", None),
367
+ )
346
368
  if args.command == "create":
347
369
  return handle_create(
348
370
  config,
@@ -764,6 +786,48 @@ def _resolve_computer_ref(client: RuntimeClient, ref: str) -> str:
764
786
 
765
787
 
766
788
 
789
+ def _coerce_github_repo(repo: str | None) -> str | None:
790
+ text = str(repo or "").strip()
791
+ if not text:
792
+ return None
793
+ if not re.fullmatch(r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+", text):
794
+ raise RuntimeAPIError("repo must be in owner/name format")
795
+ owner, name = text.split("/", 1)
796
+ return f"{owner}/{name}"
797
+
798
+
799
+
800
+ def _coerce_git_branch(branch: str | None) -> str | None:
801
+ text = str(branch or "").strip()
802
+ if not text:
803
+ return None
804
+ if text == "@":
805
+ raise RuntimeAPIError("branch name is invalid")
806
+ if text.startswith("-") or text.endswith("/") or text.endswith(".") or text.endswith(".lock"):
807
+ raise RuntimeAPIError("branch name is invalid")
808
+ if ".." in text or "//" in text or "@{" in text:
809
+ raise RuntimeAPIError("branch name is invalid")
810
+ if any(ch in text for ch in (" ", "~", "^", ":", "?", "*", "[", "\\")):
811
+ raise RuntimeAPIError("branch name is invalid")
812
+ if any(ord(ch) < 32 or ord(ch) == 127 for ch in text):
813
+ raise RuntimeAPIError("branch name is invalid")
814
+ return text
815
+
816
+
817
+
818
+ def _render_branch_workspace(payload: dict[str, Any]) -> None:
819
+ computer = payload.get("computer") or {}
820
+ repo = str(payload.get("repo") or "")
821
+ branch = str(payload.get("branch") or "")
822
+ action = "created branch" if payload.get("create_branch") else "opened branch"
823
+ _render_computer_panel(computer, title="workspace")
824
+ _UI.console().print(f"[green]✓[/green] {action} [bold]{repo}#{branch}[/bold]")
825
+ url = computer.get("public_url")
826
+ if url:
827
+ _UI.console().print(f"[bold green]→[/bold green] [cyan]{url}[/cyan]")
828
+
829
+
830
+
767
831
  def _computer_name(c: dict[str, Any]) -> str:
768
832
  return str(c.get("slug") or c.get("id") or "?")
769
833
 
@@ -1492,6 +1556,35 @@ def handle_api_keys_revoke(config: RuntimeConfig, key_id: str | None) -> int:
1492
1556
 
1493
1557
 
1494
1558
 
1559
+ def _render_integrations_table(payload: dict[str, Any]) -> None:
1560
+ mods = _UI.rich()
1561
+ Table = mods["Table"]
1562
+ integrations = payload.get("integrations") or []
1563
+ if not isinstance(integrations, list) or not integrations:
1564
+ _UI.console().print("[yellow]no integrations found[/yellow]")
1565
+ return
1566
+
1567
+ table = Table(title="Integrations", expand=True, box=mods["box"].SIMPLE_HEAVY)
1568
+ table.add_column("Slug", style="bold white")
1569
+ table.add_column("Provider", style="white")
1570
+ table.add_column("Owner", style="white")
1571
+ table.add_column("Transport", style="white")
1572
+ table.add_column("Backend", style="white")
1573
+
1574
+ for entry in integrations:
1575
+ if not isinstance(entry, dict):
1576
+ continue
1577
+ table.add_row(
1578
+ str(entry.get("slug") or "—"),
1579
+ str(entry.get("provider") or "—"),
1580
+ str(entry.get("credential_ref") or "—"),
1581
+ str(entry.get("transport") or "—"),
1582
+ str(entry.get("backend") or "—"),
1583
+ )
1584
+ _UI.console().print(table)
1585
+
1586
+
1587
+
1495
1588
  def _coerce_secret_set_slug(slug: str | None) -> str | None:
1496
1589
  value = str(slug or "").strip()
1497
1590
  return value or None
@@ -1624,6 +1717,14 @@ def handle_secrets_delete(config: RuntimeConfig, slug: str | None) -> int:
1624
1717
 
1625
1718
 
1626
1719
 
1720
+ def handle_integrations_list(config: RuntimeConfig) -> int:
1721
+ if (err := _require_api_key(config)) is not None:
1722
+ return err
1723
+ client = RuntimeClient(base_url=config.base_url, api_key=config.api_key)
1724
+ return report_success(client.list_integrations(), _render_integrations_table)
1725
+
1726
+
1727
+
1627
1728
  def handle_integrations_connect(
1628
1729
  config: RuntimeConfig,
1629
1730
  provider: str,
@@ -1686,6 +1787,74 @@ def handle_integrations_connect(
1686
1787
 
1687
1788
 
1688
1789
 
1790
+ def handle_switch(
1791
+ config: RuntimeConfig,
1792
+ repo: str | None,
1793
+ branch: str | None,
1794
+ *,
1795
+ create_branch: bool = False,
1796
+ base_branch: str | None = None,
1797
+ ) -> int:
1798
+ if (err := _require_api_key(config)) is not None:
1799
+ return err
1800
+ if base_branch is not None and not create_branch:
1801
+ return report_error("--from can only be used together with -c")
1802
+
1803
+ try:
1804
+ resolved_repo = _coerce_github_repo(repo)
1805
+ resolved_branch = _coerce_git_branch(branch)
1806
+ except RuntimeAPIError as exc:
1807
+ return report_error(str(exc), status_code=exc.status_code)
1808
+
1809
+ if resolved_repo is None:
1810
+ if not _interactive():
1811
+ return report_error("--repo is required")
1812
+ resolved_repo = _prompt_text("GitHub repo (owner/name)")
1813
+ try:
1814
+ resolved_repo = _coerce_github_repo(resolved_repo)
1815
+ except RuntimeAPIError as exc:
1816
+ return report_error(str(exc), status_code=exc.status_code)
1817
+ if resolved_repo is None:
1818
+ return report_error("repo is required")
1819
+
1820
+ if resolved_branch is None:
1821
+ if not _interactive():
1822
+ return report_error("branch is required")
1823
+ prompt = "New branch name" if create_branch else "Branch name"
1824
+ resolved_branch = _prompt_text(prompt)
1825
+ try:
1826
+ resolved_branch = _coerce_git_branch(resolved_branch)
1827
+ except RuntimeAPIError as exc:
1828
+ return report_error(str(exc), status_code=exc.status_code)
1829
+ if resolved_branch is None:
1830
+ return report_error("branch is required")
1831
+
1832
+ client = RuntimeClient(base_url=config.base_url, api_key=config.api_key)
1833
+ try:
1834
+ payload = _with_spinner(
1835
+ "opening branch workspace…",
1836
+ lambda: client.switch_workspace(
1837
+ repo=resolved_repo,
1838
+ branch=resolved_branch,
1839
+ create=create_branch,
1840
+ base_branch=base_branch,
1841
+ ),
1842
+ )
1843
+ except RuntimeAPIError as exc:
1844
+ return report_error(str(exc), status_code=exc.status_code)
1845
+
1846
+ computer = payload.get("computer") or {}
1847
+ computer_id = str(computer.get("id") or "").strip()
1848
+ if not computer_id:
1849
+ return report_error("server did not return a computer id")
1850
+
1851
+ code = report_success(payload, _render_branch_workspace)
1852
+ if code != 0 or not _interactive():
1853
+ return code
1854
+ return _enter_computer(config, computer_id)
1855
+
1856
+
1857
+
1689
1858
  def handle_onboarding(config: RuntimeConfig) -> int:
1690
1859
  code = handle_login(config, None, None, None)
1691
1860
  if code != 0:
@@ -54,6 +54,9 @@ class RuntimeClient:
54
54
 
55
55
  # --- Integrations ---
56
56
 
57
+ def list_integrations(self) -> dict[str, Any]:
58
+ return self._request("GET", "/api/integrations", auth_required=True)
59
+
57
60
  def start_github_connect(self, *, slug: str | None = None) -> dict[str, Any]:
58
61
  body: dict[str, Any] = {}
59
62
  if slug:
@@ -96,6 +99,25 @@ class RuntimeClient:
96
99
 
97
100
  # --- Computers ---
98
101
 
102
+ def switch_workspace(
103
+ self,
104
+ *,
105
+ repo: str,
106
+ branch: str,
107
+ create: bool = False,
108
+ base_branch: str | None = None,
109
+ ) -> dict[str, Any]:
110
+ body: dict[str, Any] = {"repo": repo, "branch": branch, "create": create}
111
+ if base_branch is not None:
112
+ body["from"] = base_branch
113
+ return self._request(
114
+ "POST",
115
+ "/api/workspaces/switch",
116
+ json=body,
117
+ auth_required=True,
118
+ timeout=max(self.timeout, DEFAULT_WARMUP_TIMEOUT),
119
+ )
120
+
99
121
  def create_computer(
100
122
  self,
101
123
  *,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: runtime-sdk
3
- Version: 0.4.18
3
+ Version: 0.4.20
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,13 @@ 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 integrations list
70
+ runtime integrations connect github
69
71
 
70
72
  # Computers
73
+ runtime switch --repo owner/repo feature/login # open or create the branch workspace computer
74
+ runtime switch -c feature/login --repo owner/repo # create a new branch workspace from the repo default branch
75
+ runtime checkout --repo owner/repo feature/login # alias for switch
71
76
  runtime create # creates a computer with the starter app already published
72
77
  runtime create myapp --command "python3 app.py" --cwd /home/ubuntu --port 3000
73
78
  runtime enter <name-or-id> # accepts slug/name like test, or the computer id
@@ -157,6 +162,28 @@ show|clear` expose that same durable app state directly. The low-level
157
162
  `runtime run` stays one-shot: the filesystem is restored after going cold, but
158
163
  that ad-hoc process is not.
159
164
 
165
+ ## GitHub branch workspaces
166
+
167
+ `runtime switch` gives a repo branch its own Runtime computer so you can jump
168
+ between parallel tasks without local worktrees.
169
+
170
+ ```bash
171
+ runtime integrations connect github
172
+ runtime switch --repo owner/repo feature/login
173
+ runtime switch -c feature/search --repo owner/repo --from main
174
+ ```
175
+
176
+ Rules:
177
+ - GitHub must already be connected with `runtime integrations connect github`.
178
+ - `runtime switch` opens an existing branch workspace, or creates the computer if
179
+ the branch exists but the workspace does not yet.
180
+ - `runtime switch -c` creates a new branch first, then opens that branch's
181
+ workspace.
182
+ - Each `repo + branch` maps to one dedicated computer.
183
+ - The repo is cloned into `/home/ubuntu/<repo>` and new shells in that computer
184
+ start there automatically.
185
+ - `runtime checkout ...` is an alias for `runtime switch ...`.
186
+
160
187
  Run the SDK unit tests through the backend project environment:
161
188
 
162
189
  ```bash
File without changes