Metadata-Version: 2.4
Name: urun-cli
Version: 0.5.7
Summary: End-user CLI for deploying apps to urun
Project-URL: Homepage, https://urun.sh
Project-URL: Repository, https://github.com/urun-sh/urun-cli
Project-URL: Issues, https://github.com/urun-sh/urun-cli/issues
Author: urun
License-Expression: MIT
License-File: LICENSE
Keywords: cli,deploy,urun
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Build Tools
Requires-Python: >=3.11
Description-Content-Type: text/markdown

# urun CLI

Deploy Python apps to urun from your terminal.

[![PyPI](https://img.shields.io/pypi/v/urun-cli.svg)](https://pypi.org/project/urun-cli/)
[![Python](https://img.shields.io/pypi/pyversions/urun-cli.svg)](https://pypi.org/project/urun-cli/)

## Install

```bash
uv tool install urun-cli
# or
pip install urun-cli
```

The package installs the `urun` command:

```bash
urun --version
```

For one-off `uvx` usage:

```bash
uvx --from urun-cli urun --version
# or the package-matching command alias
uvx urun-cli --version
```

## Quick start

Save an org-scoped deploy API key locally with `urun login`:

```bash
urun login
```

If `URUN_API_KEY` is not already set, `urun login` opens the urun console API
keys page, prompts you to paste the generated `urun_sk_...` key, verifies it
with the urun API, and stores credentials for later commands.

If you already have a key, you can pass it directly:

```bash
urun login --api-key urun_sk_<secret>
```

For CI or one-off commands, you can still use the environment variable:

```bash
export URUN_API_KEY=urun_sk_<secret>
```

Create `app.py`:

```python
import urun
from urun import App

app = App("hello-h100")


@app.function(gpus="h100:1")
def hello(ctx: urun.Context):
    print(f"running on {ctx.device}")
    return {"device": str(ctx.device)}
```

Run it:

```bash
urun run app.py
```

In this release, `urun run` uses the same deploy pipeline as `urun deploy`.
`deploy` remains available as the lower-level command while the full
deploy/run/monitor workflow is being built.

### Launch an official browser demo

For official `urun-examples` apps, the shortest browser stream path is:

```bash
cd ~/workspace/urun-examples
urun deploy prompt_canary/backend/app.py --name prompt-canary --timeout 900
urun demo prompt-canary
```

`urun demo` reads `urun-demo-manifest.json` from the examples checkout and
supports manifest entries with `demo.supported=true`. It uses your stored API
key server-side to request a short-lived browser JWT, sets the frontend's
manifest-declared `NEXT_PUBLIC_*` environment variables, starts the local Next.js
frontend, and opens it in your browser.

Use the manifest slug as the positional argument. If you deployed an official
example under a custom app name, pass it explicitly:

```bash
urun demo matrix-game-3 --app test-matrix-game
```

Override the examples checkout with `--examples-root` or `URUN_EXAMPLES_ROOT`;
override the session API with `--session-url` or `URUN_SESSION_BASE_URL`.

## Serve a model from the catalog

`urun serve` deploys a model straight from the urun model catalog — no app code
required. It is `urun deploy` with a serve app templated from the resolved
catalog row (engine, HF repo, GPU, engine args).

List the enumerable model matrix (models, variants, the GPU each fits, engine
shape, and whether the placement is dev-testable or prod-only):

```bash
urun serve catalog
urun serve catalog --json
```

Serve a model. With no variant the model's default (first) variant is used; with
no `--gpu` the variant's first placement is used:

```bash
urun serve qwen-coder                    # default variant + first placement
urun serve glm-5.2:UD-IQ2_M              # explicit <id>:<variant>
urun serve qwen-coder-480b:fp8 --gpu b200:8
```

The catalog is anon-readable reference data published by a urun-infra migration.
Reads go over PostgREST on the same control-plane host as the API URL; supply the
public Supabase anon key via `--anon-key` or `URUN_CATALOG_ANON_KEY`
(`URUN_SUPABASE_ANON_KEY` is also honored). Override the PostgREST base with
`--catalog-url` / `URUN_CATALOG_URL` if needed.

By default `urun serve <id>` renders a templated serve app and deploys it. If you
maintain your own `_serve` app, point `--entrypoint` at it; the CLI deploys that
app with `URUN_SERVE_CONFIG` and `URUN_SERVE_CATALOG_ROW` set to the resolved
row instead of rendering one.

## Inspect apps

List every app deployed in your org and its current status:

```bash
urun list apps
```

Sample output:

```text
APP                    FUNCTION        COMPUTE  STATUS         RELEASE       DETAIL
causal-forcing-stream  generate_video  h100:1   ready          fa61f31b0961  0/1 GPU units in use
queued-app             warmup          a10:1    provisioning   000000000000  building
```

The STATUS column is one of `provisioning`, `ready`, `pending`, `paused`, or
`failed`. It is derived from three backend signals reporting on sequential
lifecycle phases:

| Build (S3 status)      | Promotion (`app_deployments`) | Capacity (`function_ready`) | STATUS         |
| ---------------------- | ----------------------------- | --------------------------- | -------------- |
| `queued` \| `building` | (no row yet)                  | -                           | `provisioning` |
| `failed`               | (no row yet)                  | -                           | `failed`       |
| `ready`                | `active`                      | `false`                     | `pending`      |
| `ready`                | `active`                      | `true`                      | `ready`        |
| `ready`                | `paused`                      | (irrelevant)                | `paused`       |
| `ready`                | `failed`                      | (irrelevant)                | `failed`       |

The DETAIL column carries the disambiguating signal (raw build state, error
message, `ready_reason`, or in-use GPU counts). Pass `--json` for the raw
payload.

This command is **experimental** and requires the server-side `GET /apps`
endpoint, which is in development.

## Inspect sessions

List live and historical sessions in your org. Newest sessions appear at the
**bottom** of the table so the command works well with `tail`:

```bash
urun list sessions
urun list sessions | tail -20
urun list sessions --state failed
urun list sessions --limit 500
```

Sample output:

```text
ID            APP                FUNCTION        SHAPE    STARTED               DURATION  STATE       DETAIL
2cc8a91f4b3d  helios             world_gen       h100:4   2026-06-02 10:55 UTC       44s  failed      no_capacity
3f0017daee01  helios             world_gen       h100:1   2026-06-02 11:08 UTC    18m43s  completed   client_disconnect
4a1c886e2d0a  causal-forcing-…   generate_video  h100:1   2026-06-02 14:21 UTC     3m12s  live        -
```

The STATE column maps the raw backend status to a user-friendly label:

| Backend status | STATE       |
| -------------- | ----------- |
| `allocated`    | `starting`  |
| `connected`    | `live`      |
| `closed`       | `completed` |
| `failed`       | `failed`    |
| `cancelled`    | `cancelled` |

DURATION is computed from `allocated_at` to `closed_at` for terminal sessions,
or `allocated_at` to now for live ones. DETAIL carries `close_reason` when
present. Pass `--json` for the raw payload (full IDs, ISO timestamps, all
fields).

Pass `--limit` to control how many rows are fetched (default 100).

This command is **experimental** and requires the server-side `GET /sessions`
endpoint, which is in development.

## Inspect active compute

List the compute slices your org currently has provisioned:

```bash
urun list compute
```

Sample output:

```text
APP                    FUNCTION        SHAPE   INSTANCES  GPU UNITS  SESSIONS  AGE
causal-forcing-stream  generate_video  h100:1  1/2        1/2        1         12s
helios                 world_gen       h100:4  0/1        0/4        0         3m
```

Each row is one actively provisioned `(app, function, compute_shape)`
slice. `INSTANCES` and `GPU UNITS` show `<allocated>/<provisioned>` — a
row with `0/1` is an idle warm runtime with no active sessions on it.
`SESSIONS` is the live session count. `AGE` is how stale the capacity
snapshot is; very old ages may indicate the runtime is no longer
reporting.

Slices with no provisioned capacity are omitted, so this command answers
"what is running right now". For the full deployment catalogue
(including paused / failed / unprovisioned apps) use `urun list apps`;
for historical or in-flight sessions use `urun list sessions`.

Pass `--limit` to control how many rows are fetched (default 100).

This command is **experimental** and requires the server-side `GET /compute`
endpoint, which is in development.

## Manage apps

Manage the lifecycle of a single deployed app. The app is addressed by its
slug (the name shown under `APP` in `urun list apps`); every operation is
org-scoped via your API key.

Show detailed status for one app (the single-app complement to `list apps`):

```bash
urun app status lingbot
```

```text
            App: lingbot
           Name: LingBot
    Environment: prod
     App status: active
     Deployment: active
Desired replicas: 2
       Function: handle_lingbot_runtime
        Compute: b200:4
            GPU: 4 x b200
        Release: 1c6d6287abcd
  Live sessions: 1
```

Scale an app's runtime replica count (the backend's scaling knob; the
control plane turns it into the runtime StatefulSet replica count):

```bash
urun app scale lingbot --replicas 3
urun app scale lingbot --replicas 0   # drain to zero without retiring
```

GPU count and compute shape are fixed at deploy time per release (set via
`@app.function`), so `scale` intentionally exposes only `--replicas`.

Disable an app so the control plane stops running it (drives the deployment
to `paused` and the app to `disabled`, so the materializer stops recreating
its runtime). This is the clean, reversible, API-driven alternative to a
manual database edit:

```bash
urun app disable lingbot-handle        # prompts for confirmation
urun app disable lingbot-handle --yes  # skip confirmation
```

Enable a disabled app and bring it back online:

```bash
urun app enable lingbot-handle
```

Stop a queued, starting, or live session:

```bash
urun stop session sess_123 --yes
```

All `app` subcommands accept `--environment` (default `prod`) and `--json`.
These commands are **experimental** and require the server-side `app`
lifecycle endpoint, which is in development.

## What gets deployed

`urun deploy` creates a source manifest from your Python entrypoint:

| Entrypoint | Included source |
| --- | --- |
| `urun deploy app.py` | `app.py` and local Python files it imports |

Dependencies are declared in your urun app code. Project-level files such as
`pyproject.toml` and `requirements.txt` are not uploaded as dependency
declarations by the CLI.

Generated/cache content such as `.git`, dotfiles, `__pycache__`, and `.pyc`
files is excluded. Add `.urunignore` to exclude additional paths.

Non-Python assets such as templates, static files, and data files are not
auto-included yet.

## Common options

Shared by `run` and `deploy`:

| Option | Description |
| --- | --- |
| `--name` | Override the derived app name. |
| `--api-url` | Override the API URL; defaults to `URUN_API_URL`, saved login credentials, or `https://api.urun.sh/v1`. |
| `--api-key` | Deploy API key; defaults to `URUN_API_KEY` or saved login credentials. |
| `--no-wait` | Finalize but do not poll for readiness. |
| `--poll-interval`, `--timeout` | Control readiness polling. |

## Troubleshooting

| Error | Fix |
| --- | --- |
| `missing API key` | Run `urun login`, set `URUN_API_KEY`, or pass `--api-key`. |
| `invalid API key format` | Use `urun_<32 lowercase hex chars>`. |
| `entrypoint not found` | Run from the project root or pass the entrypoint path. |
| `path is outside the project root` | Move the file under the project before deploying. |
| Expected files are missing | Import local Python files from `app.py`; non-Python assets are not auto-included yet. |

## Development

Contributing and test instructions are in [CONTRIBUTING.md](CONTRIBUTING.md).

## License

MIT.

## Development environment

This repo has a Nix/direnv/devcontainer baseline:

```bash
direnv allow
just sync
just check
```

Use VS Code Dev Containers to open the repository with the same toolchain in a container. Copy `devcontainer.env.example` to `.devcontainer.env` if you need to pass local git identity or other non-secret development settings into the container.
