---
title: Quilium CLI
description: Every Quilium CLI command, flag, and config file, plus how authentication, output and exit codes work.
updated: 2026-07-30
---

The Quilium CLI does two separate jobs. It manages the **theme** in your working directory — scaffold, dev server, push, pull — and it reads and writes the **site's content** — pages, content blocks, collections, media, images — from the shell. It is published as the `quilium` package, exposes the `quilium` binary, and requires **Node 20 or later**. This page documents **v1.1.2**.

```bash
npm i -g quilium
```

<div class="q-callout q-callout-note"><i class="fa-solid fa-flask lead"></i>
  <div><b>Live example: Lisière.</b> Every command on this page is shown against <b>Lisière</b>, the demo site of an independent long-form review. It is bilingual, its navigation key is <code>main</code>, and it carries three collections: <code>articles</code> (the backbone, with a detail route), <code>auteurs</code> and <code>abonnes</code>.</div></div>

The CLI checks the registry at most once an hour, in the background, and prints an upgrade line when a newer version is out. Nothing is ever installed for you.

## Commands

`init`, `login`, `logout` and the `site` group run from anywhere. Everything else runs from the project root — the directory holding `quilium.json` — unless it accepts `--website`.

### Project and theme

| Command | Arguments and flags | Action |
|---|---|---|
| `init <id>` | `<id>` site UUID (positional); `-n, --name`; `-d, --dir` (**required**); `-f, --force` | Create the project directory, write `quilium.json`, then pull the theme. |
| `login` | — | Log in through the browser and store the credentials. |
| `logout` | — | Clear the stored credentials. |
| `whoami` | `--json`. Alias: `me` | Show the signed-in account, and its access to this site. |
| `run [port]` | `[port]` (default 9000); `-L, --live`; `--host` | Run the local dev server. |
| `start [port]` | `[port]` (default 9000); `--host` | Run the dev server as a background daemon. |
| `stop` | — | Stop the daemon. |
| `push` | `-f, --files <glob>`; `-k, --keep-zip`; `-y, --yes` | Zip and upload the theme, validate it, then upload the dictionary. |
| `pull` | `-f, --force` | Download and unzip the theme into the working directory. |
| `dictionary parse` | — | Extract i18n msgids from local templates and upload them. |
| `migrate` | — | Rewrite a legacy `quilium.json` for the current infrastructure. |

### Sites, content and media

| Command | Arguments and flags | Action |
|---|---|---|
| `site list` | `--full`; `--json`; `--only`; `--raw`; `-o, --out` | Table of every site the account can reach. |
| `site get <uuid>` | `--me`; `--json`; `--only`; `--raw`; `-o, --out` | One site, as a detail panel. |
| `site create` | `--name`; `--org`; `--no-wait`; `--timeout`; `-y, --yes`; `--json`; `-o, --out` | Create a site, wait for its deploy, print its UUID. |
| `site public [on\|off]` | `--json`; `--only`; `-o, --out` | Show, or turn on / off, the public mode of the draft. |
| `nav list` | `--only`; `--raw`; `-o, --out` | Navigations with their page trees. |
| `page get <ref>` | `<ref>` = `<nav>/<page uuid>`; `--nav`; `--includes`; `--only`; `--raw`; `-o, --out` | One page with its content blocks. |
| `content get <ref>` | `<ref>` = `<nav>/<page>/<block>`; `--nav`; `--page`; `--only`; `--includes`; `--raw`; `-o, --out` | Fetch a content block. |
| `content update <ref>` | `--field`; `--json-field`; `--no-verify`; `--json`; `-o, --out` | Set one or more fields. |
| `content push <ref> <file>` | `--at` (default `custom.content`); `--no-verify`; `--json`; `-o, --out` | Write a local file into one field. |
| `content diff <ref> <file>` | `--at`; `--exit-code` | Unified diff of that one field, local against remote. |
| `item list <type>` | `--filter`; `--search`; `--state`; `--sort`; `--direction`; `--page`; `--per-page`; `--includes`; `--only`; `--raw`; `-o, --out` | List a collection's items. |
| `item get <type> <id>` | `--includes`; `--only`; `--raw`; `-o, --out` | Fetch one item. |
| `item create <type> [file]` | `[file]` JSON, or `-` for stdin; `--field`; `--json-field`; `--no-verify` | Create an item, print its UUID. |
| `item update <type> <id>` | `--field`; `--json-field`; `--no-verify`; `--json`; `-o, --out` | Update an item's fields. |
| `item delete <type> <id>` | `-y, --yes`; `--json` | Delete an item. Asks first. |
| `image generate <prompt>` | `--size`; `--quality`; `--format`; `--save`; `--alt`; `--folder`; `-o, --out`; `--timeout` | Generate an image, print its URL. |
| `image edit [source]` | `--prompt`; `--from-media`; `--with`; same output flags as `generate` | Edit an image, print its URL. |
| `image result <job_id>` | same output flags as `generate` | Re-attach to an interrupted generation. |
| `media upload <file>` | `--name`; `--alt`; `--description`; `--credits`; `--folder`; `--json`; `-o, --out` | Upload a local file, print its media UUID. |

Every command in this second table also takes `--website <uuid>`, except `site list`, `site get` and `site create`, which are account-level. See [Targeting a site](#targeting-a-site).

### Global flags

| Flag | Action |
|---|---|
| `-v, --verbose` | Trace every request, method and URL only. Same as `DEBUG=quilium`. |
| `-h, --help` | Show help for the command or subcommand. |
| `-V, --version` | Print the CLI version. |

<div class="q-callout q-callout-warn"><i class="fa-solid fa-triangle-exclamation lead"></i>
  <div><b><code>-v</code> is verbose, not version.</b> Version moved to the capital <code>-V</code> when the debug flag was given the short form. A script calling <code>quilium -v</code> to read a version number now gets a usage error.</div></div>

## Targeting a site

Content commands need to know which site to act on. Two ways, and no guessing between them:

- run the command inside a project directory, and the site comes from `quilium.json`;
- or name it explicitly with `--website`, from anywhere.

```bash
quilium nav list --website <site-id>
```

Omitting `--website` outside a project is an error, not a fallback.

`push`, `pull`, `run`, `start`, `stop` and `dictionary parse` deliberately refuse `--website`. They act on the local working directory and read `quilium.json`, full stop.

The three `site` commands are the other exception: they act on the **account**, so they take no `--website`.

## init

```bash
quilium init <site-id> -d lisiere -n "Lisière"
```

| Flag | Required | Action |
|---|---|---|
| `<id>` | yes | The site UUID, from the dashboard or from `quilium site list`. |
| `-n, --name` | no | Project name written to `quilium.json`. |
| `-d, --dir` | **yes** | Directory to create the project in. |
| `-f, --force` | no | Overwrite an existing target directory. |

Creates the directory, writes `quilium.json`, then runs a `pull` so the working tree matches the current theme.

## login, logout, whoami

```bash
quilium login
```

`login` opens your browser and runs an OAuth 2.0 authorization code flow with PKCE. There is nothing to copy or paste, and tokens refresh on their own. Credentials are stored in `~/.quilium`, mode `0600`. `logout` clears them.

Coming from v0, this is the one thing to redo: the pasted-token files that version used (`~/.quilium/token*`) are no longer read. Run `quilium login` once; the old files can be deleted.

The dev proxy never forwards that account-wide token: it exchanges it for a short-lived token scoped to the project's site.

```bash
quilium whoami
```

`whoami` (alias `me`) answers "which account am I, and can it reach this site?". It calls the account service, so an expired session or a wrong account surfaces here instead of as a 403 in the middle of a `run` or a `push`. Run it from a project directory to also get the site-access verdict; `--json` makes it machine-readable.

For CI, set `QUILIUM_JWT_TOKEN` instead of logging in.

## run, start, stop

```bash
quilium run
```

`run` serves the site locally on `[port]` (default 9000), rendering **your local templates** against content, routes and dictionaries fetched from the CMS. It renders **draft** content, which is what you want while building; `-L, --live` renders the published content instead, which is a deliberate exception, not the daily loop.

Templates are read from disk per request, so a saved edit shows up on the next page load. Nothing to restart, nothing to rebuild.

`start` runs the same server detached; `stop` stops it.

<div class="q-callout q-callout-warn"><i class="fa-solid fa-triangle-exclamation lead"></i>
  <div><b>The dev server is local by design.</b> It binds to <code>127.0.0.1</code> and rejects foreign <code>Host</code> headers, because the proxy forwards your access token. <code>--host 0.0.0.0</code> exposes it on your network; the CLI prints a warning when you do.</div></div>

## push

```bash
quilium push
```

`push` archives the theme (templates, assets, config), uploads it, prints the server-side validation report, then uploads the dictionary. It also:

- refuses to push when the working directory is empty;
- checks git status and asks for confirmation when the working tree is dirty or out of sync with the remote;
- always bundles `quilium.json`: the frontend reads `private` from it (see the note below).

| Flag | Action |
|---|---|
| `-f, --files <glob>` | Push only files matching the glob (partial push), after confirming the list. |
| `-k, --keep-zip` | Keep the generated archive on disk instead of deleting it. |
| `-y, --yes` | Skip every confirmation prompt. For scripts and CI. |

<div class="q-callout q-callout-warn"><i class="fa-solid fa-triangle-exclamation lead"></i>
  <div><b>Commit before you push.</b> Push warns on a dirty or out-of-sync git tree and lets you continue. Commit first, so what is live matches what is in version control.</div></div>

## pull

`pull` downloads the theme and unzips it into the working directory. It refuses to run when the target already has content, unless you pass `-f, --force`, which asks for confirmation and then replaces everything. It **skips the bundled `quilium.json`**, so your local config survives.

## site

The only commands that act on your account rather than on one site, and the only ones that print for a human by default — finding a UUID is the point, and a wall of JSON is a bad way to do it.

```bash
quilium site list                        # every site this account can reach
quilium site get <uuid> --me             # --me adds your own profile on that site
quilium site create --name "Lisière"
```

`site list` prints a table: name, engine, status, id. `STATUS` is one column, showing `deployState` when it is not `deploy_success` and `state` otherwise. `--full` adds owner and domain.

The format never changes on its own. Piping drops the colour, never switches to JSON, so a script that forgot a flag cannot silently get something else. Ask for machine-readable output explicitly with `--json`, `--raw`, `--only` or `-o`.

`site create` takes the name only. The slug is derived from it server-side, and the engine is always Liquid, from a blank theme. `--org <uuid>` hands ownership to an organisation instead of your account. At a terminal it prompts for anything you did not pass, including an arrow-key owner picker, and asks for confirmation; nothing is ever asked when stdin is not a terminal, so a missing `--name` under an agent or in CI stays a clean error rather than a command hanging on a question nobody reads.

It then **waits for the deploy** (about 30 seconds) before handing back the UUID, because no site-scoped command works until the deploy finishes. `--no-wait` opts out, `--timeout` changes the budget. stdout carries the UUID alone, everything else is on stderr:

```bash
ID=$(quilium site create --name "Lisière" --yes)
quilium init "$ID" --dir ./lisiere
```

### Public draft

`site public` is the one `site` command bound to a site (it takes `--website` like the content commands). The live site is always public; the **draft** — the preview address where pages are prepared before publishing — is reserved to the members of the site unless its public mode is on, which serves it to anyone with the address, unpublished pages included. Handy to have a client review a site before launch.

```bash
quilium site public          # show: protected, or PUBLIC (since, by whom)
quilium site public on       # anyone with the draft address can see it
quilium site public off      # members only again
```

Only the owner, administrators and webdesigners of the site can flip it; anyone else gets exit code `5`. Nothing is asked, since nothing goes offline. The same switch is the *Public draft* checkbox in the backoffice, under *Settings › General*.

It replaces `"private": false` in `quilium.json`, which is **deprecated**. The engine still honours the key while the switch is off, so a draft can be public without the switch: the command says so, and turning the switch off does not protect the draft until the key is removed and the theme pushed again.

## nav, page, content

UUIDs are not guessable, and a page UUID alone does not identify a block — a block lives at `<navigation>/<page>/<block>`. Three commands chain into each other, and that chain is the discovery path:

```bash
quilium nav list --only navigations.0.key          # → main
quilium page get main/<page-uuid> --only contents  # block ids and types
quilium content get main/<page-uuid>/<block-uuid>
```

If you already hold the block UUID, `--nav` and `--page` save you building the ref:

```bash
quilium content get <block-uuid> --nav main --page <page-uuid>
```

`content` is built around one loop: the document is written to disk, edited there, and pushed back. It never has to be pasted into a terminal or into a conversation.

```bash
REF=main/<page-uuid>/<block-uuid>

quilium content get $REF --only custom.content -o body.html
$EDITOR body.html
quilium content diff $REF body.html
quilium content push $REF body.html
```

`content push` writes to `custom.content` by default. For any other field, `--at`. To set several fields at once, or short values, use `content update`:

```bash
quilium content update $REF \
  --field custom.title=@title.txt \
  --field custom.excerpt="Là où les mondes se touchent" \
  --json-field custom.category='["paysage"]'
```

A content block's fields live under `custom`, which is the wrapper the API expects. `--only custom.content` on the way out matches `--field custom.content=` on the way back: one name for one field, both directions.

`content diff --exit-code` behaves like `git diff` — exit `1` when there are differences, `0` when there are none.

## item

Collections are the bulk surface, and where a shell loop beats editing rows by hand. `<type>` is the collection's type identifier.

```bash
quilium item list articles
quilium item list articles --search lisiere
quilium item list articles --filter category=paysage --sort date --direction desc
quilium item list articles --page 2 --per-page 50
```

The payload on stdout is the **bare array**, so `jq` and `--only` address the rows directly. Pagination goes to stderr, where it will not pollute a redirect.

```bash
quilium item list articles --only 0.title
quilium item list articles | jq -r '.[].slug'
```

Reading and writing one row:

```bash
quilium item get articles <id> --only body -o body.html
quilium item update articles <id> --field date=2026-09-14
quilium item update articles <id> --field body=@body.html
quilium item delete articles <id> --yes
```

A collection's fields are **flat** — the same names `item get` reads back. The `data.` prefix (`--field data.date=2026-09-14`) is also accepted, and is what scripts written before July 2026 use; nothing needs changing.

`item create` takes a JSON document of the fields themselves, with no wrapper, and prints the new UUID alone:

```bash
quilium item create articles ./article.json
quilium item create articles --field title="Vendanges" --field slug=vendanges

jq -c '.[]' articles.json | while read -r row; do
  echo "$row" | quilium item create articles --no-verify
done
```

## image, media

Generation and editing spend **site credits**. `standard` is the default tier; `ultra` is slow and expensive, and opt-in. stdout is the URL alone, so the commands chain.

```bash
quilium image generate "a forest edge at dawn, ink on cold paper" \
  --quality fast --size landscape

quilium image generate "a forest edge at dawn" \
  | quilium image edit --prompt "add mist in the understory"
```

| Flag | Action |
|---|---|
| `-o <file>` | Also download the bytes, so you can look at the result. |
| `--save` | Persist it into the media library and print the **media UUID** instead of the URL. |
| `--from-media <uuid>` | Use a library media as the edit source; its signed URL is resolved for you. |
| `--with <url>` | Extra reference image for a multi-image edit. Repeatable. |

The generated URL is a **temporary** storage link. To keep the image, `--save` it in the same command rather than hoping the link survives:

```bash
ID=$(quilium image generate "bottle on a stone wall" --save --alt "Bouteille")
quilium item update articles <id> --json-field image="[{\"id\": \"$ID\"}]"
```

A job is paid for the moment it starts, so its id is printed on stderr *before* the first poll. If you lose the command, `quilium image result <job_id>` re-attaches. This is best effort: the gateway keeps job state in memory, drops it once retrieved, and expires it after ten minutes.

`media upload` pushes a local file into the media library and prints its UUID. The bytes never pass through a command argument: the CLI asks for a short-lived upload ticket, sends the file straight to storage, then registers it.

```bash
ID=$(quilium media upload ./photo.jpg --name "Lisière, l'écotone")
quilium item update articles <id> --json-field image="[{\"id\": \"$ID\"}]"
```

## dictionary parse

```bash
quilium dictionary parse
```

`dictionary parse` scans your templates for i18n msgids and uploads them. In a `.liquid` template it matches the filter form, `'msgid' | _` (or its alias `| t`).

Only **literal** source strings are extracted; a msgid built from a variable renders at runtime but is never picked up. This is the extraction step of the `| _` workflow, see [The multilingual model](/en/docs/multilingual-model) and [Liquid filters](/en/docs/liquid-filters).

`push` already runs this step, so the standalone command is for when you want the dictionary refreshed without shipping a theme. The `dictionary` group is a group rather than a bare command because the CMS-side translation tools, which run in the opposite direction, will join it.

## migrate

`migrate` rewrites the `quilium.json` of a project created on the discontinued `eu1` infrastructure so it points at the current one. It is a no-op on an already-current config. The other commands refuse a legacy config and point at `migrate`.

## Output and input

The stream split is the contract these commands are built on.

| Stream | Carries |
|---|---|
| **stdout** | the payload, and only for reads. Mutations print an id and nothing else. |
| **stderr** | status lines, pagination, warnings, progress. |

So `quilium content get … > body.html` produces a clean file, and `| jq` never chokes on a status line.

| Flag | Effect |
|---|---|
| `--only <dot.path>` | Extract a single field. A string field comes out as **raw bytes**, so an HTML block is HTML, not a quoted JSON string. Structured fields come out as JSON. |
| `-o, --out <file>` | Write the payload to a file, leaving stdout empty. A *file*, not a format: there is no `-o json`. |
| `--json` | Make a *mutation* print the full API response. |
| `--raw` | Keep the API envelope instead of unwrapping it. |
| `--includes <list>` | Ask for extra data, for example `translations.en,translations.fr`. |

Values are never given by interpolation. HTML and Markdown are full of quotes, `$`, backticks and newlines, so `--field body="$(cat body.html)"` mangles the content and walks into the shell's argument size limit.

| Form | Meaning |
|---|---|
| `--field path=value` | A literal string. |
| `--field path=@file` | The file's bytes. |
| `--field path=-` | stdin. Once per command. |
| `--field path=@@x` | A literal value that starts with `@`. |
| `--json-field path=…` | The same sources, parsed as JSON. |

## Write verification

The API folds unknown keys into the resource's field bag, drops whatever the type does not declare, and still answers `200`. A typo is therefore a write that reports success and changes nothing — the failure a loop over 200 rows cannot see. So every write checks itself, and fails when a field it sent is missing from the response:

```
✖ 1 field was not written: millesim.
  The API ignores a field the type does not declare, and still answers 200.
  Check the spelling against "quilium item get articles <id>".
```

The test is presence, not equality: the server normalises what it stores, so comparing values would cry wolf on every second write. Fields that did land stay written — this is a report, not a rollback.

The check costs one fuller response body and never an extra request. `--no-verify` turns it off for a mass import where that transfer adds up.

## Exit codes

A script never has to parse prose.

| Code | Meaning | Usual fix |
|---|---|---|
| `0` | Success. | |
| `1` | Generic failure. Also `content diff --exit-code` when there are differences. | |
| `3` | Not signed in, or refused after a refresh. | `quilium login` |
| `4` | Not found: wrong id, wrong nav, wrong route. | Re-run the discovery chain. |
| `5` | Forbidden: your profile cannot do this. | Check `quilium whoami`. |
| `6` | Validation. The per-field errors are printed. | Fix the fields and retry. |
| `7` | The gateway could not be reached. | Network. |

A 422 prints which field the API refused and why, rather than a bare status. `6` also covers the two refusals that never reach the API — a write with no field at all, and a write whose fields were silently dropped — so a script sees one code for "the payload was wrong", whichever side caught it.

## `quilium.json`

The project config, written by `init` at the project root. It **is** included in every `push` (the frontend reads `whiteList`, and the deprecated `private`, back from it) and is preserved across `pull`.

```json
{
  "website": "<site-id>",
  "name": "Lisière"
}
```

| Key | Notes |
|---|---|
| `website` | The site this project is bound to. Required. |
| `name` | Project display name. |
| `frontendEndpoint`, `frontendPort`, `frontendProtocol` | Override the frontend host, port and protocol (default `https:`). |
| `backendEndpoint`, `backendProtocol` | Override the CMS host and protocol (defaults to the frontend protocol). |
| `gatewayEndpoint`, `gatewayProtocol` | Override the gateway the content commands talk to. `QUILIUM_GATEWAY_BASE_URL` overrides both. |
| `private` | **Deprecated** — use [`quilium site public on`](#public-draft). `false` still serves the draft publicly while the public mode is off. |
| `whiteList` | IPs allowed on the draft site without authenticating. |
| `dictionary` | Glob options for the dictionary extraction. |

<div class="q-callout q-callout-note"><i class="fa-solid fa-circle-info lead"></i>
  <div><b>Local overrides travel.</b> <code>quilium.json</code> is part of the pushed theme, and the frontend reads <code>private</code> and <code>whiteList</code> back from it. <code>pull</code> never overwrites your local copy, but any endpoint override you keep for local development does go up on the next push.</div></div>

## `.quiliumignore`

A gitignore-style file at the project root. Files it matches are excluded from `push` and from dictionary extraction.

## Where to go next

<div class="q-cards">
  <a class="q-card reco" href="/en/docs/quickstart"><span class="k">Recommended</span><span class="t">Quickstart, 2 min</span><span class="d">Run a demo site locally with the CLI.</span></a>
  <a class="q-card" href="/en/docs/publish-and-go-live"><span class="t">Publish and go live</span><span class="d">Push the theme and put the site online.</span></a>
  <a class="q-card" href="/en/docs/troubleshooting"><span class="t">Troubleshooting</span><span class="d">When a command fails, or the render is not what you expected.</span></a>
  <a class="q-card" href="/en/docs/mcp-tools"><span class="t">MCP tools</span><span class="d">Drive the same site through an agent.</span></a>
</div>
