urun-cli 0.5.32__tar.gz → 0.6.1__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.
Files changed (34) hide show
  1. {urun_cli-0.5.32 → urun_cli-0.6.1}/PKG-INFO +34 -22
  2. {urun_cli-0.5.32 → urun_cli-0.6.1}/README.md +33 -21
  3. urun_cli-0.6.1/hatch_build.py +145 -0
  4. {urun_cli-0.5.32 → urun_cli-0.6.1}/pyproject.toml +12 -1
  5. {urun_cli-0.5.32 → urun_cli-0.6.1}/src/urun/api.py +21 -10
  6. {urun_cli-0.5.32 → urun_cli-0.6.1}/src/urun/catalog.py +151 -6
  7. {urun_cli-0.5.32 → urun_cli-0.6.1}/src/urun/cli.py +1047 -557
  8. {urun_cli-0.5.32 → urun_cli-0.6.1}/src/urun/client_tokens.py +38 -16
  9. {urun_cli-0.5.32 → urun_cli-0.6.1}/src/urun/dev.py +225 -31
  10. {urun_cli-0.5.32 → urun_cli-0.6.1}/src/urun/devconsole.py +2 -2
  11. {urun_cli-0.5.32 → urun_cli-0.6.1}/src/urun/devstream.py +151 -8
  12. {urun_cli-0.5.32 → urun_cli-0.6.1}/src/urun/frontend_launch.py +344 -22
  13. {urun_cli-0.5.32 → urun_cli-0.6.1}/src/urun/launch_auth.py +63 -17
  14. {urun_cli-0.5.32 → urun_cli-0.6.1}/src/urun/nodes.py +4 -2
  15. urun_cli-0.5.32/hatch_build.py +0 -93
  16. {urun_cli-0.5.32 → urun_cli-0.6.1}/.gitignore +0 -0
  17. {urun_cli-0.5.32 → urun_cli-0.6.1}/CHANGELOG.md +0 -0
  18. {urun_cli-0.5.32 → urun_cli-0.6.1}/LICENSE +0 -0
  19. {urun_cli-0.5.32 → urun_cli-0.6.1}/SECURITY.md +0 -0
  20. {urun_cli-0.5.32 → urun_cli-0.6.1}/src/urun/__init__.py +0 -0
  21. {urun_cli-0.5.32 → urun_cli-0.6.1}/src/urun/_bin/README.md +0 -0
  22. {urun_cli-0.5.32 → urun_cli-0.6.1}/src/urun/buildlogs.py +0 -0
  23. {urun_cli-0.5.32 → urun_cli-0.6.1}/src/urun/config.py +0 -0
  24. {urun_cli-0.5.32 → urun_cli-0.6.1}/src/urun/deps.py +0 -0
  25. {urun_cli-0.5.32 → urun_cli-0.6.1}/src/urun/discovery.py +0 -0
  26. {urun_cli-0.5.32 → urun_cli-0.6.1}/src/urun/errors.py +0 -0
  27. {urun_cli-0.5.32 → urun_cli-0.6.1}/src/urun/fleet.py +0 -0
  28. {urun_cli-0.5.32 → urun_cli-0.6.1}/src/urun/manifest.py +0 -0
  29. {urun_cli-0.5.32 → urun_cli-0.6.1}/src/urun/openai_proxy_bin.py +0 -0
  30. {urun_cli-0.5.32 → urun_cli-0.6.1}/src/urun/repo_deps.py +0 -0
  31. {urun_cli-0.5.32 → urun_cli-0.6.1}/src/urun/retry.py +0 -0
  32. {urun_cli-0.5.32 → urun_cli-0.6.1}/src/urun/runtime_health.py +0 -0
  33. {urun_cli-0.5.32 → urun_cli-0.6.1}/src/urun/scratch.py +0 -0
  34. {urun_cli-0.5.32 → urun_cli-0.6.1}/src/urun/serve.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: urun-cli
3
- Version: 0.5.32
3
+ Version: 0.6.1
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
@@ -108,8 +108,8 @@ urun deploy prompt_canary/backend/app.py --name prompt-canary --timeout 900
108
108
  urun demo prompt-canary --examples-root ~/workspace/urun-examples
109
109
  ```
110
110
 
111
- The positional argument is the **deployed app slug** (as shown by `urun list
112
- apps`). Nothing is declared in a manifest — every value is discovered:
111
+ The positional argument is the **deployed app slug** (as shown by `urun app
112
+ list`). Nothing is declared in a manifest — every value is discovered:
113
113
 
114
114
  | value | where it comes from |
115
115
  | --- | --- |
@@ -193,7 +193,7 @@ row instead of rendering one.
193
193
  List every app deployed in your org and its current status:
194
194
 
195
195
  ```bash
196
- urun list apps
196
+ urun app list
197
197
  ```
198
198
 
199
199
  Sample output:
@@ -226,21 +226,22 @@ endpoint, which is in development.
226
226
 
227
227
  ## View logs
228
228
 
229
- Use `urun logs` for build logs or runtime logs scoped to an app, a function
229
+ Use `urun build logs`, `urun app logs`, `urun function logs`, `urun session logs`,
230
+ or `urun log list` for build logs or runtime logs scoped to an app, a function
230
231
  within an app, or a session:
231
232
 
232
233
  ```bash
233
- urun list logs
234
- urun logs build <build-id>
235
- urun logs app <app>
236
- urun logs function <app> <function>
237
- urun logs session <session>
234
+ urun log list
235
+ urun build logs <build-id>
236
+ urun app logs <app>
237
+ urun function logs <app> <function>
238
+ urun session logs <session>
238
239
  ```
239
240
 
240
241
  Runtime commands accept `--follow`/`-f` for live output and `--json` for JSON
241
- (one-shot) or NDJSON (followed) output. Use `urun list logs` without filters to
242
+ (one-shot) or NDJSON (followed) output. Use `urun log list` without filters to
242
243
  list all accessible logs, or add the same app, function, session, level, text,
243
- and time filters. `urun tail logs` remains available for the legacy follow
244
+ and time filters. `urun log tail` remains available for the legacy follow
244
245
  workflow. The current logs API does not filter by environment.
245
246
 
246
247
  ## Inspect sessions
@@ -249,10 +250,10 @@ List live and historical sessions in your org. Newest sessions appear at the
249
250
  **bottom** of the table so the command works well with `tail`:
250
251
 
251
252
  ```bash
252
- urun list sessions
253
- urun list sessions | tail -20
254
- urun list sessions --state failed
255
- urun list sessions --limit 500
253
+ urun session list
254
+ urun session list | tail -20
255
+ urun session list --state failed
256
+ urun session list --limit 500
256
257
  ```
257
258
 
258
259
  Sample output:
@@ -289,7 +290,7 @@ endpoint, which is in development.
289
290
  List the compute slices your org currently has provisioned:
290
291
 
291
292
  ```bash
292
- urun list compute
293
+ urun compute list
293
294
  ```
294
295
 
295
296
  Sample output:
@@ -309,8 +310,8 @@ reporting.
309
310
 
310
311
  Slices with no provisioned capacity are omitted, so this command answers
311
312
  "what is running right now". For the full deployment catalogue
312
- (including paused / failed / unprovisioned apps) use `urun list apps`;
313
- for historical or in-flight sessions use `urun list sessions`.
313
+ (including paused / failed / unprovisioned apps) use `urun app list`;
314
+ for historical or in-flight sessions use `urun session list`.
314
315
 
315
316
  Pass `--limit` to control how many rows are fetched (default 100).
316
317
 
@@ -320,10 +321,10 @@ endpoint, which is in development.
320
321
  ## Manage apps
321
322
 
322
323
  Manage the lifecycle of a single deployed app. The app is addressed by its
323
- slug (the name shown under `APP` in `urun list apps`); every operation is
324
+ slug (the name shown under `APP` in `urun app list`); every operation is
324
325
  org-scoped via your API key.
325
326
 
326
- Show detailed status for one app (the single-app complement to `list apps`):
327
+ Show detailed status for one app (the single-app complement to `app list`):
327
328
 
328
329
  ```bash
329
330
  urun app status lingbot
@@ -370,10 +371,21 @@ Enable a disabled app and bring it back online:
370
371
  urun app enable lingbot-handle
371
372
  ```
372
373
 
374
+ If `urun deploy` or `urun dev --attach` reports that a function was paused
375
+ after repeated startup failures, inspect its runtime logs before overriding
376
+ the safety pause. Then explicitly resume every paused function:
377
+
378
+ ```bash
379
+ urun app enable rapid-clips --force
380
+ ```
381
+
382
+ `--force` may restart a known-crashing function; it does not change the
383
+ platform's automatic pause policy.
384
+
373
385
  Stop a queued, starting, or live session:
374
386
 
375
387
  ```bash
376
- urun stop session sess_123 --yes
388
+ urun session stop sess_123 --yes
377
389
  ```
378
390
 
379
391
  All `app` subcommands accept `--environment` (default `prod`) and `--json`.
@@ -86,8 +86,8 @@ urun deploy prompt_canary/backend/app.py --name prompt-canary --timeout 900
86
86
  urun demo prompt-canary --examples-root ~/workspace/urun-examples
87
87
  ```
88
88
 
89
- The positional argument is the **deployed app slug** (as shown by `urun list
90
- apps`). Nothing is declared in a manifest — every value is discovered:
89
+ The positional argument is the **deployed app slug** (as shown by `urun app
90
+ list`). Nothing is declared in a manifest — every value is discovered:
91
91
 
92
92
  | value | where it comes from |
93
93
  | --- | --- |
@@ -171,7 +171,7 @@ row instead of rendering one.
171
171
  List every app deployed in your org and its current status:
172
172
 
173
173
  ```bash
174
- urun list apps
174
+ urun app list
175
175
  ```
176
176
 
177
177
  Sample output:
@@ -204,21 +204,22 @@ endpoint, which is in development.
204
204
 
205
205
  ## View logs
206
206
 
207
- Use `urun logs` for build logs or runtime logs scoped to an app, a function
207
+ Use `urun build logs`, `urun app logs`, `urun function logs`, `urun session logs`,
208
+ or `urun log list` for build logs or runtime logs scoped to an app, a function
208
209
  within an app, or a session:
209
210
 
210
211
  ```bash
211
- urun list logs
212
- urun logs build <build-id>
213
- urun logs app <app>
214
- urun logs function <app> <function>
215
- urun logs session <session>
212
+ urun log list
213
+ urun build logs <build-id>
214
+ urun app logs <app>
215
+ urun function logs <app> <function>
216
+ urun session logs <session>
216
217
  ```
217
218
 
218
219
  Runtime commands accept `--follow`/`-f` for live output and `--json` for JSON
219
- (one-shot) or NDJSON (followed) output. Use `urun list logs` without filters to
220
+ (one-shot) or NDJSON (followed) output. Use `urun log list` without filters to
220
221
  list all accessible logs, or add the same app, function, session, level, text,
221
- and time filters. `urun tail logs` remains available for the legacy follow
222
+ and time filters. `urun log tail` remains available for the legacy follow
222
223
  workflow. The current logs API does not filter by environment.
223
224
 
224
225
  ## Inspect sessions
@@ -227,10 +228,10 @@ List live and historical sessions in your org. Newest sessions appear at the
227
228
  **bottom** of the table so the command works well with `tail`:
228
229
 
229
230
  ```bash
230
- urun list sessions
231
- urun list sessions | tail -20
232
- urun list sessions --state failed
233
- urun list sessions --limit 500
231
+ urun session list
232
+ urun session list | tail -20
233
+ urun session list --state failed
234
+ urun session list --limit 500
234
235
  ```
235
236
 
236
237
  Sample output:
@@ -267,7 +268,7 @@ endpoint, which is in development.
267
268
  List the compute slices your org currently has provisioned:
268
269
 
269
270
  ```bash
270
- urun list compute
271
+ urun compute list
271
272
  ```
272
273
 
273
274
  Sample output:
@@ -287,8 +288,8 @@ reporting.
287
288
 
288
289
  Slices with no provisioned capacity are omitted, so this command answers
289
290
  "what is running right now". For the full deployment catalogue
290
- (including paused / failed / unprovisioned apps) use `urun list apps`;
291
- for historical or in-flight sessions use `urun list sessions`.
291
+ (including paused / failed / unprovisioned apps) use `urun app list`;
292
+ for historical or in-flight sessions use `urun session list`.
292
293
 
293
294
  Pass `--limit` to control how many rows are fetched (default 100).
294
295
 
@@ -298,10 +299,10 @@ endpoint, which is in development.
298
299
  ## Manage apps
299
300
 
300
301
  Manage the lifecycle of a single deployed app. The app is addressed by its
301
- slug (the name shown under `APP` in `urun list apps`); every operation is
302
+ slug (the name shown under `APP` in `urun app list`); every operation is
302
303
  org-scoped via your API key.
303
304
 
304
- Show detailed status for one app (the single-app complement to `list apps`):
305
+ Show detailed status for one app (the single-app complement to `app list`):
305
306
 
306
307
  ```bash
307
308
  urun app status lingbot
@@ -348,10 +349,21 @@ Enable a disabled app and bring it back online:
348
349
  urun app enable lingbot-handle
349
350
  ```
350
351
 
352
+ If `urun deploy` or `urun dev --attach` reports that a function was paused
353
+ after repeated startup failures, inspect its runtime logs before overriding
354
+ the safety pause. Then explicitly resume every paused function:
355
+
356
+ ```bash
357
+ urun app enable rapid-clips --force
358
+ ```
359
+
360
+ `--force` may restart a known-crashing function; it does not change the
361
+ platform's automatic pause policy.
362
+
351
363
  Stop a queued, starting, or live session:
352
364
 
353
365
  ```bash
354
- urun stop session sess_123 --yes
366
+ urun session stop sess_123 --yes
355
367
  ```
356
368
 
357
369
  All `app` subcommands accept `--environment` (default `prod`) and `--json`.
@@ -0,0 +1,145 @@
1
+ """Hatch build hook: platform-tag wheels that carry the vendored urun-openai binary.
2
+
3
+ The `urun compat` proxy binary (see ``src/urun/_bin/README.md``) is a native
4
+ executable, so a wheel that ships it must NOT claim ``py3-none-any`` — pip
5
+ would happily install a mac binary on linux. This hook makes the wheel tag
6
+ tell the truth, both ways:
7
+
8
+ - binary PRESENT at ``src/urun/_bin/urun-openai`` → ``pure_python = False``
9
+ and an explicit ``py3-none-<platform>`` tag. NOT ``infer_tag`` — that would
10
+ stamp ``cp3XX-cp3XX-<platform>`` and needlessly bind the (pure-Python) code
11
+ to the building interpreter's minor version; the only platform-specific
12
+ content is the data binary. A present-but-non-executable binary fails the
13
+ build loudly — shipping it would only fail later on user machines.
14
+ - binary ABSENT → nothing changes: the wheel stays pure ``py3-none-any`` and
15
+ ships without the binary (`urun compat` then resolves via its override/PATH
16
+ lanes or fails loud at runtime).
17
+
18
+ The platform component is derived explicitly, never a bare ``linux_<arch>``:
19
+
20
+ - Linux → ``manylinux_<major>_<minor>_<arch>`` from the GLIBC symbol versions
21
+ the VENDORED BINARY ITSELF actually references (``objdump -T``), never from
22
+ ``platform.libc_ver()`` (the build machine's installed glibc). PyPI rejects
23
+ bare ``linux_*`` uploads outright, and in the isolated build env
24
+ ``packaging.tags.platform_tags()`` can yield bare ``linux_x86_64`` first —
25
+ so the tag is constructed, not inferred.
26
+
27
+ INCIDENT (2026-08-25): tagging by the build machine's installed glibc
28
+ (``platform.libc_ver()`` on the ``ubuntu-24.04`` release runner → glibc
29
+ 2.39) produced wheels tagged ``manylinux_2_39_x86_64`` even though the
30
+ actual bun-compiled binary's highest referenced symbol is ``GLIBC_2.17``
31
+ (confirmed via ``objdump -T``) — a decade-wide, entirely incidental gap
32
+ between "what the build runner happened to have installed" and "what the
33
+ binary actually needs". ``manylinux_2_39`` is only installable on glibc
34
+ ≥2.39 (Ubuntu 24.04+, roughly 2024 onward), so pip/uv silently rejected the
35
+ platform wheel on every older-but-still-current distro (Ubuntu 20.04/22.04,
36
+ Debian 11/12, RHEL 8/9, ...) and fell back to the binary-less
37
+ ``py3-none-any`` wheel with ZERO signal at install time — `uv tool install
38
+ urun-cli` "succeeded" and `urun compat` failed at runtime claiming the
39
+ binary "is not installed". Reading the binary's OWN symbol requirements is
40
+ the only floor that isn't a lie in either direction.
41
+ - everything else (macOS) → the most specific ``packaging.tags`` platform tag
42
+ (e.g. ``macosx_15_0_arm64``).
43
+
44
+ The release workflow builds one wheel per platform with the binary vendored
45
+ (``scripts/vendor_openai_proxy.py --from-npm``) plus one pure wheel + sdist
46
+ without it, and verifies each with ``scripts/verify_wheel.py`` — a
47
+ ``py3-none-any`` wheel containing the binary fails the release.
48
+ """
49
+
50
+ from __future__ import annotations
51
+
52
+ import os
53
+ import re
54
+ import subprocess
55
+ import sysconfig
56
+ from pathlib import Path
57
+
58
+ from hatchling.builders.hooks.plugin.interface import BuildHookInterface
59
+
60
+ _GLIBC_VERSION_RE = re.compile(r"\bGLIBC_(\d+)\.(\d+)(?:\.\d+)?\b")
61
+
62
+
63
+ def parse_required_glibc_versions(objdump_dynsym_output: str) -> set[tuple[int, int]]:
64
+ """``{(major, minor), ...}`` GLIBC symbol versions referenced in ``objdump
65
+ -T`` text — pure string parsing, no filesystem/subprocess access."""
66
+ return {
67
+ (int(m.group(1)), int(m.group(2)))
68
+ for m in _GLIBC_VERSION_RE.finditer(objdump_dynsym_output)
69
+ }
70
+
71
+
72
+ def required_glibc_symbol_versions(binary: Path) -> set[tuple[int, int]]:
73
+ """The GLIBC_x.y versions ``binary``'s own dynamic symbols reference.
74
+
75
+ Reads the binary directly instead of trusting the BUILD machine's
76
+ installed glibc (``platform.libc_ver()``) — a build machine can have a
77
+ far newer glibc than anything the compiled binary actually links
78
+ against, and tagging by the build machine rather than the binary's real
79
+ floor silently locks the platform wheel out of most real-world Linux
80
+ installs (see the module docstring's INCIDENT)."""
81
+ try:
82
+ result = subprocess.run(
83
+ ["objdump", "-T", str(binary)], capture_output=True, text=True, check=False
84
+ )
85
+ except FileNotFoundError as exc:
86
+ raise RuntimeError(
87
+ "objdump (binutils) is not on PATH — required to read the vendored "
88
+ "binary's real glibc floor; refusing to guess one"
89
+ ) from exc
90
+ if result.returncode != 0:
91
+ raise RuntimeError(
92
+ f"objdump -T {binary} failed (rc={result.returncode}): {result.stderr.strip()!r} — "
93
+ "cannot verify the binary's real glibc floor; refusing to guess one"
94
+ )
95
+ return parse_required_glibc_versions(result.stdout)
96
+
97
+
98
+ def manylinux_tag(normalized_platform: str, glibc_versions: set[tuple[int, int]]) -> str:
99
+ """``manylinux_<major>_<minor>_<arch>`` from the binary's OWN required
100
+ GLIBC_x.y symbol versions — loud when none were found (statically linked
101
+ or unparseable; mislabeling in either direction would be a lie)."""
102
+ if not glibc_versions:
103
+ raise RuntimeError(
104
+ "the vendored binary declares no GLIBC_x.y symbol version requirements "
105
+ "(statically linked, or objdump output did not parse) — refusing to guess "
106
+ "a manylinux floor"
107
+ )
108
+ major, minor = max(glibc_versions)
109
+ arch = normalized_platform.removeprefix("linux_")
110
+ if not arch:
111
+ raise RuntimeError(
112
+ f"cannot derive a manylinux tag: no architecture in platform {normalized_platform!r}"
113
+ )
114
+ return f"manylinux_{major}_{minor}_{arch}"
115
+
116
+
117
+ def wheel_platform_tag(binary: Path) -> str:
118
+ """The platform component for a wheel carrying ``binary`` on THIS machine."""
119
+ normalized = sysconfig.get_platform().replace("-", "_").replace(".", "_")
120
+ if normalized.startswith("linux_"):
121
+ return manylinux_tag(normalized, required_glibc_symbol_versions(binary))
122
+ # macOS (and anything else): packaging's most specific tag, e.g.
123
+ # macosx_15_0_arm64. packaging is a hatchling dependency, always present.
124
+ from packaging.tags import platform_tags
125
+
126
+ tag = next(iter(platform_tags()))
127
+ if tag.startswith("linux_"): # belt & braces: never a bare linux tag
128
+ raise RuntimeError(f"refusing bare {tag!r} — PyPI rejects it")
129
+ return tag
130
+
131
+
132
+ class VendoredBinaryBuildHook(BuildHookInterface):
133
+ PLUGIN_NAME = "custom"
134
+
135
+ def initialize(self, version: str, build_data: dict) -> None:
136
+ binary = os.path.join(self.root, "src", "urun", "_bin", "urun-openai")
137
+ if not os.path.isfile(binary):
138
+ return # pure wheel, no binary — the honest py3-none-any case
139
+ if not os.access(binary, os.X_OK):
140
+ raise RuntimeError(
141
+ f"vendored binary {binary} exists but is not executable — "
142
+ "re-run scripts/vendor_openai_proxy.py instead of shipping a broken wheel"
143
+ )
144
+ build_data["pure_python"] = False
145
+ build_data["tag"] = f"py3-none-{wheel_platform_tag(Path(binary))}"
@@ -10,7 +10,17 @@ build-backend = "hatchling.build"
10
10
 
11
11
  [project]
12
12
  name = "urun-cli"
13
- version = "0.5.32"
13
+ # Cut the paused-function recovery fixes merged after v0.6.0.
14
+ #
15
+ # WHY THIS IS EDITED BY HAND. [tool.semantic_release] below is fully
16
+ # configured -- conventional commit parser, version_toml pointing here, tag
17
+ # format, changelog templates -- but NOTHING invokes it: `grep -rn
18
+ # semantic.release .github/workflows/` matches nothing. release.yaml only
19
+ # READS this value and skips publishing when the matching tag already exists,
20
+ # so every merge remains on the already-published version until this value is
21
+ # advanced. Wiring semantic-release into CI remains separate work; this patch
22
+ # bump is the minimal release unblock for the three post-v0.6.0 fixes.
23
+ version = "0.6.1"
14
24
  description = "End-user CLI for deploying apps to urun"
15
25
  readme = "README.md"
16
26
  requires-python = ">=3.11"
@@ -28,6 +38,7 @@ classifiers = [
28
38
  "Programming Language :: Python :: 3.12",
29
39
  "Topic :: Software Development :: Build Tools",
30
40
  ]
41
+ dependencies = []
31
42
 
32
43
  [project.urls]
33
44
  Homepage = "https://urun.sh"
@@ -191,7 +191,7 @@ class ApiClient:
191
191
 
192
192
  The server filters out slices with no provisioned GPU units, so the
193
193
  listing reflects what is actively running for the org rather than
194
- the full deployment catalogue (which is ``urun list apps``).
194
+ the full deployment catalogue (which is ``urun app list``).
195
195
 
196
196
  ``limit`` is passed through as a query param; this method does not
197
197
  impose a client-side maximum.
@@ -517,12 +517,6 @@ class ApiClient:
517
517
  def list_git_installations(self) -> dict[str, Any]:
518
518
  return self._get("/cli/git-installations")
519
519
 
520
- def list_marketplace_apps(self, params: dict[str, Any] | None = None) -> dict[str, Any]:
521
- return self._get("/cli/marketplace-apps", params)
522
-
523
- def get_marketplace_app(self, slug: str) -> dict[str, Any]:
524
- return self._get(f"/cli/marketplace-apps/{urllib.parse.quote(slug, safe='')}")
525
-
526
520
  def app_status(self, app: str, environment: str | None = None) -> dict[str, Any]:
527
521
  """Fetch the detailed lifecycle status for a single app.
528
522
 
@@ -613,8 +607,25 @@ class ApiClient:
613
607
  body: dict[str, Any] | None = {"environment": environment} if environment else None
614
608
  return self._json("POST", f"/app/{urllib.parse.quote(app, safe='')}/activate", body)
615
609
 
610
+ def set_function_paused(
611
+ self,
612
+ app: str,
613
+ function_name: str,
614
+ paused: bool,
615
+ environment: str | None = None,
616
+ ) -> dict[str, Any]:
617
+ """Pause or resume one function of an app.
618
+
619
+ Hits ``POST /app/<slug>/pause`` with the function name and explicit
620
+ pause state. The org is derived from the API key.
621
+ """
622
+ body: dict[str, Any] = {"paused": paused, "function_name": function_name}
623
+ if environment:
624
+ body["environment"] = environment
625
+ return self._json("POST", f"/app/{urllib.parse.quote(app, safe='')}/pause", body)
626
+
616
627
  def retire_app(self, app: str, environment: str | None = None) -> dict[str, Any]:
617
- """Retire an app through the CLI edge route (backs ``urun delete app``).
628
+ """Retire an app through the CLI edge route (backs ``urun app delete``).
618
629
 
619
630
  Hits ``DELETE /cli/apps/<slug>`` which calls the org-scoped
620
631
  ``urun_stage2_retire_app`` lifecycle RPC: the deployment is driven to
@@ -1106,7 +1117,7 @@ def poll_until_done(
1106
1117
  "The control plane kept answering, so this is not a poll failure: it "
1107
1118
  f"reported '{last_state}' for the whole {timeout:.0f}s budget. If the app "
1108
1119
  "is in fact already serving, the build-status document is wedged "
1109
- "server-side — check `urun logs build` for the real build outcome."
1120
+ "server-side — check `urun build logs` for the real build outcome."
1110
1121
  )
1111
1122
  time.sleep(_poll_sleep_interval(time.time() - poll_started, interval))
1112
1123
 
@@ -1143,7 +1154,7 @@ def _poll_streak_error(
1143
1154
  f"GET /deployment-status/{manifest_hash}: {describe_error(exc)}. "
1144
1155
  f"Failing now rather than re-polling a broken endpoint for the rest of the "
1145
1156
  f"wait budget; {staleness}. The build itself may still be running "
1146
- f"server-side — check `urun logs build` for the real outcome."
1157
+ f"server-side — check `urun build logs` for the real outcome."
1147
1158
  )
1148
1159
 
1149
1160