runtime-sdk 0.4.25__tar.gz → 0.4.27__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.25
3
+ Version: 0.4.27
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 integrations list
70
- runtime integrations connect github
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 integrations connect github
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 integrations connect github`.
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 integrations list
48
- runtime integrations connect github
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 integrations connect github
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 integrations connect github`.
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "runtime-sdk"
3
- version = "0.4.25"
3
+ version = "0.4.27"
4
4
  description = "Runtime Python SDK and CLI"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -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
- integrations_cmd = subparsers.add_parser("integrations", help="Connect account-level integrations")
196
- integrations_subparsers = integrations_cmd.add_subparsers(dest="integrations_action", required=True)
197
- integrations_subparsers.add_parser("list", aliases=["ls"], help="List integrations")
198
- integrations_connect = integrations_subparsers.add_parser("connect", help="Connect an integration")
199
- integrations_connect.add_argument("provider", choices=["github"])
200
- integrations_connect.add_argument("--slug", default="github", help="Integration slug to create (default: github)")
201
- integrations_connect.add_argument("--owner", default=None, help="GitHub username or org where the app is installed")
202
- integrations_connect.add_argument("--no-open", action="store_true", help="Print the install URL instead of opening a browser")
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
- ("integrations list", "List account-level integrations"),
380
- ("integrations connect github", "Install the RuntimeVM GitHub App"),
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 == "integrations":
510
- if args.integrations_action in ("list", "ls"):
511
- return handle_integrations_list(config)
512
- if args.integrations_action == "connect":
513
- return handle_integrations_connect(config, args.provider, args.slug, args.owner, no_open=args.no_open)
514
- return report_error("unknown integrations command")
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,
@@ -1693,30 +1700,31 @@ def handle_api_keys_revoke(config: RuntimeConfig, key_id: str | None) -> int:
1693
1700
 
1694
1701
 
1695
1702
 
1696
- def _render_integrations_table(payload: dict[str, Any]) -> None:
1703
+ def _render_github_installations_table(payload: dict[str, Any]) -> None:
1697
1704
  mods = _UI.rich()
1698
1705
  Table = mods["Table"]
1699
- integrations = payload.get("integrations") or []
1700
- if not isinstance(integrations, list) or not integrations:
1701
- _UI.console().print("[yellow]no integrations found[/yellow]")
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]")
1702
1709
  return
1703
1710
 
1704
- table = Table(title="Integrations", expand=True, box=mods["box"].SIMPLE_HEAVY)
1705
- table.add_column("Slug", style="bold white")
1706
- 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")
1707
1713
  table.add_column("Owner", style="white")
1708
- table.add_column("Transport", style="white")
1709
- table.add_column("Backend", style="white")
1714
+ table.add_column("Type", style="white")
1715
+ table.add_column("Repos", style="white")
1716
+ table.add_column("Status", style="white")
1710
1717
 
1711
- for entry in integrations:
1718
+ for entry in installations:
1712
1719
  if not isinstance(entry, dict):
1713
1720
  continue
1721
+ status = "suspended" if entry.get("suspended") else "connected"
1714
1722
  table.add_row(
1715
- str(entry.get("slug") or "—"),
1716
- str(entry.get("provider") or "—"),
1717
- str(entry.get("credential_ref") or "—"),
1718
- str(entry.get("transport") or "—"),
1719
- str(entry.get("backend") or "—"),
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,
1720
1728
  )
1721
1729
  _UI.console().print(table)
1722
1730
 
@@ -1854,21 +1862,68 @@ def handle_secrets_delete(config: RuntimeConfig, slug: str | None) -> int:
1854
1862
 
1855
1863
 
1856
1864
 
1857
- def handle_integrations_list(config: RuntimeConfig) -> int:
1865
+ def handle_github_list(config: RuntimeConfig) -> int:
1866
+ if (err := _require_api_key(config)) is not None:
1867
+ return err
1868
+ client = RuntimeClient(base_url=config.base_url, api_key=config.api_key)
1869
+ return report_success(client.list_github_installations(), _render_github_installations_table)
1870
+
1871
+
1872
+
1873
+ def handle_github_disconnect(config: RuntimeConfig, ref: str | None) -> int:
1858
1874
  if (err := _require_api_key(config)) is not None:
1859
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
+
1860
1884
  client = RuntimeClient(base_url=config.base_url, api_key=config.api_key)
1861
- return report_success(client.list_integrations(), _render_integrations_table)
1885
+ installation_id: int | None = None
1886
+ owner = resolved_ref
1887
+
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
+ )
1862
1918
 
1863
1919
 
1864
1920
 
1865
- def handle_integrations_connect(
1921
+ def handle_integrate_github(
1866
1922
  config: RuntimeConfig,
1867
1923
  provider: str,
1868
- slug: str | None,
1869
- owner: str | None,
1870
1924
  *,
1871
1925
  no_open: bool = False,
1926
+ timeout_seconds: int = 180,
1872
1927
  ) -> int:
1873
1928
  if (err := _require_api_key(config)) is not None:
1874
1929
  return err
@@ -1876,11 +1931,11 @@ def handle_integrations_connect(
1876
1931
  return report_error(f"unsupported integration provider: {provider}")
1877
1932
 
1878
1933
  client = RuntimeClient(base_url=config.base_url, api_key=config.api_key)
1879
- payload = _with_spinner("getting GitHub install link…", lambda: client.start_github_connect(slug=slug))
1934
+ payload = _with_spinner("getting GitHub install link…", client.start_github_connect)
1880
1935
  install_url = str(payload.get("install_url") or "").strip()
1881
- resolved_slug = str(payload.get("slug") or slug or "github").strip() or "github"
1882
- if not install_url:
1883
- return report_error("server did not return a GitHub install URL")
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")
1884
1939
 
1885
1940
  opened = False
1886
1941
  if not no_open:
@@ -1890,37 +1945,26 @@ def handle_integrations_connect(
1890
1945
  if _interactive():
1891
1946
  action = "opened in your browser" if opened else "copy this URL into your browser"
1892
1947
  _UI.console().print(f"[cyan]→[/cyan] {action}: [link={install_url}]{install_url}[/link]")
1893
- _UI.console().print("[dim]Install the RuntimeVM GitHub App, then come back here.[/dim]")
1894
- input("Press Enter after the GitHub App is installed... ")
1895
- if not owner:
1896
- owner = _prompt_text("GitHub owner (username or org)")
1897
- elif not owner:
1898
- return report_error("--owner is required without a TTY")
1899
-
1900
- resolved_owner = str(owner or "").strip()
1901
- if not resolved_owner:
1902
- return report_error("GitHub owner is required")
1903
-
1904
- integration = _with_spinner(
1905
- "connecting GitHub…",
1906
- lambda: client.create_integration(
1907
- slug=resolved_slug,
1908
- provider="github",
1909
- transport="scoped_credential",
1910
- backend="native",
1911
- credential_source="external_account",
1912
- credential_ref=resolved_owner,
1913
- config={},
1914
- ),
1915
- )
1948
+ _UI.console().print("[dim]Choose the GitHub account/org and repo access in GitHub. Waiting for completion…[/dim]")
1916
1949
 
1917
- def render(result: dict[str, Any]) -> None:
1918
- connected = result.get("integration") or {}
1919
- _UI.console().print(
1920
- f"[green]✓[/green] connected [bold]{connected.get('slug', resolved_slug)}[/bold] → [bold]{connected.get('credential_ref', resolved_owner)}[/bold]"
1921
- )
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)
1922
1966
 
1923
- return report_success({"integration": integration, "install_url": install_url}, render)
1967
+ return report_error(f"timed out waiting for GitHub install to finish (last status: {last_status})")
1924
1968
 
1925
1969
 
1926
1970
 
@@ -3207,7 +3251,7 @@ _runtime_complete() {
3207
3251
  COMPREPLY=()
3208
3252
  cur="${COMP_WORDS[COMP_CWORD]}"
3209
3253
  if [[ ${COMP_CWORD} -eq 1 ]]; then
3210
- COMPREPLY=( $(compgen -W "create list ls info url start enter run publish delete rm share startup service proxy switch checkout integrations secrets api-keys signup verify login whoami logout completion help" -- "$cur") )
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") )
3211
3255
  return
3212
3256
  fi
3213
3257
  sub="${COMP_WORDS[1]}"
@@ -3256,7 +3300,8 @@ _runtime() {
3256
3300
  'proxy:Port proxies'
3257
3301
  'switch:Open a GitHub branch workspace'
3258
3302
  'checkout:Open a GitHub branch workspace'
3259
- 'integrations:Integrations'
3303
+ 'github:GitHub connections'
3304
+ 'integrate:Connect GitHub'
3260
3305
  'secrets:Secret sets'
3261
3306
  'api-keys:API keys'
3262
3307
  'signup:Send verification code'
@@ -3321,6 +3366,8 @@ complete -c runtime -n '__fish_use_subcommand' -a share -d 'Set visibility'
3321
3366
  complete -c runtime -n '__fish_use_subcommand' -a startup -d 'Startup config'
3322
3367
  complete -c runtime -n '__fish_use_subcommand' -a service -d 'Service config'
3323
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'
3324
3371
  complete -c runtime -n '__fish_use_subcommand' -a help -d 'Extended help'
3325
3372
  complete -c runtime -n '__fish_use_subcommand' -a completion -d 'Completion'
3326
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
- # --- Integrations ---
55
+ # --- GitHub App ---
56
56
 
57
- def list_integrations(self) -> dict[str, Any]:
58
- return self._request("GET", "/api/integrations", auth_required=True)
57
+ def start_github_connect(self) -> dict[str, Any]:
58
+ return self._request("POST", "/api/github/connect", auth_required=True)
59
59
 
60
- def start_github_connect(self, *, slug: str | None = None) -> dict[str, Any]:
61
- body: dict[str, Any] = {}
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 create_integration(
67
- self,
68
- *,
69
- slug: str,
70
- provider: str,
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.25
3
+ Version: 0.4.27
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 integrations list
70
- runtime integrations connect github
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 integrations connect github
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 integrations connect github`.
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