Quilium CLI
Every Quilium CLI command, flag, and config file, plus how authentication, output and exit codes work.
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.
npm i -g quilium
main, and it carries three collections: articles (the backbone, with a detail route), auteurs and abonnes.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.
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. |
-v is verbose, not version. Version moved to the capital -V when the debug flag was given the short form. A script calling quilium -v to read a version number now gets a usage error.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.
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
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
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.
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
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.
127.0.0.1 and rejects foreign Host headers, because the proxy forwards your access token. --host 0.0.0.0 exposes it on your network; the CLI prints a warning when you do.push
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 readsprivatefrom 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. |
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.
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:
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.
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:
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:
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.
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:
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.
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.
quilium item list articles --only 0.title
quilium item list articles | jq -r '.[].slug'
Reading and writing one row:
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:
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.
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:
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.
ID=$(quilium media upload ./photo.jpg --name "Lisière, l'écotone")
quilium item update articles <id> --json-field image="[{\"id\": \"$ID\"}]"
dictionary parse
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 and 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.
{
"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. 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. |
quilium.json is part of the pushed theme, and the frontend reads private and whiteList back from it. pull never overwrites your local copy, but any endpoint override you keep for local development does go up on the next push..quiliumignore
A gitignore-style file at the project root. Files it matches are excluded from push and from dictionary extraction.
Where to go next
Last updated September 30, 2026.