> ## Documentation Index
> Fetch the complete documentation index at: https://runinfra.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Download a package with the CLI

> Install the RunInfra CLI, approve a terminal from your browser, and pull an optimized model package your workspace owns. Covers headless GPU hosts, revoking a terminal, and resuming an interrupted download.

Optimized model packages are large. A kit can run to tens of gigabytes, and the
machine that needs it is usually a GPU host you reached over SSH, not the laptop
you are reading this on. The RunInfra CLI exists for that: you approve the
terminal once from your browser, then pull packages your workspace owns directly
onto the machine that will serve them.

<Note>
  The CLI never asks for your password and never asks you to copy an API key.
  Approving in the browser creates the credential, stores it in a file only your
  user account can read, and you revoke it from Settings whenever you want.
</Note>

## Before you start

<Columns cols={2}>
  <Card title="A terminal, and nothing else" icon="terminal">
    The standalone build needs no Node and no Python. If you would rather use a
    package manager you already have, `pip` and `npm` install the same client.
  </Card>

  <Card title="A role that can deploy" icon="shield-check">
    Connecting a terminal grants package downloads, so it needs a role in the
    workspace that can deploy. A viewer sees an explanation instead of an
    Approve button.
  </Card>
</Columns>

Your workspace also has to own the package you are pulling. Buy it from the
catalog first, then come back here.

## Install

Three ways in. They install the same client, so pick whichever matches the
machine you are standing on.

<Tabs>
  <Tab title="No Node, no Python">
    ```bash theme={"dark"}
    curl -fsSL https://raw.githubusercontent.com/RightNow-AI/runinfra-cli/main/install.sh | sh
    ```

    A single self-contained executable that carries its own runtime. This is
    usually the right answer on a GPU host, which is the machine most likely to
    be pulling forty gigabytes and least likely to have a toolchain on it.

    The installer checks the download against the release checksums, and checks
    those checksums against our signature, before it writes anything into place.
    It tells you the directory it used and prints the command to add it to your
    PATH rather than editing your shell profile behind your back.

    On Windows, in PowerShell:

    ```powershell theme={"dark"}
    irm https://raw.githubusercontent.com/RightNow-AI/runinfra-cli/main/install.ps1 | iex
    ```
  </Tab>

  <Tab title="Python">
    ```bash theme={"dark"}
    pip install runinfra-cli
    ```

    pip picks the build for your platform, so the same line works on Linux,
    macOS and Windows. The command it installs is still `runinfra`.

    The distribution is `runinfra-cli`, not `runinfra`. `runinfra` on PyPI is
    the inference SDK, a different product.
  </Tab>

  <Tab title="Node">
    ```bash theme={"dark"}
    npm install -g @runinfra/cli
    ```

    Needs Node 20 or newer. Zero runtime dependencies, so it installs inside a
    slim container and behind a registry mirror.
  </Tab>
</Tabs>

<Note>
  Whichever you choose, the CLI tells you when a newer version is out. It reads
  that from a response header on a request it was already making, so it never
  phones home, and it never updates itself. It prints the one command to run,
  correct for how you installed it.
</Note>

## The happy path

<Steps>
  <Step title="Connect the terminal">
    ```bash theme={"dark"}
    runinfra login
    ```

    The CLI prints a short code and a URL:

    ```text theme={"dark"}
    To connect this terminal:
      1. On any device, open  https://runinfra.ai/cli/authorize
      2. Type this code:      K7QM-3JXP
    The page will not fill the code in for you. If a link ever arrives with the code already in it, it did not come from RunInfra.
      Waiting for approval, 847s left
    ```

    The code is valid for 15 minutes.
  </Step>

  <Step title="Approve it in your browser">
    Open the URL on any device where you are signed in to RunInfra and type the
    code. You will see the account, the workspace, the terminal that asked
    (shown as `user@host`), the code you typed, and the single thing it grants:
    downloading optimized model packages the workspace already owns.

    Press **Approve**. The browser confirms, and the terminal continues on its
    own:

    ```text theme={"dark"}
    Connected to workspace 83e8372a-1c4d-4f7a-9d21-5b6e0a2c8f10.
      Credentials stored at /home/mona/.config/runinfra/credentials.json
      This access expires 2026-10-24T09:14:02.118Z.
      Revoke this terminal any time from Settings, API keys.
    ```

    Access lasts 90 days unless you revoke it sooner.
  </Step>

  <Step title="Pull the package">
    ```bash theme={"dark"}
    runinfra pull qwen3-6-27b --out /data/models
    ```

    ```text theme={"dark"}
    Downloading qwen3-6-27b-v4.kit (41.2 GB) into /data/models
      qwen3-6-27b-v4.kit  [########----------------]  33.4%  13.8 GB / 41.2 GB  92.4 MB/s  ETA 49m 12s
    Transferred 41.2 GB in 1h 14m.
      Checksum verified.
    Saved qwen3-6-27b-v4.kit.
    /data/models/qwen3-6-27b-v4.kit
    ```

    The final path is the only thing printed to standard output, so
    `MODEL=$(runinfra pull qwen3-6-27b)` captures the path and nothing else.
  </Step>
</Steps>

Use `--concurrency N` (1 to 16, default 8) to tune parallel connections, and
`--out DIR` to choose the destination. Without `--out` the file lands in the
current directory.

## On a headless GPU host

Nothing changes. The CLI does not try to open a browser over SSH, in CI, or on a
machine with no graphical display, so it goes straight to the code and tells you
why:

```text theme={"dark"}
mona@gpu-07:~$ runinfra login
Using the device flow because this is an SSH session.
To connect this terminal:
  1. On any device, open  https://runinfra.ai/cli/authorize
  2. Type this code:      T4WX-9BQH
```

Type that code into your laptop's browser and approve. The GPU host is connected
without a browser ever running on it.

`runinfra login --device` forces this path from any machine.

<Warning>
  You type the code, you never click a link that contains it. The approval page
  has no field that can be filled in from a URL. If you ever receive a link with
  a sign-in code already in it, it did not come from RunInfra, and approving it
  would connect somebody else's terminal to your workspace.
</Warning>

## Check what a machine is holding

```bash theme={"dark"}
runinfra whoami
```

```text theme={"dark"}
Workspace  83e8372a-1c4d-4f7a-9d21-5b6e0a2c8f10
Key        rp_k3n9 (rp_k...8fq2)
Grants     catalog:download
Endpoint   https://runinfra.ai
Connected  2026-07-26T09:14:02.118Z
Expires    2026-10-24T09:14:02.118Z (in 2159h 58m)
This is what this machine has stored. Access may have been revoked from Settings, API keys since it was granted.
```

This reads the local file. It does not ask the server, so a terminal revoked five
minutes ago still appears here and fails on its next command.

## Verify the CLI you downloaded

The installers already do this for you and refuse to install anything that does
not check out. This section is for when you want to confirm it yourself, or when
your policy requires a signature on anything that reaches a production host.

Every release publishes `SHA256SUMS` alongside the executables, and
`SHA256SUMS.sig`, an Ed25519 signature over those checksums.

```bash theme={"dark"}
curl -fsSLO https://github.com/RightNow-AI/runinfra-cli/releases/latest/download/SHA256SUMS
curl -fsSLO https://github.com/RightNow-AI/runinfra-cli/releases/latest/download/SHA256SUMS.sig
curl -fsSLO https://raw.githubusercontent.com/RightNow-AI/runinfra-cli/main/runinfra-release.pub

openssl pkeyutl -verify -pubin -inkey runinfra-release.pub \
  -rawin -in SHA256SUMS -sigfile SHA256SUMS.sig
sha256sum --ignore-missing -c SHA256SUMS
```

Confirm the key is ours before you trust it. Its fingerprint is:

```text theme={"dark"}
5b2c8f637c0cd00a61ec6f126e5a9493c022ef1ea5be70fdd3ef4adf55801532
```

```bash theme={"dark"}
openssl pkey -pubin -in runinfra-release.pub -outform DER \
  | tail -c 32 | sha256sum
```

<Note>
  A key you downloaded from the same place as the file it verifies proves
  nothing on its own, because whoever replaced one could replace both. That
  fingerprint is also pinned inside `install.sh` and published on the
  [npm page](https://www.npmjs.com/package/@runinfra/cli), neither of which
  serves the binaries.
</Note>

**What a good signature proves.** The file came out of the RunInfra release
pipeline and has not been altered since. That is what defeats an edited asset or
a hostile mirror.

**What it does not prove.** That the pipeline itself was not subverted. The
signing key lives in our release automation, so anyone who could tamper with
that automation could also sign. We would rather say this plainly than let a
green checkmark imply more than it means.

On macOS, `/usr/bin/openssl` is LibreSSL and has no `-rawin`, so the command
above needs a real OpenSSL, for example `brew install openssl@3`. Windows
PowerShell has no built-in Ed25519 at all, so `install.ps1` verifies the
checksum and tells you the signature was not checked rather than pretending
otherwise.

## Revoke a terminal

Two different actions, and it matters which one you need.

<AccordionGroup>
  <Accordion title="End a terminal's access for good">
    Open **Settings, API keys** in the dashboard. Every terminal you connected
    is listed as `RunInfra CLI: user@host`, with when it was created and when it
    was last used. Revoke the row.

    The next command that terminal runs is refused:

    ```text theme={"dark"}
    error: This terminal's access was revoked.
      Run `runinfra login` to connect it again.
    ```

    This is what to use for a lost laptop, a decommissioned GPU host, or a
    contractor who is finished. Revocation applies to the next request. A
    download URL already handed out stays usable for up to 30 minutes, so
    revoking stops new downloads rather than reaching into a transfer already in
    flight.
  </Accordion>

  <Accordion title="Sign out of the machine in front of you">
    ```bash theme={"dark"}
    runinfra logout
    ```

    ```text theme={"dark"}
    Removed this machine's credentials.
      The key rp_k3n9 (rp_k...8fq2) is still valid until 2026-10-24T09:14:02.118Z.
      To end its access now, revoke it in Settings, API keys on runinfra.ai.
    ```

    This deletes the local file only. It does not revoke anything, and it says
    so, because deleting a file on a machine you still control is not how you
    handle a machine you have lost.
  </Accordion>
</AccordionGroup>

Your role is re-checked on every command. Removing someone from the workspace, or
lowering their role below deploy, cuts off their connected terminals immediately
without touching any keys.

## When a download is interrupted

**Run the same command again.** That is the whole procedure. The CLI keeps the
bytes it already has and continues from there.

```text theme={"dark"}
^C
Stopping. Run the same command again to resume.
error: Download interrupted.
  Run the same command again to resume from the bytes already on disk.
```

```text theme={"dark"}
$ runinfra pull qwen3-6-27b --out /data/models
Resuming qwen3-6-27b-v4.kit: 13.8 GB of 41.2 GB already on disk.
  qwen3-6-27b-v4.kit  [##########--------------]  41.7%  17.2 GB / 41.2 GB  88.1 MB/s  ETA 46m 30s
```

A second Ctrl-C exits immediately. Dropped connections, expired download URLs
and slow links are all handled inside a single run and need no action from you.

While a download is in progress you will see two files next to the destination:
`<name>.part` holds the bytes, and `<name>.part.json` records which parts are
safely written. Both disappear when the download finishes. The finished file only
ever appears under its real name once the bytes are complete and verified, so
anything watching that directory can never pick up a half-downloaded package.

<AccordionGroup>
  <Accordion title="It says it is starting over instead of resuming">
    The CLI restarts rather than resuming in three cases, and prints which one
    applies:

    * The resume record is missing or unreadable. The partial file is
      preallocated to the full size, so its length says nothing about which bytes
      are real. Guessing would produce a file that fails its checksum hours
      later.
    * The package was republished while you were downloading. Continuing would
      mix two versions into one file.
    * A file with the final name already exists at a different size. Nothing is
      overwritten. Move it, or pass a different `--out`.
  </Accordion>

  <Accordion title="The checksum did not match">
    ```text theme={"dark"}
    error: The downloaded bytes do not match the published checksum for qwen3-6-27b.
      The partial file was left at /data/models/qwen3-6-27b-v4.kit.part. Delete it and run the command again.
    ```

    The file is never renamed to its final name when this happens, so you cannot
    accidentally serve corrupt bytes. Delete the `.part` file and pull again. If
    it happens twice on the same package, contact support with the slug.
  </Accordion>

  <Accordion title="It stopped before downloading anything">
    Free space is checked before the first byte is written, against what is left
    to fetch plus a small margin:

    ```text theme={"dark"}
    error: Not enough free space in /data/models: 12.4 GB available, 28.7 GB needed.
      Free some space or pass --out to a larger volume, then run the same command again to resume.
    ```

    Free space or point `--out` at a larger volume. Anything already downloaded
    is still there.
  </Accordion>

  <Accordion title="It says the workspace does not own the package">
    ```text theme={"dark"}
    error: Your workspace does not own qwen3-6-27b, or that slug does not exist.
      Check the slug on runinfra.ai, or buy the package first.
    ```

    Check the slug on the package page in the catalog. If you are in more than
    one workspace, check that this terminal is connected to the one that bought
    it: `runinfra whoami` prints the workspace id.
  </Accordion>

  <Accordion title="It says access expired">
    A connected terminal lasts 90 days. Run `runinfra login` again to reconnect
    it. Downloads you already completed are unaffected.
  </Accordion>
</AccordionGroup>

## Exit codes for scripts

Failures are grouped so a CI job can tell a transient blip from a permanent
refusal instead of retrying everything forever.

| Code | Meaning                                                           | Retry                  |
| ---- | ----------------------------------------------------------------- | ---------------------- |
| 0    | Success                                                           |                        |
| 2    | Bad usage, or Node older than 20                                  | No                     |
| 3    | Not signed in, denied, expired, or revoked                        | After `runinfra login` |
| 4    | The workspace does not own it, or its files are not published yet | No                     |
| 5    | Network or server failure                                         | Yes                    |
| 6    | Checksum mismatch, or the package changed mid-download            | Start over             |
| 7    | No space, or the destination is not writable                      | After freeing space    |
| 130  | Interrupted                                                       | Yes, it resumes        |

<Tip>
  In a container image build, run `runinfra login --device` once on a machine
  with a browser and mount the credentials directory, or set
  `RUNINFRA_CONFIG_DIR` to a path your build can read. Sign-in is interactive by
  design: there is no way to mint a terminal credential without a human
  approving it.
</Tip>
