urun-cli 0.5.3__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.3 → urun_cli-0.5.4}/CHANGELOG.md +56 -14
- {urun_cli-0.5.3 → urun_cli-0.5.4}/PKG-INFO +30 -6
- {urun_cli-0.5.3 → urun_cli-0.5.4}/README.md +29 -5
- {urun_cli-0.5.3 → urun_cli-0.5.4}/pyproject.toml +1 -1
- {urun_cli-0.5.3 → urun_cli-0.5.4}/src/urun/api.py +143 -76
- {urun_cli-0.5.3 → urun_cli-0.5.4}/src/urun/catalog.py +315 -26
- {urun_cli-0.5.3 → urun_cli-0.5.4}/src/urun/cli.py +765 -374
- {urun_cli-0.5.3 → urun_cli-0.5.4}/src/urun/config.py +29 -12
- {urun_cli-0.5.3 → 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.3 → urun_cli-0.5.4}/src/urun/serve.py +9 -7
- urun_cli-0.5.3/src/urun/discovery.py +0 -183
- {urun_cli-0.5.3 → urun_cli-0.5.4}/.gitignore +0 -0
- {urun_cli-0.5.3 → urun_cli-0.5.4}/LICENSE +0 -0
- {urun_cli-0.5.3 → urun_cli-0.5.4}/SECURITY.md +0 -0
- {urun_cli-0.5.3 → urun_cli-0.5.4}/src/urun/__init__.py +0 -0
- {urun_cli-0.5.3 → urun_cli-0.5.4}/src/urun/errors.py +0 -0
- {urun_cli-0.5.3 → urun_cli-0.5.4}/src/urun/manifest.py +0 -0
- {urun_cli-0.5.3 → urun_cli-0.5.4}/src/urun/retry.py +0 -0
|
@@ -1,17 +1,44 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.5.
|
|
3
|
+
## 0.5.4
|
|
4
4
|
|
|
5
|
-
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
`
|
|
11
|
-
|
|
12
|
-
|
|
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`.
|
|
13
21
|
|
|
14
|
-
|
|
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.
|
|
15
42
|
|
|
16
43
|
- Make `urun deploy` resilient to transient network blips. The manifest/blob
|
|
17
44
|
push and the "Waiting for build to complete" build-status poll now retry on
|
|
@@ -45,17 +72,32 @@
|
|
|
45
72
|
catalogue; `urun list sessions` for history). Supports `--limit`
|
|
46
73
|
(default 100) and `--json`. Reads from a new control-plane
|
|
47
74
|
endpoint `GET /compute` (server-side edge function not yet shipped).
|
|
48
|
-
- Add experimental `urun app status|scale|
|
|
75
|
+
- Add experimental `urun app status|scale|disable|enable` lifecycle commands
|
|
49
76
|
for managing a single deployed app (addressed by slug, org-scoped via the API
|
|
50
77
|
key). `app status` shows the build/deployment/capacity/live-session rollup;
|
|
51
78
|
`app scale --replicas N` sets the deployment's desired replica count (use 0 to
|
|
52
79
|
drain; GPU/shape stay fixed at deploy time, so only `--replicas` is exposed);
|
|
53
|
-
`app
|
|
80
|
+
`app disable` disables an app — driving it to `paused`/
|
|
54
81
|
`disabled` so the control-plane materializer stops recreating its runtime —
|
|
55
|
-
with an interactive confirmation (`--yes` to skip); `app
|
|
56
|
-
|
|
82
|
+
with an interactive confirmation (`--yes` to skip); `app enable` reverses it.
|
|
83
|
+
All accept `--environment` (default `prod`) and `--json`. Backed by a
|
|
57
84
|
new server-side `app` edge function (`GET /app/<slug>`,
|
|
58
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.
|
|
59
101
|
|
|
60
102
|
## 0.3.0
|
|
61
103
|
|
|
@@ -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
|
|
@@ -98,6 +98,24 @@ In this release, `urun run` uses the same deploy pipeline as `urun deploy`.
|
|
|
98
98
|
`deploy` remains available as the lower-level command while the full
|
|
99
99
|
deploy/run/monitor workflow is being built.
|
|
100
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
|
+
|
|
101
119
|
## Serve a model from the catalog
|
|
102
120
|
|
|
103
121
|
`urun serve` deploys a model straight from the urun model catalog — no app code
|
|
@@ -279,20 +297,26 @@ urun app scale lingbot --replicas 0 # drain to zero without retiring
|
|
|
279
297
|
GPU count and compute shape are fixed at deploy time per release (set via
|
|
280
298
|
`@app.function`), so `scale` intentionally exposes only `--replicas`.
|
|
281
299
|
|
|
282
|
-
|
|
300
|
+
Disable an app so the control plane stops running it (drives the deployment
|
|
283
301
|
to `paused` and the app to `disabled`, so the materializer stops recreating
|
|
284
302
|
its runtime). This is the clean, reversible, API-driven alternative to a
|
|
285
303
|
manual database edit:
|
|
286
304
|
|
|
287
305
|
```bash
|
|
288
|
-
urun app
|
|
289
|
-
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
|
|
290
314
|
```
|
|
291
315
|
|
|
292
|
-
|
|
316
|
+
Stop a queued, starting, or live session:
|
|
293
317
|
|
|
294
318
|
```bash
|
|
295
|
-
urun
|
|
319
|
+
urun stop session sess_123 --yes
|
|
296
320
|
```
|
|
297
321
|
|
|
298
322
|
All `app` subcommands accept `--environment` (default `prod`) and `--json`.
|
|
@@ -76,6 +76,24 @@ In this release, `urun run` uses the same deploy pipeline as `urun deploy`.
|
|
|
76
76
|
`deploy` remains available as the lower-level command while the full
|
|
77
77
|
deploy/run/monitor workflow is being built.
|
|
78
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
|
+
|
|
79
97
|
## Serve a model from the catalog
|
|
80
98
|
|
|
81
99
|
`urun serve` deploys a model straight from the urun model catalog — no app code
|
|
@@ -257,20 +275,26 @@ urun app scale lingbot --replicas 0 # drain to zero without retiring
|
|
|
257
275
|
GPU count and compute shape are fixed at deploy time per release (set via
|
|
258
276
|
`@app.function`), so `scale` intentionally exposes only `--replicas`.
|
|
259
277
|
|
|
260
|
-
|
|
278
|
+
Disable an app so the control plane stops running it (drives the deployment
|
|
261
279
|
to `paused` and the app to `disabled`, so the materializer stops recreating
|
|
262
280
|
its runtime). This is the clean, reversible, API-driven alternative to a
|
|
263
281
|
manual database edit:
|
|
264
282
|
|
|
265
283
|
```bash
|
|
266
|
-
urun app
|
|
267
|
-
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
|
|
268
292
|
```
|
|
269
293
|
|
|
270
|
-
|
|
294
|
+
Stop a queued, starting, or live session:
|
|
271
295
|
|
|
272
296
|
```bash
|
|
273
|
-
urun
|
|
297
|
+
urun stop session sess_123 --yes
|
|
274
298
|
```
|
|
275
299
|
|
|
276
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
|
|
|
@@ -185,6 +185,73 @@ class ApiClient:
|
|
|
185
185
|
f"/cli/builds/{urllib.parse.quote(manifest_hash_or_prefix, safe='')}/purge"
|
|
186
186
|
)
|
|
187
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
|
+
|
|
188
255
|
def list_releases(self, app: str, environment: str | None = None) -> dict[str, Any]:
|
|
189
256
|
return self._get("/cli/releases", {"app": app, "environment": environment})
|
|
190
257
|
|
|
@@ -243,35 +310,15 @@ class ApiClient:
|
|
|
243
310
|
def list_api_keys(self) -> dict[str, Any]:
|
|
244
311
|
return self._get("/cli/api-keys")
|
|
245
312
|
|
|
246
|
-
def create_api_key(self, label: str, service_account: str | None = None) -> dict[str, Any]:
|
|
247
|
-
return self._post("/cli/api-keys", {"label": label, "service_account": service_account})
|
|
248
|
-
|
|
249
|
-
def revoke_api_key(self, key_id: str) -> dict[str, Any]:
|
|
250
|
-
return self._post(f"/cli/api-keys/{urllib.parse.quote(key_id, safe='')}/revoke")
|
|
251
|
-
|
|
252
313
|
def list_service_accounts(self) -> dict[str, Any]:
|
|
253
314
|
return self._get("/cli/service-accounts")
|
|
254
315
|
|
|
255
|
-
def create_service_account(self, name: str) -> dict[str, Any]:
|
|
256
|
-
return self._post("/cli/service-accounts", {"name": name})
|
|
257
|
-
|
|
258
|
-
def delete_service_account(self, account_id: str) -> dict[str, Any]:
|
|
259
|
-
return self._delete(f"/cli/service-accounts/{urllib.parse.quote(account_id, safe='')}")
|
|
260
|
-
|
|
261
316
|
def list_members(self) -> dict[str, Any]:
|
|
262
317
|
return self._get("/cli/members")
|
|
263
318
|
|
|
264
319
|
def get_member(self, actor_id: str) -> dict[str, Any]:
|
|
265
320
|
return self._get(f"/cli/members/{urllib.parse.quote(actor_id, safe='')}")
|
|
266
321
|
|
|
267
|
-
def invite_member(self, email: str, role: str) -> dict[str, Any]:
|
|
268
|
-
return self._post("/cli/members/invite", {"email": email, "role": role})
|
|
269
|
-
|
|
270
|
-
def set_member_role(self, actor_id: str, role: str) -> dict[str, Any]:
|
|
271
|
-
return self._post(
|
|
272
|
-
f"/cli/members/{urllib.parse.quote(actor_id, safe='')}/role", {"role": role}
|
|
273
|
-
)
|
|
274
|
-
|
|
275
322
|
def list_activity(self, params: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
276
323
|
return self._get("/cli/activity", params)
|
|
277
324
|
|
|
@@ -284,74 +331,27 @@ class ApiClient:
|
|
|
284
331
|
def get_issue(self, issue: str) -> dict[str, Any]:
|
|
285
332
|
return self._get(f"/cli/issues/{urllib.parse.quote(issue, safe='')}")
|
|
286
333
|
|
|
287
|
-
def set_issue_status(self, issue: str, status: str, note: str | None = None) -> dict[str, Any]:
|
|
288
|
-
return self._post(
|
|
289
|
-
f"/cli/issues/{urllib.parse.quote(issue, safe='')}/status",
|
|
290
|
-
{"status": status, "note": note},
|
|
291
|
-
)
|
|
292
|
-
|
|
293
334
|
def list_alert_rules(self) -> dict[str, Any]:
|
|
294
335
|
return self._get("/cli/alert-rules")
|
|
295
336
|
|
|
296
337
|
def get_alert_rule(self, rule_id: str) -> dict[str, Any]:
|
|
297
338
|
return self._get(f"/cli/alert-rules/{urllib.parse.quote(rule_id, safe='')}")
|
|
298
339
|
|
|
299
|
-
def create_alert_rule(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
300
|
-
return self._post("/cli/alert-rules", payload)
|
|
301
|
-
|
|
302
|
-
def update_alert_rule(self, rule_id: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
303
|
-
return self._post(f"/cli/alert-rules/{urllib.parse.quote(rule_id, safe='')}", payload)
|
|
304
|
-
|
|
305
|
-
def delete_alert_rule(self, rule_id: str) -> dict[str, Any]:
|
|
306
|
-
return self._delete(f"/cli/alert-rules/{urllib.parse.quote(rule_id, safe='')}")
|
|
307
|
-
|
|
308
340
|
def list_alert_channels(self) -> dict[str, Any]:
|
|
309
341
|
return self._get("/cli/alert-channels")
|
|
310
342
|
|
|
311
|
-
def create_alert_channel(self, kind: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
312
|
-
return self._post(f"/cli/alert-channels/{urllib.parse.quote(kind, safe='')}", payload)
|
|
313
|
-
|
|
314
|
-
def set_alert_channel_enabled(self, channel_id: str, enabled: bool) -> dict[str, Any]:
|
|
315
|
-
return self._post(
|
|
316
|
-
f"/cli/alert-channels/{urllib.parse.quote(channel_id, safe='')}/enabled",
|
|
317
|
-
{"enabled": enabled},
|
|
318
|
-
)
|
|
319
|
-
|
|
320
|
-
def delete_alert_channel(self, channel_id: str) -> dict[str, Any]:
|
|
321
|
-
return self._delete(f"/cli/alert-channels/{urllib.parse.quote(channel_id, safe='')}")
|
|
322
|
-
|
|
323
343
|
def list_git_installations(self) -> dict[str, Any]:
|
|
324
344
|
return self._get("/cli/git-installations")
|
|
325
345
|
|
|
326
346
|
def list_git_repos(self) -> dict[str, Any]:
|
|
327
347
|
return self._get("/cli/git-repos")
|
|
328
348
|
|
|
329
|
-
def link_app(self, app: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
330
|
-
return self._post(f"/cli/apps/{urllib.parse.quote(app, safe='')}/git", payload)
|
|
331
|
-
|
|
332
|
-
def unlink_app(self, app: str) -> dict[str, Any]:
|
|
333
|
-
return self._delete(f"/cli/apps/{urllib.parse.quote(app, safe='')}/git")
|
|
334
|
-
|
|
335
|
-
def deploy_app_from_git(self, app: str, branch: str | None = None) -> dict[str, Any]:
|
|
336
|
-
body: dict[str, Any] = {}
|
|
337
|
-
if branch is not None:
|
|
338
|
-
body["branch"] = branch
|
|
339
|
-
return self._post(f"/cli/apps/{urllib.parse.quote(app, safe='')}/deploy", body)
|
|
340
|
-
|
|
341
349
|
def list_marketplace_apps(self, params: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
342
350
|
return self._get("/cli/marketplace-apps", params)
|
|
343
351
|
|
|
344
352
|
def get_marketplace_app(self, slug: str) -> dict[str, Any]:
|
|
345
353
|
return self._get(f"/cli/marketplace-apps/{urllib.parse.quote(slug, safe='')}")
|
|
346
354
|
|
|
347
|
-
def install_app(self, slug: str, environment: str | None = None) -> dict[str, Any]:
|
|
348
|
-
body: dict[str, Any] = {}
|
|
349
|
-
if environment is not None:
|
|
350
|
-
body["environment"] = environment
|
|
351
|
-
return self._post(
|
|
352
|
-
f"/cli/marketplace-apps/{urllib.parse.quote(slug, safe='')}/install", body
|
|
353
|
-
)
|
|
354
|
-
|
|
355
355
|
def app_status(self, app: str, environment: str | None = None) -> dict[str, Any]:
|
|
356
356
|
"""Fetch the detailed lifecycle status for a single app.
|
|
357
357
|
|
|
@@ -383,19 +383,19 @@ class ApiClient:
|
|
|
383
383
|
return self._json("POST", f"/app/{urllib.parse.quote(app, safe='')}/scale", body)
|
|
384
384
|
|
|
385
385
|
def delete_app(self, app: str, environment: str | None = None) -> dict[str, Any]:
|
|
386
|
-
"""
|
|
386
|
+
"""Disable an app deployment.
|
|
387
387
|
|
|
388
388
|
Hits ``DELETE /app/<slug>``, which drives the deployment to ``paused``
|
|
389
389
|
(and the app to ``disabled``) so the control-plane materializer stops
|
|
390
390
|
recreating the app's runtime StatefulSet. Reversible with
|
|
391
391
|
:meth:`activate_app`. The org is derived from the API key. Returns
|
|
392
|
-
``{"app": {...}}`` with the
|
|
392
|
+
``{"app": {...}}`` with the disabled deployment.
|
|
393
393
|
"""
|
|
394
394
|
body: dict[str, Any] | None = {"environment": environment} if environment else None
|
|
395
395
|
return self._json("DELETE", f"/app/{urllib.parse.quote(app, safe='')}", body)
|
|
396
396
|
|
|
397
397
|
def activate_app(self, app: str, environment: str | None = None) -> dict[str, Any]:
|
|
398
|
-
"""
|
|
398
|
+
"""Enable a previously disabled app deployment.
|
|
399
399
|
|
|
400
400
|
Hits ``POST /app/<slug>/activate`` (status back to ``active``), so the
|
|
401
401
|
materializer resumes recreating the app's StatefulSet. The org is
|
|
@@ -429,6 +429,73 @@ class ApiClient:
|
|
|
429
429
|
"""
|
|
430
430
|
return self._json("POST", "/org-config/auth-connections", payload)
|
|
431
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
|
+
|
|
432
499
|
def _json(self, method: str, path: str, body: dict[str, Any] | None) -> dict[str, Any]:
|
|
433
500
|
data = (
|
|
434
501
|
None
|