Skip to content

CLI reference

Every @dukkan.one/cli command for themes and apps with its options, exit codes, output, files, environment variables, and troubleshooting.

Updated 9 Sept 20267 min read
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.

Shell
npx @dukkan.one/cli COMMAND
# or, after a global install: dukkan COMMAND

Exit 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#

Shell
npx @dukkan.one/cli login

Device-flow sign-in (RFC 8628):

Text
  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-XXXXXX from 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 on 401; 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]#

Shell
npx @dukkan.one/cli theme init my-theme

Writes 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]#

Shell
npx @dukkan.one/cli theme check
npx @dukkan.one/cli theme check --remote

Validates 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>]#

Shell
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.

Text
✓ Pushed version 7 (3f9a1c2b7d4e…)
✓ No changes — version 7 already on the server.
  • --name is the display name (up to 80 characters); the default is External 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]#

Shell
npx @dukkan.one/cli theme pull ./restored

Downloads the sandbox's current draft and prints ✓ Pulled N files (version …). The CLI refuses any path that escapes the target directory.

theme dev [dir]#

Shell
npx @dukkan.one/cli theme dev

Pushes once, requests a preview link from POST /theme-api/v1/theme/preview, then watches the directory and pushes on every change:

Text
✓ 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_TOKEN

A 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]#

Shell
npx @dukkan.one/cli app init my-app

Scaffolds 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
Text
✓ 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 events

Requesting clients:read prints a warning: the portal requires the accepted data processing agreement before a draft can carry it (Scopes).

app list#

Shell
npx @dukkan.one/cli app list

Your 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#

Shell
npx @dukkan.one/cli app config pull
npx @dukkan.one/cli app config push

pull 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
Text
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_ID

app dev [dir]#

Shell
npx @dukkan.one/cli app dev

Starts 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
Text
✓ 14:05:12 order.created                9f3a1c2b  200 test
✗ 14:06:40 order.paid                   0b7e55d1  500

Each 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>#

Shell
npx @dukkan.one/cli app webhook trigger order.created

Asks 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
Text
✓ 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#

Shell
npx @dukkan.one/cli app logs --follow

The 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
cli-credentials.jsonJSON
{
  "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