urun-cli 0.5.2__tar.gz → 0.5.4__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.
- urun_cli-0.5.4/CHANGELOG.md +123 -0
- {urun_cli-0.5.2 → urun_cli-0.5.4}/PKG-INFO +42 -13
- {urun_cli-0.5.2 → urun_cli-0.5.4}/README.md +41 -12
- {urun_cli-0.5.2 → urun_cli-0.5.4}/pyproject.toml +1 -1
- {urun_cli-0.5.2 → urun_cli-0.5.4}/src/urun/api.py +157 -76
- {urun_cli-0.5.2 → urun_cli-0.5.4}/src/urun/catalog.py +407 -49
- {urun_cli-0.5.2 → urun_cli-0.5.4}/src/urun/cli.py +1176 -421
- {urun_cli-0.5.2 → urun_cli-0.5.4}/src/urun/config.py +29 -12
- {urun_cli-0.5.2 → urun_cli-0.5.4}/src/urun/deps.py +194 -0
- urun_cli-0.5.4/src/urun/discovery.py +513 -0
- {urun_cli-0.5.2 → urun_cli-0.5.4}/src/urun/manifest.py +15 -1
- {urun_cli-0.5.2 → urun_cli-0.5.4}/src/urun/serve.py +47 -23
- urun_cli-0.5.2/CHANGELOG.md +0 -70
- urun_cli-0.5.2/src/urun/discovery.py +0 -183
- {urun_cli-0.5.2 → urun_cli-0.5.4}/.gitignore +0 -0
- {urun_cli-0.5.2 → urun_cli-0.5.4}/LICENSE +0 -0
- {urun_cli-0.5.2 → urun_cli-0.5.4}/SECURITY.md +0 -0
- {urun_cli-0.5.2 → urun_cli-0.5.4}/src/urun/__init__.py +0 -0
- {urun_cli-0.5.2 → urun_cli-0.5.4}/src/urun/errors.py +0 -0
- {urun_cli-0.5.2 → urun_cli-0.5.4}/src/urun/retry.py +0 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.5.4
|
|
4
|
+
|
|
5
|
+
- Ship the serving-engine dependency map (`urun.catalog.ENGINE_DEPS`) additions
|
|
6
|
+
and pin syncs that landed on `main` after `v0.5.3` but were never released:
|
|
7
|
+
the `kyutai-moshi`, `moshi-vis`, `stt`, `tts`, `echo-stt`, and `echo-speak`
|
|
8
|
+
engine shapes (#98), the sphn 0.2.x / `moshi==0.2.13` pin syncs (#100, #101),
|
|
9
|
+
and the platform `llamacpp-diffusion` wheel pin (#104). The installed
|
|
10
|
+
`v0.5.3` CLI's strict fail-loud guard therefore refused live dev deploys of
|
|
11
|
+
`urun serve hibiki-zero-3b:bf16` / `urun serve moshivis:bf16` ("serve engine
|
|
12
|
+
'kyutai-moshi' is not in the CLI's known serving-engine set ...") even though
|
|
13
|
+
`main`'s map was already in sync with urun-python — the drift was the stale
|
|
14
|
+
RELEASE, not the source. This release carries the synced map to PyPI.
|
|
15
|
+
- Make that strict refusal self-diagnosing: the error now names the RUNNING
|
|
16
|
+
CLI version and tells the operator to upgrade first (`uv tool upgrade
|
|
17
|
+
urun-cli`), and only then to suspect genuine source drift from urun-python's
|
|
18
|
+
`urun.serve.engines.extras.ENGINE_DEPS` (which the cross-repo parity test
|
|
19
|
+
guards in CI). The old text sent operators to edit a map that was already
|
|
20
|
+
correct at `origin/main`.
|
|
21
|
+
|
|
22
|
+
- Make the org-secrets store the canonical source for app-declared
|
|
23
|
+
`Credentials(env=..., optional_env=...)`. On `urun deploy`, each declared name
|
|
24
|
+
is resolved against the org-secrets store FIRST — org-level secrets plus this
|
|
25
|
+
app's app-level secrets, exactly the set the control plane injects into the
|
|
26
|
+
runtime pod env. A name already in the store is used as-is and is NEVER read
|
|
27
|
+
from or overridden by the deployer's shell (no silent shadowing of an org
|
|
28
|
+
secret by a stray local value). Only declared names the store does not yet hold
|
|
29
|
+
are filled from the local environment and delivered through the documented
|
|
30
|
+
app-scoped fallback channel. A REQUIRED credential resolvable from NEITHER the
|
|
31
|
+
store NOR the local env fails loud at deploy (never a silent drop that
|
|
32
|
+
resurfaces as a 401 inside the cluster); a missing OPTIONAL credential is
|
|
33
|
+
skipped silently (its contract). Previously the deploy read declared
|
|
34
|
+
credentials ONLY from the deployer's shell and failed if they were unset there
|
|
35
|
+
even when the org secret existed — and pushed the local value as an app secret
|
|
36
|
+
that shadowed the canonical org secret. Secret values are never printed.
|
|
37
|
+
- Add `urun get secret <NAME>` (metadata only — the store never returns values)
|
|
38
|
+
to round out the secret CRUD alongside `urun set secret` / `urun list secrets`
|
|
39
|
+
/ `urun delete secret`. `urun set secret <NAME>` with neither `--value` nor
|
|
40
|
+
`--from-file` now reads the value from a non-echoing secure prompt so it never
|
|
41
|
+
lands in shell history / argv.
|
|
42
|
+
|
|
43
|
+
- Make `urun deploy` resilient to transient network blips. The manifest/blob
|
|
44
|
+
push and the "Waiting for build to complete" build-status poll now retry on
|
|
45
|
+
transient failures (read/connect timeouts, connection resets, 5xx, 429) with
|
|
46
|
+
exponential backoff + jitter (base 1s, factor 2, cap 30s) instead of fatally
|
|
47
|
+
exiting on the first `network error: The read operation timed out`. A single
|
|
48
|
+
failed status read backs off and re-polls — the build continues server-side —
|
|
49
|
+
while a real build failure or a 4xx auth/validation error still fails fast and
|
|
50
|
+
loud. Every retry is logged (`transient ...; retrying in Ns`) so backoff is
|
|
51
|
+
visible, never silent. New `urun.retry` module centralizes the
|
|
52
|
+
transient-vs-terminal classification and backoff policy.
|
|
53
|
+
|
|
54
|
+
- Add experimental `urun list apps` subcommand. Reads from a new control-plane
|
|
55
|
+
endpoint `GET /apps` (server-side edge function not yet shipped). The STATUS
|
|
56
|
+
column projects the app's current lifecycle phase to one of `provisioning`,
|
|
57
|
+
`ready`, `pending`, `paused`, or `failed`; see the README or
|
|
58
|
+
`urun list apps --help` for the full mapping. Supports `--json` for the raw
|
|
59
|
+
payload.
|
|
60
|
+
- Add experimental `urun list sessions` subcommand. Lists live and historical
|
|
61
|
+
sessions for the org, with per-row STARTED / DURATION / STATE / DETAIL.
|
|
62
|
+
Newest sessions print at the bottom of the table (tail-friendly). Supports
|
|
63
|
+
`--limit` (default 100), `--state` (filter to a single backend
|
|
64
|
+
status), and `--json`. Reads from a new control-plane endpoint
|
|
65
|
+
`GET /sessions` (server-side edge function not yet shipped).
|
|
66
|
+
- Add experimental `urun list compute` subcommand. Lists the compute slices
|
|
67
|
+
the org currently has provisioned, broken down per (app, function,
|
|
68
|
+
compute_shape) with INSTANCES (allocated/ready), GPU UNITS
|
|
69
|
+
(allocated/ready), live SESSIONS count, and snapshot AGE. Slices with no
|
|
70
|
+
provisioned GPU units are omitted server-side so the listing reflects
|
|
71
|
+
what is running right now (`urun list apps` for the full deployment
|
|
72
|
+
catalogue; `urun list sessions` for history). Supports `--limit`
|
|
73
|
+
(default 100) and `--json`. Reads from a new control-plane
|
|
74
|
+
endpoint `GET /compute` (server-side edge function not yet shipped).
|
|
75
|
+
- Add experimental `urun app status|scale|disable|enable` lifecycle commands
|
|
76
|
+
for managing a single deployed app (addressed by slug, org-scoped via the API
|
|
77
|
+
key). `app status` shows the build/deployment/capacity/live-session rollup;
|
|
78
|
+
`app scale --replicas N` sets the deployment's desired replica count (use 0 to
|
|
79
|
+
drain; GPU/shape stay fixed at deploy time, so only `--replicas` is exposed);
|
|
80
|
+
`app disable` disables an app — driving it to `paused`/
|
|
81
|
+
`disabled` so the control-plane materializer stops recreating its runtime —
|
|
82
|
+
with an interactive confirmation (`--yes` to skip); `app enable` reverses it.
|
|
83
|
+
All accept `--environment` (default `prod`) and `--json`. Backed by a
|
|
84
|
+
new server-side `app` edge function (`GET /app/<slug>`,
|
|
85
|
+
`POST /app/<slug>/scale`, `DELETE /app/<slug>`, `POST /app/<slug>/activate`).
|
|
86
|
+
- Remove duplicate command surfaces in favor of one canonical spelling:
|
|
87
|
+
`urun app status` replaces `urun get app`, `urun app scale` replaces
|
|
88
|
+
`urun scale app`, and bare `urun org` replaces `urun org id` /
|
|
89
|
+
`urun get org-id`. `urun get org` remains for the richer org-config summary.
|
|
90
|
+
|
|
91
|
+
## 0.5.3
|
|
92
|
+
|
|
93
|
+
- Publish the previously-unreleased `urun app purge` and `urun build purge`
|
|
94
|
+
subcommands. Both landed on `main` after the `v0.5.2` tag was cut, and the
|
|
95
|
+
release workflow skips re-publishing when the `pyproject` version is unchanged,
|
|
96
|
+
so they were never available on PyPI. This is a version-bump-only release that
|
|
97
|
+
carries the existing, already-tested commands to PyPI so CI/operators can run
|
|
98
|
+
`uvx --from urun-cli==0.5.3 urun app purge <slug>` (hard-delete orphaned app
|
|
99
|
+
metadata after `urun app delete`) without a private git install. No CLI
|
|
100
|
+
behavior changes beyond making these commands installable.
|
|
101
|
+
|
|
102
|
+
## 0.3.0
|
|
103
|
+
|
|
104
|
+
- Implement the `urun org` / `urun org id` command: print the caller's org id
|
|
105
|
+
(resolved via the control-plane org-config endpoint).
|
|
106
|
+
- Implement `urun auth jwks set`: register the org's trusted JWKS for federated
|
|
107
|
+
identity, via `--jwks-url` or `--jwks-json` (file or stdin), with optional
|
|
108
|
+
`--issuer`/`--audience`. This registers a trust relationship only; JWTs are
|
|
109
|
+
issued out of band and the CLI never mints, fetches, or stores one.
|
|
110
|
+
- Add `ApiClient.register_trusted_jwks`, calling `POST /org-config/trusted-jwks`.
|
|
111
|
+
|
|
112
|
+
## 0.2.0
|
|
113
|
+
|
|
114
|
+
- Bumped past 0.1.1/0.1.2 because both filenames were occupied by previously-deleted PyPI artifacts.
|
|
115
|
+
- Drop the org-id requirement from `urun deploy`; authentication now relies solely on the API key.
|
|
116
|
+
- Add `urun login`, `urun run`, and `urun org`/`urun auth` subcommands.
|
|
117
|
+
|
|
118
|
+
## 0.1.0
|
|
119
|
+
|
|
120
|
+
- Initial public `urun deploy` CLI.
|
|
121
|
+
- Deploy from a Python app file with local Python imports included automatically.
|
|
122
|
+
- Source manifest generation, dependency declaration upload, and API deploy flow.
|
|
123
|
+
- Public docs for the config-free v1 CLI surface.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: urun-cli
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.4
|
|
4
4
|
Summary: End-user CLI for deploying apps to urun
|
|
5
5
|
Project-URL: Homepage, https://urun.sh
|
|
6
6
|
Project-URL: Repository, https://github.com/urun-sh/urun-cli
|
|
@@ -51,21 +51,26 @@ uvx urun-cli --version
|
|
|
51
51
|
|
|
52
52
|
## Quick start
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
with `urun login`:
|
|
54
|
+
Save an org-scoped deploy API key locally with `urun login`:
|
|
56
55
|
|
|
57
56
|
```bash
|
|
58
|
-
urun login
|
|
57
|
+
urun login
|
|
59
58
|
```
|
|
60
59
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
If `URUN_API_KEY` is not already set, `urun login` opens the urun console API
|
|
61
|
+
keys page, prompts you to paste the generated `urun_sk_...` key, verifies it
|
|
62
|
+
with the urun API, and stores credentials for later commands.
|
|
63
|
+
|
|
64
|
+
If you already have a key, you can pass it directly:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
urun login --api-key urun_sk_<secret>
|
|
68
|
+
```
|
|
64
69
|
|
|
65
70
|
For CI or one-off commands, you can still use the environment variable:
|
|
66
71
|
|
|
67
72
|
```bash
|
|
68
|
-
export URUN_API_KEY=
|
|
73
|
+
export URUN_API_KEY=urun_sk_<secret>
|
|
69
74
|
```
|
|
70
75
|
|
|
71
76
|
Create `app.py`:
|
|
@@ -93,6 +98,24 @@ In this release, `urun run` uses the same deploy pipeline as `urun deploy`.
|
|
|
93
98
|
`deploy` remains available as the lower-level command while the full
|
|
94
99
|
deploy/run/monitor workflow is being built.
|
|
95
100
|
|
|
101
|
+
### Launch the prompt-canary browser demo
|
|
102
|
+
|
|
103
|
+
For the official `urun-examples` prompt-canary app, the shortest browser stream
|
|
104
|
+
path is:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
cd ~/workspace/urun-examples
|
|
108
|
+
urun deploy prompt_canary/backend/app.py --name prompt-canary --timeout 900
|
|
109
|
+
urun demo prompt-canary
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`urun demo` currently supports `prompt-canary` only. It uses your stored API key
|
|
113
|
+
server-side to request a short-lived browser JWT, sets the required
|
|
114
|
+
`NEXT_PUBLIC_URUN_*` environment variables, starts the local Next.js frontend,
|
|
115
|
+
and opens it in your browser. Override the examples checkout with
|
|
116
|
+
`--examples-root` or `URUN_EXAMPLES_ROOT`; override the session API with
|
|
117
|
+
`--session-url` or `URUN_SESSION_BASE_URL`.
|
|
118
|
+
|
|
96
119
|
## Serve a model from the catalog
|
|
97
120
|
|
|
98
121
|
`urun serve` deploys a model straight from the urun model catalog — no app code
|
|
@@ -274,20 +297,26 @@ urun app scale lingbot --replicas 0 # drain to zero without retiring
|
|
|
274
297
|
GPU count and compute shape are fixed at deploy time per release (set via
|
|
275
298
|
`@app.function`), so `scale` intentionally exposes only `--replicas`.
|
|
276
299
|
|
|
277
|
-
|
|
300
|
+
Disable an app so the control plane stops running it (drives the deployment
|
|
278
301
|
to `paused` and the app to `disabled`, so the materializer stops recreating
|
|
279
302
|
its runtime). This is the clean, reversible, API-driven alternative to a
|
|
280
303
|
manual database edit:
|
|
281
304
|
|
|
282
305
|
```bash
|
|
283
|
-
urun app
|
|
284
|
-
urun app
|
|
306
|
+
urun app disable lingbot-handle # prompts for confirmation
|
|
307
|
+
urun app disable lingbot-handle --yes # skip confirmation
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Enable a disabled app and bring it back online:
|
|
311
|
+
|
|
312
|
+
```bash
|
|
313
|
+
urun app enable lingbot-handle
|
|
285
314
|
```
|
|
286
315
|
|
|
287
|
-
|
|
316
|
+
Stop a queued, starting, or live session:
|
|
288
317
|
|
|
289
318
|
```bash
|
|
290
|
-
urun
|
|
319
|
+
urun stop session sess_123 --yes
|
|
291
320
|
```
|
|
292
321
|
|
|
293
322
|
All `app` subcommands accept `--environment` (default `prod`) and `--json`.
|
|
@@ -29,21 +29,26 @@ uvx urun-cli --version
|
|
|
29
29
|
|
|
30
30
|
## Quick start
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
with `urun login`:
|
|
32
|
+
Save an org-scoped deploy API key locally with `urun login`:
|
|
34
33
|
|
|
35
34
|
```bash
|
|
36
|
-
urun login
|
|
35
|
+
urun login
|
|
37
36
|
```
|
|
38
37
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
If `URUN_API_KEY` is not already set, `urun login` opens the urun console API
|
|
39
|
+
keys page, prompts you to paste the generated `urun_sk_...` key, verifies it
|
|
40
|
+
with the urun API, and stores credentials for later commands.
|
|
41
|
+
|
|
42
|
+
If you already have a key, you can pass it directly:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
urun login --api-key urun_sk_<secret>
|
|
46
|
+
```
|
|
42
47
|
|
|
43
48
|
For CI or one-off commands, you can still use the environment variable:
|
|
44
49
|
|
|
45
50
|
```bash
|
|
46
|
-
export URUN_API_KEY=
|
|
51
|
+
export URUN_API_KEY=urun_sk_<secret>
|
|
47
52
|
```
|
|
48
53
|
|
|
49
54
|
Create `app.py`:
|
|
@@ -71,6 +76,24 @@ In this release, `urun run` uses the same deploy pipeline as `urun deploy`.
|
|
|
71
76
|
`deploy` remains available as the lower-level command while the full
|
|
72
77
|
deploy/run/monitor workflow is being built.
|
|
73
78
|
|
|
79
|
+
### Launch the prompt-canary browser demo
|
|
80
|
+
|
|
81
|
+
For the official `urun-examples` prompt-canary app, the shortest browser stream
|
|
82
|
+
path is:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
cd ~/workspace/urun-examples
|
|
86
|
+
urun deploy prompt_canary/backend/app.py --name prompt-canary --timeout 900
|
|
87
|
+
urun demo prompt-canary
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`urun demo` currently supports `prompt-canary` only. It uses your stored API key
|
|
91
|
+
server-side to request a short-lived browser JWT, sets the required
|
|
92
|
+
`NEXT_PUBLIC_URUN_*` environment variables, starts the local Next.js frontend,
|
|
93
|
+
and opens it in your browser. Override the examples checkout with
|
|
94
|
+
`--examples-root` or `URUN_EXAMPLES_ROOT`; override the session API with
|
|
95
|
+
`--session-url` or `URUN_SESSION_BASE_URL`.
|
|
96
|
+
|
|
74
97
|
## Serve a model from the catalog
|
|
75
98
|
|
|
76
99
|
`urun serve` deploys a model straight from the urun model catalog — no app code
|
|
@@ -252,20 +275,26 @@ urun app scale lingbot --replicas 0 # drain to zero without retiring
|
|
|
252
275
|
GPU count and compute shape are fixed at deploy time per release (set via
|
|
253
276
|
`@app.function`), so `scale` intentionally exposes only `--replicas`.
|
|
254
277
|
|
|
255
|
-
|
|
278
|
+
Disable an app so the control plane stops running it (drives the deployment
|
|
256
279
|
to `paused` and the app to `disabled`, so the materializer stops recreating
|
|
257
280
|
its runtime). This is the clean, reversible, API-driven alternative to a
|
|
258
281
|
manual database edit:
|
|
259
282
|
|
|
260
283
|
```bash
|
|
261
|
-
urun app
|
|
262
|
-
urun app
|
|
284
|
+
urun app disable lingbot-handle # prompts for confirmation
|
|
285
|
+
urun app disable lingbot-handle --yes # skip confirmation
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Enable a disabled app and bring it back online:
|
|
289
|
+
|
|
290
|
+
```bash
|
|
291
|
+
urun app enable lingbot-handle
|
|
263
292
|
```
|
|
264
293
|
|
|
265
|
-
|
|
294
|
+
Stop a queued, starting, or live session:
|
|
266
295
|
|
|
267
296
|
```bash
|
|
268
|
-
urun
|
|
297
|
+
urun stop session sess_123 --yes
|
|
269
298
|
```
|
|
270
299
|
|
|
271
300
|
All `app` subcommands accept `--environment` (default `prod`) and `--json`.
|
|
@@ -117,6 +117,12 @@ class ApiClient:
|
|
|
117
117
|
{"reason": reason} if reason else {},
|
|
118
118
|
)
|
|
119
119
|
|
|
120
|
+
def create_demo_token(self, app: str, function_name: str | None = None) -> dict[str, Any]:
|
|
121
|
+
body: dict[str, Any] = {"app": app}
|
|
122
|
+
if function_name:
|
|
123
|
+
body["function"] = function_name
|
|
124
|
+
return self._post("/cli/demo-token", body)
|
|
125
|
+
|
|
120
126
|
def list_compute(self, limit: int | None = None) -> dict[str, Any]:
|
|
121
127
|
"""List currently-provisioned compute slices for the authenticated org.
|
|
122
128
|
|
|
@@ -163,12 +169,6 @@ class ApiClient:
|
|
|
163
169
|
def get_capacity_lease(self, lease_id: str) -> dict[str, Any]:
|
|
164
170
|
return self._get(f"/cli/capacity-leases/{urllib.parse.quote(lease_id, safe='')}")
|
|
165
171
|
|
|
166
|
-
def create_capacity_lease(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
167
|
-
return self._post("/cli/capacity-leases", payload)
|
|
168
|
-
|
|
169
|
-
def cancel_capacity_lease(self, lease_id: str) -> dict[str, Any]:
|
|
170
|
-
return self._post(f"/cli/capacity-leases/{urllib.parse.quote(lease_id, safe='')}/cancel")
|
|
171
|
-
|
|
172
172
|
def list_builds(self, params: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
173
173
|
return self._get("/cli/builds", params)
|
|
174
174
|
|
|
@@ -180,6 +180,78 @@ class ApiClient:
|
|
|
180
180
|
f"/cli/builds/{urllib.parse.quote(build_id, safe='')}/logs", {"tail": tail}
|
|
181
181
|
)
|
|
182
182
|
|
|
183
|
+
def purge_build(self, manifest_hash_or_prefix: str) -> dict[str, Any]:
|
|
184
|
+
return self._delete(
|
|
185
|
+
f"/cli/builds/{urllib.parse.quote(manifest_hash_or_prefix, safe='')}/purge"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
def build_app(
|
|
189
|
+
self, prompt: str, app: str | None = None, visibility: str | None = None
|
|
190
|
+
) -> dict[str, Any]:
|
|
191
|
+
"""Enqueue a steerable builder-agent run.
|
|
192
|
+
|
|
193
|
+
Mirrors the uRun MCP builder surface (``build_app`` / ``iterate_app`` /
|
|
194
|
+
``debug_app`` plus the builder steering tools). The MCP tools enqueue by
|
|
195
|
+
INSERTing rows directly into Supabase (``public.builder_runs`` /
|
|
196
|
+
``public.builder_steering``) with a service-role key — there is no
|
|
197
|
+
costly-agents HTTP service. A CLI must never hold a service-role key, so
|
|
198
|
+
the CLI mirrors the same logical operations and the same request-body
|
|
199
|
+
field names but routes them through the org-API-key control-plane REST
|
|
200
|
+
plane (``/cli/builders...``), which performs the privileged insert
|
|
201
|
+
server-side (edge function TBD, same convention as
|
|
202
|
+
``list_apps``/``list_sessions``).
|
|
203
|
+
"""
|
|
204
|
+
body: dict[str, Any] = {"mode": "build", "prompt": prompt}
|
|
205
|
+
if app:
|
|
206
|
+
body["app"] = app
|
|
207
|
+
if visibility:
|
|
208
|
+
body["visibility"] = visibility
|
|
209
|
+
return self._post("/cli/builders", body)
|
|
210
|
+
|
|
211
|
+
def iterate_app(self, app: str, request: str, repo: str | None = None) -> dict[str, Any]:
|
|
212
|
+
body: dict[str, Any] = {"mode": "feature", "app": app, "request": request}
|
|
213
|
+
if repo:
|
|
214
|
+
body["repo"] = repo
|
|
215
|
+
return self._post("/cli/builders", body)
|
|
216
|
+
|
|
217
|
+
def debug_app(
|
|
218
|
+
self, app: str, symptom: str | None = None, repo: str | None = None
|
|
219
|
+
) -> dict[str, Any]:
|
|
220
|
+
body: dict[str, Any] = {"mode": "debug", "app": app}
|
|
221
|
+
if symptom:
|
|
222
|
+
body["symptom"] = symptom
|
|
223
|
+
if repo:
|
|
224
|
+
body["repo"] = repo
|
|
225
|
+
return self._post("/cli/builders", body)
|
|
226
|
+
|
|
227
|
+
def list_builders(self, limit: int | None = None) -> dict[str, Any]:
|
|
228
|
+
return self._get("/cli/builders", {"limit": limit})
|
|
229
|
+
|
|
230
|
+
def get_builder(self, agent_id: str) -> dict[str, Any]:
|
|
231
|
+
return self._get(f"/cli/builders/{urllib.parse.quote(agent_id, safe='')}")
|
|
232
|
+
|
|
233
|
+
def message_builder(self, agent_id: str, message: str) -> dict[str, Any]:
|
|
234
|
+
return self._post(
|
|
235
|
+
f"/cli/builders/{urllib.parse.quote(agent_id, safe='')}/message", {"message": message}
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
def resolve_builder_checkpoint(
|
|
239
|
+
self,
|
|
240
|
+
agent_id: str,
|
|
241
|
+
decision: str,
|
|
242
|
+
amend: dict[str, Any] | None = None,
|
|
243
|
+
reason: str | None = None,
|
|
244
|
+
) -> dict[str, Any]:
|
|
245
|
+
body: dict[str, Any] = {"decision": decision}
|
|
246
|
+
if amend is not None:
|
|
247
|
+
body["amend"] = amend
|
|
248
|
+
if reason is not None:
|
|
249
|
+
body["reason"] = reason
|
|
250
|
+
return self._post(f"/cli/builders/{urllib.parse.quote(agent_id, safe='')}/resolve", body)
|
|
251
|
+
|
|
252
|
+
def stop_builder(self, agent_id: str) -> dict[str, Any]:
|
|
253
|
+
return self._post(f"/cli/builders/{urllib.parse.quote(agent_id, safe='')}/stop")
|
|
254
|
+
|
|
183
255
|
def list_releases(self, app: str, environment: str | None = None) -> dict[str, Any]:
|
|
184
256
|
return self._get("/cli/releases", {"app": app, "environment": environment})
|
|
185
257
|
|
|
@@ -238,35 +310,15 @@ class ApiClient:
|
|
|
238
310
|
def list_api_keys(self) -> dict[str, Any]:
|
|
239
311
|
return self._get("/cli/api-keys")
|
|
240
312
|
|
|
241
|
-
def create_api_key(self, label: str, service_account: str | None = None) -> dict[str, Any]:
|
|
242
|
-
return self._post("/cli/api-keys", {"label": label, "service_account": service_account})
|
|
243
|
-
|
|
244
|
-
def revoke_api_key(self, key_id: str) -> dict[str, Any]:
|
|
245
|
-
return self._post(f"/cli/api-keys/{urllib.parse.quote(key_id, safe='')}/revoke")
|
|
246
|
-
|
|
247
313
|
def list_service_accounts(self) -> dict[str, Any]:
|
|
248
314
|
return self._get("/cli/service-accounts")
|
|
249
315
|
|
|
250
|
-
def create_service_account(self, name: str) -> dict[str, Any]:
|
|
251
|
-
return self._post("/cli/service-accounts", {"name": name})
|
|
252
|
-
|
|
253
|
-
def delete_service_account(self, account_id: str) -> dict[str, Any]:
|
|
254
|
-
return self._delete(f"/cli/service-accounts/{urllib.parse.quote(account_id, safe='')}")
|
|
255
|
-
|
|
256
316
|
def list_members(self) -> dict[str, Any]:
|
|
257
317
|
return self._get("/cli/members")
|
|
258
318
|
|
|
259
319
|
def get_member(self, actor_id: str) -> dict[str, Any]:
|
|
260
320
|
return self._get(f"/cli/members/{urllib.parse.quote(actor_id, safe='')}")
|
|
261
321
|
|
|
262
|
-
def invite_member(self, email: str, role: str) -> dict[str, Any]:
|
|
263
|
-
return self._post("/cli/members/invite", {"email": email, "role": role})
|
|
264
|
-
|
|
265
|
-
def set_member_role(self, actor_id: str, role: str) -> dict[str, Any]:
|
|
266
|
-
return self._post(
|
|
267
|
-
f"/cli/members/{urllib.parse.quote(actor_id, safe='')}/role", {"role": role}
|
|
268
|
-
)
|
|
269
|
-
|
|
270
322
|
def list_activity(self, params: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
271
323
|
return self._get("/cli/activity", params)
|
|
272
324
|
|
|
@@ -279,74 +331,27 @@ class ApiClient:
|
|
|
279
331
|
def get_issue(self, issue: str) -> dict[str, Any]:
|
|
280
332
|
return self._get(f"/cli/issues/{urllib.parse.quote(issue, safe='')}")
|
|
281
333
|
|
|
282
|
-
def set_issue_status(self, issue: str, status: str, note: str | None = None) -> dict[str, Any]:
|
|
283
|
-
return self._post(
|
|
284
|
-
f"/cli/issues/{urllib.parse.quote(issue, safe='')}/status",
|
|
285
|
-
{"status": status, "note": note},
|
|
286
|
-
)
|
|
287
|
-
|
|
288
334
|
def list_alert_rules(self) -> dict[str, Any]:
|
|
289
335
|
return self._get("/cli/alert-rules")
|
|
290
336
|
|
|
291
337
|
def get_alert_rule(self, rule_id: str) -> dict[str, Any]:
|
|
292
338
|
return self._get(f"/cli/alert-rules/{urllib.parse.quote(rule_id, safe='')}")
|
|
293
339
|
|
|
294
|
-
def create_alert_rule(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
295
|
-
return self._post("/cli/alert-rules", payload)
|
|
296
|
-
|
|
297
|
-
def update_alert_rule(self, rule_id: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
298
|
-
return self._post(f"/cli/alert-rules/{urllib.parse.quote(rule_id, safe='')}", payload)
|
|
299
|
-
|
|
300
|
-
def delete_alert_rule(self, rule_id: str) -> dict[str, Any]:
|
|
301
|
-
return self._delete(f"/cli/alert-rules/{urllib.parse.quote(rule_id, safe='')}")
|
|
302
|
-
|
|
303
340
|
def list_alert_channels(self) -> dict[str, Any]:
|
|
304
341
|
return self._get("/cli/alert-channels")
|
|
305
342
|
|
|
306
|
-
def create_alert_channel(self, kind: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
307
|
-
return self._post(f"/cli/alert-channels/{urllib.parse.quote(kind, safe='')}", payload)
|
|
308
|
-
|
|
309
|
-
def set_alert_channel_enabled(self, channel_id: str, enabled: bool) -> dict[str, Any]:
|
|
310
|
-
return self._post(
|
|
311
|
-
f"/cli/alert-channels/{urllib.parse.quote(channel_id, safe='')}/enabled",
|
|
312
|
-
{"enabled": enabled},
|
|
313
|
-
)
|
|
314
|
-
|
|
315
|
-
def delete_alert_channel(self, channel_id: str) -> dict[str, Any]:
|
|
316
|
-
return self._delete(f"/cli/alert-channels/{urllib.parse.quote(channel_id, safe='')}")
|
|
317
|
-
|
|
318
343
|
def list_git_installations(self) -> dict[str, Any]:
|
|
319
344
|
return self._get("/cli/git-installations")
|
|
320
345
|
|
|
321
346
|
def list_git_repos(self) -> dict[str, Any]:
|
|
322
347
|
return self._get("/cli/git-repos")
|
|
323
348
|
|
|
324
|
-
def link_app(self, app: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
325
|
-
return self._post(f"/cli/apps/{urllib.parse.quote(app, safe='')}/git", payload)
|
|
326
|
-
|
|
327
|
-
def unlink_app(self, app: str) -> dict[str, Any]:
|
|
328
|
-
return self._delete(f"/cli/apps/{urllib.parse.quote(app, safe='')}/git")
|
|
329
|
-
|
|
330
|
-
def deploy_app_from_git(self, app: str, branch: str | None = None) -> dict[str, Any]:
|
|
331
|
-
body: dict[str, Any] = {}
|
|
332
|
-
if branch is not None:
|
|
333
|
-
body["branch"] = branch
|
|
334
|
-
return self._post(f"/cli/apps/{urllib.parse.quote(app, safe='')}/deploy", body)
|
|
335
|
-
|
|
336
349
|
def list_marketplace_apps(self, params: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
337
350
|
return self._get("/cli/marketplace-apps", params)
|
|
338
351
|
|
|
339
352
|
def get_marketplace_app(self, slug: str) -> dict[str, Any]:
|
|
340
353
|
return self._get(f"/cli/marketplace-apps/{urllib.parse.quote(slug, safe='')}")
|
|
341
354
|
|
|
342
|
-
def install_app(self, slug: str, environment: str | None = None) -> dict[str, Any]:
|
|
343
|
-
body: dict[str, Any] = {}
|
|
344
|
-
if environment is not None:
|
|
345
|
-
body["environment"] = environment
|
|
346
|
-
return self._post(
|
|
347
|
-
f"/cli/marketplace-apps/{urllib.parse.quote(slug, safe='')}/install", body
|
|
348
|
-
)
|
|
349
|
-
|
|
350
355
|
def app_status(self, app: str, environment: str | None = None) -> dict[str, Any]:
|
|
351
356
|
"""Fetch the detailed lifecycle status for a single app.
|
|
352
357
|
|
|
@@ -378,19 +383,19 @@ class ApiClient:
|
|
|
378
383
|
return self._json("POST", f"/app/{urllib.parse.quote(app, safe='')}/scale", body)
|
|
379
384
|
|
|
380
385
|
def delete_app(self, app: str, environment: str | None = None) -> dict[str, Any]:
|
|
381
|
-
"""
|
|
386
|
+
"""Disable an app deployment.
|
|
382
387
|
|
|
383
388
|
Hits ``DELETE /app/<slug>``, which drives the deployment to ``paused``
|
|
384
389
|
(and the app to ``disabled``) so the control-plane materializer stops
|
|
385
390
|
recreating the app's runtime StatefulSet. Reversible with
|
|
386
391
|
:meth:`activate_app`. The org is derived from the API key. Returns
|
|
387
|
-
``{"app": {...}}`` with the
|
|
392
|
+
``{"app": {...}}`` with the disabled deployment.
|
|
388
393
|
"""
|
|
389
394
|
body: dict[str, Any] | None = {"environment": environment} if environment else None
|
|
390
395
|
return self._json("DELETE", f"/app/{urllib.parse.quote(app, safe='')}", body)
|
|
391
396
|
|
|
392
397
|
def activate_app(self, app: str, environment: str | None = None) -> dict[str, Any]:
|
|
393
|
-
"""
|
|
398
|
+
"""Enable a previously disabled app deployment.
|
|
394
399
|
|
|
395
400
|
Hits ``POST /app/<slug>/activate`` (status back to ``active``), so the
|
|
396
401
|
materializer resumes recreating the app's StatefulSet. The org is
|
|
@@ -399,6 +404,15 @@ class ApiClient:
|
|
|
399
404
|
body: dict[str, Any] | None = {"environment": environment} if environment else None
|
|
400
405
|
return self._json("POST", f"/app/{urllib.parse.quote(app, safe='')}/activate", body)
|
|
401
406
|
|
|
407
|
+
def purge_app(self, app: str) -> dict[str, Any]:
|
|
408
|
+
"""Permanently purge orphaned app metadata for a partially-deleted app.
|
|
409
|
+
|
|
410
|
+
Hits ``DELETE /cli/apps/<slug>/purge``. The backend scopes the purge to
|
|
411
|
+
the API key's org and refuses to act while deployment rows still exist,
|
|
412
|
+
so ordinary deployed apps must use lifecycle commands first.
|
|
413
|
+
"""
|
|
414
|
+
return self._delete(f"/cli/apps/{urllib.parse.quote(app, safe='')}/purge")
|
|
415
|
+
|
|
402
416
|
def org_config(self) -> dict[str, Any]:
|
|
403
417
|
return self._json("GET", "/org-config", None)
|
|
404
418
|
|
|
@@ -415,6 +429,73 @@ class ApiClient:
|
|
|
415
429
|
"""
|
|
416
430
|
return self._json("POST", "/org-config/auth-connections", payload)
|
|
417
431
|
|
|
432
|
+
# --- MCP-parity reads -------------------------------------------------- #
|
|
433
|
+
# The uRun MCP reads GPU metrics straight from cube and runtime/deploy state
|
|
434
|
+
# straight from Supabase PostgREST with privileged server-only credentials
|
|
435
|
+
# (cube API secret / service-role key). The CLI cannot and must not hold
|
|
436
|
+
# those, so — exactly like the existing query_logs -> /cli/logs and
|
|
437
|
+
# list_failure_groups -> /cli/failure-groups mirrors — it routes the SAME
|
|
438
|
+
# semantics through the org-API-key control-plane edge surface (/cli/...).
|
|
439
|
+
|
|
440
|
+
def get_gpu_metrics(self, app: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
441
|
+
return self._get(f"/cli/gpu-metrics/{urllib.parse.quote(app, safe='')}", params)
|
|
442
|
+
|
|
443
|
+
def get_runtime_health(self, app: str, function_name: str | None = None) -> dict[str, Any]:
|
|
444
|
+
return self._get(
|
|
445
|
+
f"/cli/runtime-health/{urllib.parse.quote(app, safe='')}",
|
|
446
|
+
{"function": function_name},
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
def get_deploy_status(self, app: str, environment: str | None = None) -> dict[str, Any]:
|
|
450
|
+
return self._get(
|
|
451
|
+
f"/cli/deploy-status/{urllib.parse.quote(app, safe='')}",
|
|
452
|
+
{"environment": environment},
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
# --- Platform feedback ------------------------------------------------- #
|
|
456
|
+
def raise_feedback(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
457
|
+
return self._post("/cli/feedback", payload)
|
|
458
|
+
|
|
459
|
+
def comment_feedback(self, feedback_id: str, body: str) -> dict[str, Any]:
|
|
460
|
+
return self._post(
|
|
461
|
+
f"/cli/feedback/{urllib.parse.quote(feedback_id, safe='')}/comments",
|
|
462
|
+
{"body": body},
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
def get_feedback(self, feedback_id: str) -> dict[str, Any]:
|
|
466
|
+
return self._get(f"/cli/feedback/{urllib.parse.quote(feedback_id, safe='')}")
|
|
467
|
+
|
|
468
|
+
def list_feedback(self) -> dict[str, Any]:
|
|
469
|
+
return self._get("/cli/feedback")
|
|
470
|
+
|
|
471
|
+
# --- Dev gate ---------------------------------------------------------- #
|
|
472
|
+
def run_dev_gate(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
473
|
+
return self._post("/cli/dev-gate", payload)
|
|
474
|
+
|
|
475
|
+
# --- Console: dashboards + view defaults ------------------------------- #
|
|
476
|
+
def create_dashboard(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
477
|
+
return self._post("/cli/dashboards", payload)
|
|
478
|
+
|
|
479
|
+
def save_dashboard_config(self, dashboard_ref: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
480
|
+
return self._post(
|
|
481
|
+
f"/cli/dashboards/{urllib.parse.quote(dashboard_ref, safe='')}/config", payload
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
def set_view_default(self, surface: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
485
|
+
return self._post(f"/cli/view-defaults/{urllib.parse.quote(surface, safe='')}", payload)
|
|
486
|
+
|
|
487
|
+
def clear_view_default(self, surface: str, scope: str) -> dict[str, Any]:
|
|
488
|
+
return self._delete(
|
|
489
|
+
f"/cli/view-defaults/{urllib.parse.quote(surface, safe='')}", {"scope": scope}
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
# --- Org config deltas ------------------------------------------------- #
|
|
493
|
+
def set_org_limits(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
494
|
+
return self._post("/org-config/limits", payload)
|
|
495
|
+
|
|
496
|
+
def remove_auth_connection(self, issuer: str) -> dict[str, Any]:
|
|
497
|
+
return self._delete("/org-config/auth-connections", {"issuer": issuer})
|
|
498
|
+
|
|
418
499
|
def _json(self, method: str, path: str, body: dict[str, Any] | None) -> dict[str, Any]:
|
|
419
500
|
data = (
|
|
420
501
|
None
|