CLI
KahunaCart ships eight console commands under bin/plugin kahunacart. This page documents all of them: arguments, options, output and exit codes.
Before you begin
- Run every command from the Grav root, the directory that contains
bin/. export,importandseedrefuse an out-of-date schema. They print The KahunaCart schema is out of date. Runbin/plugin kahunacart migratefirst. and exit 1.
Command reference
| Command | Arguments and options | What it does |
|---|---|---|
migrate |
--status |
Apply pending database migrations. |
status |
none | Health check: database, migrations, job queue, providers. |
work |
--budget=<seconds> |
Process pending jobs, then run maintenance. |
sync |
--provider=<slug>, --push, --pull, --status |
Push or pull the catalog against a sync-capable provider. |
export |
file (optional) |
Write the catalog to a YAML file, or to stdout. |
import |
file (required), --dry-run |
Read a catalog file back in, upserting by slug. |
seed |
--purge |
Fill the catalog with a demo store, or remove it again. |
webhooks |
--provider=<slug>, --limit=<n>, --purge |
Recent webhook deliveries, per-provider totals, failure streaks. |
migrate
Apply pending database migrations.
bin/plugin kahunacart migrate
bin/plugin kahunacart migrate --status
| Option | Default | Effect |
|---|---|---|
--status |
off | List pending steps without applying them. |
The command prints the database engine, then every pending step grouped by migration. Without --status it applies them and finishes with a count. Nothing pending prints Schema is up to date — nothing to migrate. and exits 0.
Migrations are step-level and idempotent, so re-running a partially applied migration picks up where it left off. There are no down migrations; recovery means restoring from backup. Applies run under a database lock, so two concurrent runs cannot interleave DDL.
Run this in your deploy pipeline on a MySQL or PostgreSQL store, where database.auto_migrate does not auto-apply. See Configuration → The auto_migrate policy.
# Typical deploy step
bin/plugin kahunacart migrate --status # see what is coming
bin/plugin kahunacart migrate # apply it
status
Health check across the database, migrations, job queue and providers.
bin/plugin kahunacart status
It prints:
- Database — the dialect in use.
- A MySQL strict-mode warning if the server is not strict. A non-strict MySQL silently truncates data.
- Migrations —
up to date, or a count of pending steps. - Jobs — pending, failed and completed counts.
- Providers — slug, label and declared capabilities for each.
KahunaCart Status
=================
Database: sqlite
Migrations: up to date
Jobs: 0 pending, 0 failed, 47 completed
Providers:
offline (Bank Transfer) []
stripe (Credit / Debit Card (Stripe)) [hosted_checkout, refunds, partial_refunds]
Run this first when something is not working. A provider missing from the list is not configured, because providers only register once their credentials are present. Pending migrations explain most "column not found" errors, and a growing failed-job count means the queue is choking.
work
Process pending jobs, then run maintenance.
bin/plugin kahunacart work
bin/plugin kahunacart work --budget=120
| Option | Default | Effect |
|---|---|---|
--budget=<seconds> |
50 |
How long the runner keeps claiming jobs before stopping. |
Important
Grav's scheduler runs this every minute, registered as kahunacart-jobs on a * * * * * schedule. Run it by hand only when debugging, or when the scheduler is not set up.
Job types it handles:
| Type | Purpose |
|---|---|
order.confirmation_email |
Order confirmation to the customer, plus the BCC copy. |
order.payment_received_email |
Receipt for an order that was awaiting payment and is now paid. |
order.shipped_email |
"Your order is on its way", when an order is marked as shipped. |
order.refund_email |
Refund notice, queued when a provider confirms a refund. |
order.grant_downloads |
Mint download grants for an order's digital lines. |
customer.set_password_email |
The "choose a password" link for a new store account. Mints the token as it sends. |
stock.release_expired |
Release expired stock holds. |
cart.abandoned_sweep |
Abandoned-cart reminders. |
notifications.dispatch |
One merchant notification, fanned out across matching channels. |
notifications.low_stock_sweep |
Low-stock merchant warnings. |
sync.push_product |
Push one product to a provider. |
sync.pull_catalog |
Pull a provider's whole catalog. |
After the queue drains, or the budget runs out, four maintenance tasks run on the same tick:
- Expired stock holds are released.
- The abandoned-cart sweep runs when
cart.abandoned_enabledis on. It caps itself at 25 carts and stamps each one. Its failures are reported but not fatal. - The low-stock sweep runs, stamping each variant so it warns once per slide under the threshold. Its failures are reported but not fatal.
- Inactive carts are purged per
cart.purge_days, default90, and webhook log rows are swept perpayments.webhook_log_days, default30. Set either to0to keep the rows forever.
Output looks like:
Jobs: 3 processed, 0 failed. Holds released: 1. Carts purged: 0. Webhook rows swept: 0.
The exit code is 1 if any job failed, 0 otherwise, so it works as a monitoring check.
Failed jobs are retried with a doubling backoff, capped at one hour, up to max_attempts, default 3. After that they sit in the failed bucket with last_error recording why.
Overlapping workers process each job exactly once, so running two workers is safe. A lock held for more than 300 seconds is treated as stale and the job can be claimed again.
sync
Push or pull the catalog against a sync-capable provider. See Sync for what syncs.
bin/plugin kahunacart sync
bin/plugin kahunacart sync --provider=polar
bin/plugin kahunacart sync --provider=polar --push
bin/plugin kahunacart sync --provider=polar --pull
bin/plugin kahunacart sync --provider=polar --push --status
| Option | Effect |
|---|---|
--provider=<slug> |
Which provider. Required with --push or --pull; filters the status tables otherwise. |
--push |
Push the local catalog to the provider. |
--pull |
Pull the provider's catalog into the local one. |
--status |
Show mappings, capabilities and directions. |
Running sync with no action flag behaves as --status. Passing --push and --pull together is an error, and so is either one without --provider. Manual runs are not gated on the configured direction, which only governs automatic syncing.
The status output has two tables:
Mappings
--------
Provider Mapped By state
polar 12 synced: 11, stale: 1
Providers
---------
Provider Sync capabilities Implements sync Direction
offline — no off
polar sync_products, sync_orders yes remote
stale mappings mean the provider reported a change that was not applied because it is not master here. Implements sync: no on a provider that declares sync capabilities is a bug in that plugin. A push or pull failure prints the error and exits 1.
export
Write the catalog to a YAML file.
bin/plugin kahunacart export catalog.yaml
bin/plugin kahunacart export # to stdout, for piping or redirecting
bin/plugin kahunacart export - > catalog.yaml
| Argument | Effect |
|---|---|
file |
Where to write it. Omit it, or pass -, to print to stdout with no decoration. |
The file holds categories with their nesting, landing-page setting and picture; products; every variant; option axes and their values; category and tag assignments; image references; and translated text. Orders, customers, coupons and settings are not included.
Four things about the format matter before you edit one by hand:
- Everything is referenced by slug, never by row id.
- Amounts are quoted decimal strings —
price: '28.00', never28.00. Unquote one and the import refuses the file. - Images are paths, not bytes, relative to the store's images root. A category's picture rides under
image:. - Tags are labels matched on their derived slug, so
Organicfinds an existingorganic.
display: is written only when it is not auto, and an empty translations: block is omitted. Output is ordered by slug, so the same catalog always exports to the same file.
# KahunaCart catalog export
# Amounts are decimal strings in the currency named above; import refuses a bare number.
version: 1
currency: USD
categories:
- slug: apparel
title: Apparel
position: 0
display: both
image:
path: 3/apparel-8c21f0a91b3e.jpg
alt: A rail of shirts
- slug: shirts
title: Shirts
parent: apparel
position: 0
products:
- slug: reef-runner-tee
title: Reef Runner Tee
type: physical
status: published
tax_class: standard
categories:
- apparel
- shirts
tags:
- organic
- sale
images:
- path: products/tee.jpg
alt: A tee
position: 0
variants:
- sku: KC-TEE-M
title: Medium
is_default: true
position: 0
price: '28.00'
compare_price: '35.00'
stock_managed: true
stock_qty: 4
weight_grams: 180
import
Read a catalog file back in.
bin/plugin kahunacart import catalog.yaml
bin/plugin kahunacart import catalog.yaml --dry-run
| Argument or option | Effect |
|---|---|
file |
The catalog file to read. Required. |
--dry-run |
Validate the file and report what it holds without writing anything. |
Important
Import never deletes. A slug in the file that exists here is updated, one that does not is created, and anything in the store and not in the file is left as it was. Running the same file twice is the same as running it once.
What that means in practice:
- Products and categories are matched on slug.
- Variants are matched on SKU when they have one, and on position when they do not.
- Category assignments are added, not replaced. A file that puts a product in
appareldoes not take it out ofsale. - Tag assignments are added too. Remove a tag in the admin instead.
- Categories are imported in two passes, every row first and
parent:second, so a file may list a child before its parent. - Existing images are left alone, alt text included.
The whole import runs in one transaction, so a malformed file leaves the catalog as it found it. On success it reports how many categories, products, variants, images, options, option values and translations it wrote.
A file exported in a different currency is refused, not converted.
Validation happens up front and fails with a sentence naming the problem: an unknown category parent, a duplicate slug, or a price that is not a decimal string. Run --dry-run first on any file somebody sent you.
The YAML both commands read and write is a narrow subset: block mappings and sequences, quoted and plain scalars, comments. Anchors, flow collections, block scalars and tab indentation are refused by name.
seed
Fill an empty catalog with a demo store.
bin/plugin kahunacart seed
bin/plugin kahunacart seed --purge
| Option | Effect |
|---|---|
--purge |
Remove what the seed file names instead of creating it. Lists everything first and asks. |
It creates a nine-category tree — Clothing → Mens → Tops/Bottoms/Accessories, Clothing → Womens → Tops/Accessories, and a flat Digital Goods — plus eleven products filed on the leaves and tagged organic, new and sale.
The command is idempotent: a slug that already exists is skipped and never updated, so it cannot overwrite a real product.
It is also reversible. --purge removes exactly the slugs the seed file names, deepest categories first, with their pictures. Seeded tags are removed only when nothing else is wearing them. Existing orders are unaffected either way.
Caution
--purge deletes catalog rows. The confirmation defaults to no, so running it with --no-interaction aborts rather than wiping a catalog.
Digital products are seeded with no files attached, and the command says so when it finishes. Until you upload one, a digital order completes normally and grants nothing.
No photographs ship either. Drop a picture named after a product's slug into seed/images/, or after a category's slug into seed/images/categories/, and the seeder imports it. Both directories are optional. See seed/README.md.
webhooks
Show what the payment providers have been sending.
bin/plugin kahunacart webhooks
bin/plugin kahunacart webhooks --provider=stripe --limit=50
bin/plugin kahunacart webhooks --purge
| Option | Default | Effect |
|---|---|---|
--provider=<slug> |
all | Only show deliveries for this provider. |
--limit=<n> |
25 |
How many deliveries to list. Capped at 200. |
--purge |
off | Delete deliveries past the retention window and stop. |
It prints recent deliveries with their time, provider, action, status, reference and error, then per-provider totals with the current failure streak. A streak counts consecutive failed or invalid deliveries since the last success.
A failure streak is reported, never acted on: KahunaCart never disables a provider automatically. Check the provider's webhook secret and endpoint URL first.
--purge sweeps rows older than payments.webhook_log_days, default 30. At 0, deliveries are kept forever and --purge deletes nothing. The worker tick does the same sweep on its own.
With no deliveries recorded, the command names the registered providers instead.
Run the scheduler
If bin/plugin kahunacart work produces results by hand but nothing happens on its own, Grav's scheduler is not installed in cron:
bin/grav scheduler --jobs # is kahunacart-jobs registered?
bin/grav scheduler --install # print the crontab line to add
bin/grav scheduler --run # force a run now, to prove it works
Warning
Without the scheduler in cron, order confirmations never send, download grants are never minted, stock holds never expire, and carts are never purged. This is the most common cause of "the customer never got their download". See Troubleshooting.