CLI reference
Every @dukkan.one/cli command for themes and apps with its options, exit codes, output, files, environment variables, and troubleshooting.
On this page
The package @dukkan.one/cli is the Dukkan developer command line for themes and apps; its binary is dukkan. It requires Node.js 18.17 or newer, works against exactly one sandbox store, and can never touch a real store. @dukkan.one/theme-cli still installs and forwards to it, with a deprecation notice; switch your scripts to the new name.
npx @dukkan.one/cli COMMAND
# or, after a global install: dukkan COMMANDExit codes#
| Code | Meaning |
|---|---|
0 |
success |
1 |
any failure: a validation error, a server rejection, a login timeout, or an unexpected error |
Every failure line starts with ✗, success with ✓, warnings with !.
login#
npx @dukkan.one/cli loginDevice-flow sign-in (RFC 8628):
Open: https://developer.dukkan.one/cli/authorize?code=DKN-7RX2MK
Code: DKN-7RX2MK
Waiting for approval in the portal…
✓ Signed in. The CLI can now push themes to your sandbox store.- The code is shaped
DKN-XXXXXXfrom an unambiguous alphabet (BCDFGHJKMNPQRSTVWXYZ23456789) and valid for 10 minutes. - You open the link, confirm the code and pick the sandbox store. The CLI polls the portal every 5 seconds (adding 2 seconds on
slow_down). - It yields a
dk_cli_at_(one hour) /dk_cli_rt_(30 days) pair bound to that sandbox, refreshed automatically on401; reusing an old refresh token revokes the chain.
| Message | Cause |
|---|---|
✗ could not start sign-in (STATUS) |
the portal did not issue a device code; check DUKKAN_PORTAL_URL |
✗ sign-in was denied in the portal. |
you denied the request in the portal |
✗ sign-in timed out. Run dukkan login again. |
10 minutes passed without confirmation |
✗ sign-in failed (expired_token) |
more than 200 polls, or the code expired |
✗ Not signed in. Run: dukkan login |
no credentials file |
✗ Session expired. Run: dukkan login |
the refresh failed (a revoked chain or an expired sandbox) |
5xx errors while polling never end the sign-in; the CLI backs off up to 15 seconds and keeps going until the code expires.
Theme commands#
Unchanged from @dukkan.one/theme-cli; the full guide is Build a theme.
theme init [dir]#
npx @dukkan.one/cli theme init my-themeWrites 4 files: layout/theme.liquid, templates/home.liquid, config/settings_schema.json and locales/ar.json. Prints ✓ Starter theme created (4 files).
Warning
In release 0.1.0 the generated layout uses a legacy form the engine does not support; replace it with the correct pair from Build a theme before your first preview.
theme check [dir] [--remote]#
npx @dukkan.one/cli theme check
npx @dukkan.one/cli theme check --remoteValidates the directory locally with the same code the server applies at push and prints ✓ Bundle valid — N files, hash … or a list of ✗ lines. With --remote it sends the bundle to POST /theme-api/v1/theme/check for a smoke render (requires sign-in) and prints ✓ Server sandbox render passed.. The rules and error strings are in Theme validation. The local check needs no sign-in.
theme push [dir] [--name <name>]#
npx @dukkan.one/cli theme push --name "Damascus Gold"Validates locally, then sends the bundle to PUT /theme-api/v1/theme/draft. Every new content becomes an immutable, content-hashed version and is installed as the sandbox's draft; an identical bundle creates nothing.
✓ Pushed version 7 (3f9a1c2b7d4e…)
✓ No changes — version 7 already on the server.--nameis the display name (up to 80 characters); the default isExternal theme. The name on the reviewed listing is what merchants see.- The theme identity is derived server-side from the sandbox (
sandbox-<id>); one sandbox per theme. - A server rejection prints as
✗ Theme bundle rejected — {"errors":[…]}.
theme pull [dir]#
npx @dukkan.one/cli theme pull ./restoredDownloads the sandbox's current draft and prints ✓ Pulled N files (version …). The CLI refuses any path that escapes the target directory.
theme dev [dir]#
npx @dukkan.one/cli theme devPushes once, requests a preview link from POST /theme-api/v1/theme/preview, then watches the directory and pushes on every change:
✓ Version 7 live in preview:
https://dukkan.one/stores/YOUR_SANDBOX_SLUG?__preview=TOKEN
Watching for changes — Ctrl+C to stop.
✓ 14:05:12 version 8 — https://dukkan.one/stores/YOUR_SANDBOX_SLUG?__preview=NEW_TOKENA preview link lives 24 hours and is revoked automatically when the draft changes (draft_rev), so the CLI prints a new link only after a push that changed something. Rapid edits are coalesced; a failed push prints ✗ and watching continues.
App commands#
The app commands build, run and configure a Dukkan app on @dukkan.one/app-sdk against your sandbox store. They edit draft versions only, never activate one, and never handle webhook signing secrets. Every command accepts --json for one JSON object per line without glyphs.
app init [dir]#
npx @dukkan.one/cli app init my-appScaffolds a Nuxt 4 app on @dukkan.one/nuxt: dukkan.app.toml, .env.example, server/dukkan.ts and a landing page with the merchant states (connected, reconnect, removed, not installed) and a developer checklist. Prompts for the name, the template and the scopes; every prompt has a flag for scripts.
| Option | Meaning |
|---|---|
-t, --template <name> |
nuxt-minimal (installs in memory, right for a first run) or nuxt-postgres (sealed tokens, a durable inbox, a migration) |
-n, --name <name> |
display name |
-s, --scopes <list> |
comma-separated scopes; default orders:read |
-p, --port <port> |
dev server port written into the file; default 3000 |
-y, --yes |
accept defaults, no prompts |
✓ Created my-app in /home/dev/my-app (14 files, template nuxt-minimal).
Next:
cd my-app && npm install
cp .env.example .env # then fill in the client secret from the portal
dukkan login # once, binds the CLI to your sandbox store
dukkan app dev # tunnel, install link, live eventsRequesting clients:read prints a warning: the portal requires the accepted data processing agreement before a draft can carry it (Scopes).
app list#
npx @dukkan.one/cli app listYour team's apps with their draft and active version numbers and ids. The id is what --app takes; commands remember it in .dukkan/state.json after the first use.
app config pull and app config push#
npx @dukkan.one/cli app config pull
npx @dukkan.one/cli app config pushpull writes the portal's draft version (or the active one when there is no draft) into dukkan.app.toml, including app.client_id. push sends the file onto the draft: it creates the draft when there is none, prints the diff, asks before removing a scope, and never activates. The portal shows the version as managed by the file afterwards. The rules, the drift notice and --force are in The dukkan.app.toml file.
| Option | Meaning |
|---|---|
-a, --app <id> |
the app, when the directory has not been used before |
-f, --force |
push only: overwrite a draft edited in the portal since the last pull |
-y, --yes |
push only: confirm scope removals without asking |
Updating draft version 4 of Order notifier:
+ scope: orders:write
- topic: product.updated
✓ Updated draft version 4; the portal now shows it as managed by dukkan.app.toml.
Make it active in the portal when ready: https://developer.dukkan.one/dashboard/apps/APP_IDapp dev [dir]#
npx @dukkan.one/cli app devStarts the app, opens a cloudflared quick tunnel, registers a development session in the portal (the tunnel's callback URL and endpoint, valid at most 8 hours, sandbox only), tells the running app its public URL, prints an install link for your sandbox, then streams every delivery for the sandbox until Ctrl+C. The session is deleted on exit. The full walk-through is the quickstart.
| Option | Meaning |
|---|---|
-a, --app <id> |
the app |
-p, --port <port> |
dev server port; default from dukkan.app.toml |
--tunnel-url <url> |
use this public HTTPS URL instead of starting cloudflared |
--no-tunnel |
no public URL; webhooks cannot reach the app (use webhook trigger --local) |
--command <cmd> |
the command that starts the app; default npx nuxi dev --port <port> |
--no-app |
do not start the app; attach to one already running on the port |
✓ 14:05:12 order.created 9f3a1c2b 200 test
✗ 14:06:40 order.paid 0b7e55d1 500Each row is a delivery: status glyph, time, topic, the first 8 characters of the event id, the response status your app answered (or the platform's error code), and test for rehearsals. A 500 is your handler throwing; the platform retries on its schedule.
| Message | Cause |
|---|---|
✗ Fix the configuration above (.env), then run dukkan app dev again. |
the app booted but reported missing variables; the lines above name them |
✗ The app did not answer on http://localhost:3000/_dukkan/dev/status |
the app is not on @dukkan.one/nuxt, or took over 90 seconds to start |
cloudflared not found |
install it, or pass --tunnel-url |
app webhook trigger <topic>#
npx @dukkan.one/cli app webhook trigger order.createdAsks the platform to fire one signed rehearsal of the topic to the running app, through the real outbox and delivery worker, with a sample built from your sandbox's seeded data. Sandbox stores only; the subscription must include the topic. The CLI never sees the signing secret.
| Option | Meaning |
|---|---|
-p, --port <port> |
port of the running app |
-i, --install <id> |
the install, when the app knows several |
-d, --data <file> |
JSON file with overrides merged into the sample payload |
--local |
sign a sample locally and post it straight to the app, offline |
--secret <secret>, --store <id> |
the signing secret and store id for --local |
✓ order.created queued as event 9f3a1c2b… (1 delivery). Watch it arrive in `dukkan app dev`.
→ https://lively-otter-1234.trycloudflare.com/dukkan/webhooks (delivery 4c1d9e0a…)--local exists for offline unit tests of your handler: you pass the secret your app stored, so the delivery verifies, but nothing goes through the platform.
app logs#
npx @dukkan.one/cli app logs --followThe delivery history of your app on your sandbox, newest first, in the same row format as app dev. --follow keeps tailing, -n, --limit <n> sets the rows (default 30), --since <iso> starts after an instant.
Files#
| Path | Contents |
|---|---|
~/.config/dukkan/credentials.json |
the credentials, mode 0600 inside a 0700 directory |
.dukkan/state.json in an app directory |
the app id and the hash of the last synced dukkan.app.toml; safe to commit |
{
"portal_url": "https://developer.dukkan.one",
"api_url": "https://dukkan.one",
"access_token": "dk_cli_at_REDACTED",
"refresh_token": "dk_cli_rt_REDACTED",
"development_store_id": "5d2f8c1e-9a44-4e8b-b7a9-3065a1f20c4d"
}Leaking the file exposes one sandbox's theme drafts only, and the refresh chain revokes itself on reuse. The CLI never prints token values.
Environment variables#
| Variable | Purpose | Default |
|---|---|---|
DUKKAN_PORTAL_URL |
the developer portal for the login flow | https://developer.dukkan.one |
DUKKAN_API_URL |
the platform host (theme API and Platform API) | https://dukkan.one |
DUKKAN_JSON |
any value: same as --json |
unset |
DUKKAN_CONFIG_DIR |
the credentials directory | ~/.config/dukkan |
DUKKAN_CLI is not a variable of the CLI itself but of the dukkan-themes repository tooling: it points that tooling at an unreleased build (DUKKAN_CLI=/path/to/app/cli/dist/index.js); see validator lag.
Troubleshooting#
| Symptom | Cause and fix |
|---|---|
Missing or invalid CLI token from the server |
the sandbox expired or was purged, or the token is revoked; renew the sandbox in the portal, then login |
429 during theme dev |
over 120 requests per minute for the CLI token; too many rapid changes |
render failed: template_missing |
no templates/home.liquid |
Theme preview is not configured on this server |
a local server without a preview secret |
band rejected locally but accepted remotely |
validator lag in 0.1.0; see the note |