CLI
KahunaCart ships forty 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,import,seedandindexrefuse 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. |
store |
--status=<open\|coming_soon\|paused>, --message |
Show whether the store is open, or open, pause or announce it. |
sync |
--provider=<slug>, --push, --pull, --status |
Push or pull the catalog against a sync-capable provider. |
export |
file (optional), --orders, --scope, --set, --columns, --format, --from, --to, --status, --list-columns, --bundle, --only, --except, --no-media, --no-files, --passwords, --secrets, --list-sets |
Write the catalog to a YAML file, write orders to CSV or JSON, or write the whole store into one bundle. |
import |
file (required), --dry-run, --strict, --mapping=<name>, --merge, --full, --only, --except |
Read a catalog file back in, upserting by slug, read a CSV through a saved mapping, or restore a store bundle. |
squarespace |
folder (required), --dry-run, --weight-unit, --dimension-unit, --only, --no-images, --tidy-titles, --products, --orders, --customers, --site |
Bring a whole Squarespace store and site across from a folder of its exports. |
seed |
--purge, --strict, --zones, --currencies=<codes> |
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. |
coupons:generate |
--list, --group=<id\|name>, --count=<n>, --csv |
Mint a batch of unique discount codes into a campaign. |
downloads:check |
--all |
Report every product file a customer could not be handed. |
retention:run |
--dry-run, --limit=<n> |
Run the four data-retention rules now. |
orders:reassign |
--from=<id\|email>, --to=<id\|email>, --dry-run |
Move every order from one customer to another. |
orders:reindex |
--text, --index |
Rebuild the admin order search. |
index |
--rebuild, --dirty, --chunk=<n>, --json |
Build, drain or report on the catalog index. |
search:reindex |
none | Rebuild the storefront product search index from the catalog. |
search:embed |
--reset |
Embed the product search index for semantic search now. |
search:calibrate |
none | Measure semantic search's noise gate on this catalog and print the numbers. |
reports:rebuild |
--from=<date>, --to=<date> |
Recompute the daily report rollup. |
bench:seed |
--products=<n>, --customers=<n>, --orders=<n>, --seed=<n>, --now=<iso>, --batch=<n>, --purge, --force, --json |
Fill the store with a benchmark-sized catalog, customer list and order history. |
bench:queries |
--iterations=<n>, --warmup=<n>, --json |
Time the queries behind the storefront and the admin. |
check |
--offline, --json, --report |
The installation doctor: up to sixteen checks with a suggested fix each. |
reset |
--orders, --customers, --keep-settings, --keep-numbers, --force |
Delete the test data before going live and keep every setting. |
templates:drift |
--all, --json |
List theme template overrides that are behind the shipped templates. |
jobs:list |
--state=<state>, --type=<type>, --limit=<n>, --json |
The job queue from a shell. |
jobs:run |
id (required) |
Run one queued job now, in this process. |
jobs:cancel |
id (required) |
Cancel a pending job, or ask a running one to stop. |
webhooks:subscriptions |
--topics, --limit=<n>, --create, --name, --topic, --url, --ping, --pause, --delete, --deliveries, --redeliver, --purge |
Outbound webhook subscriptions and their delivery log. |
lang:export |
file (optional), --format, --status, --missing=<code> |
Write the translation source, or report how complete each language pack is. |
config:export |
--out=<file>, --include=<sections>, --exclude=<sections> |
Write the store's configuration to a JSON document. |
config:import |
file (required), --dry-run |
Apply a document written by config:export. |
feeds:build |
--feed=<name>, --lang=<code>, --currency=<code>, --out=<dir> |
Build the Google and OpenAI product feeds now. |
agentic:keys |
--rotate |
Show the UCP signing keys, make the first one, or rotate them. |
rates |
--refresh, --source=<code>, --json, --accept=<code>, --keep=<code> |
Show the exchange rates between the store's currencies, fetch them now, or answer a rate that was held. |
make:provider |
name (required), --dir, --title, --description, --author, --vendor, --force |
Scaffold a payment provider add-on. |
make:shipping-method |
the same seven | Scaffold a shipping rate calculator add-on. |
make:discount-operation |
the same seven | Scaffold a discount operation add-on. |
make:admin-section |
the same seven | Scaffold an admin screen add-on. |
make:migration |
name (required), --plugin, --dir, --force |
Write the next numbered, up-only migration file. |
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. A database lock keeps two runs apart.
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. - Catalog index, other currencies — only on a store that sells in more than one currency: whether the catalog index has prices for each of them, or which currencies were added since it was built (shoppers in those cannot sort or filter by price until
index --rebuild), or that the index is off or has never been built. - Jobs — pending, failed and completed counts.
- Providers — slug, label and declared capabilities for each.
- Payment methods by currency — only on a store that sells in more than one currency: one line per currency, the base first, naming the payment methods that take it, and a warning for any currency that nothing takes (a shopper in it cannot check out). Store credit is not counted.
- Exchange rates: only on a store that sells in more than one currency: the rate source in use, and a warning naming any currency whose fetched rate is older than
currencies.rates.max_age_daysworking days or has no rate. A rate you typed yourself never counts as old, and a store on the manual source is never warned.bin/plugin kahunacart rateshas the detail. - Currency and caching — only on a store that sells in more than one currency and reads the country header (
currencies.headeris set andheaderis one of thedetectsteps): one line saying that a plain catalogue URL is always private, that links carry?currency=, and that a CDN has to followCache-Controland bypass its cache when thekc_currencycookie is present. It can't see your CDN, so it sends you to Caching and CDNs.
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: providers register only once their credentials are present. Pending migrations explain most "column not found" errors, and a growing failed-job count means the queue is choking.
store
Says whether the store is open, coming soon or paused, and sets it.
bin/plugin kahunacart store # read it
bin/plugin kahunacart store --status=paused --message="We're moving to a new home. Back on Monday."
bin/plugin kahunacart store --status=coming_soon
bin/plugin kahunacart store --status=open
bin/plugin kahunacart store --message="" # back to the standard sentence
| Option | Effect |
|---|---|
--status |
open, coming_soon or paused. Anything else is refused, with exit code 1. |
--message |
What the banner and every refusal say while the store isn't open. Empty goes back to the standard sentence. Given without --status, it keeps the status the store has. |
It writes store.status and store.status_message to user/config/plugins/kahunacart.yaml, clears Grav's cache the way saving the settings does, and fires onKahunaCartStoreStatusChanged when the status changed. Then it prints the status, and while the store isn't open, what shoppers see. A deploy script pauses a store before its final export with --status=paused and opens the new one with --status=open. See Configuration → Store status.
rates
The exchange rates between the store's currencies, and a way to fetch them now.
bin/plugin kahunacart rates
bin/plugin kahunacart rates --refresh
bin/plugin kahunacart rates --refresh --source=ecb
bin/plugin kahunacart rates --json
bin/plugin kahunacart rates --accept=EGP
bin/plugin kahunacart rates --keep=EGP
| Option | Effect |
|---|---|
--refresh |
Fetch the rates from the configured source now, which is what the daily job does, and then print the list. |
--source=<code> |
With --refresh, fetch from another source for this run instead of currencies.rates.source. |
--json |
Print the list as JSON, the same document as GET /kahunacart/currencies/rates. |
--accept=<code> |
Write the rate a refresh held for that currency, with its source and day. It isn't pinned, so it keeps updating. The same as Accept on the Currencies screen. |
--keep=<code> |
Keep the old rate for that currency and let the held rate go. The same as Keep old rate on the screen. |
With no option it prints the base currency, the rate source and its fallback, and how many working days old a fetched rate may get, then one line per currency the store sells in: its rate (how many of that currency one unit of the base buys), where it came from (ecb, frankfurter, another source, or typed by hand), the day its source published it (rate of 2026-10-09) and when the store last checked it. A rate that counts as old is marked, and a currency with no rate says so. Under the list it says how the last refresh went, and why when it failed.
--refresh also names every rate that changed and how far it moved. A rate you typed is left alone and named. A currency the source doesn't list keeps its old rate and is named, so you know to type one. A rate the fallback answered is named with from frankfurter (or whichever fallback is set). When the source can't answer and the fallback does, the source's reason is printed and the refresh still counts as having worked. When neither can answer, the old rates are kept, the reason is printed and the command exits with 1.
A rate a refresh held because it moved more than currencies.rates.hold_move_percent is listed under the rates, with the new rate and how far it moved. --accept=EGP writes the held rate with its source and day, unpinned, and --keep=EGP lets the hold go and keeps the old rate. Either one exits with 1 and says why when there is no held rate for that currency. See A big move is held for you to check.
On a store that sells in one currency it says there are no rates and does nothing. See Currencies → Exchange rates.
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.quote_email |
The quote message, with the deadline the offer stands until. |
order.gift_card_email |
One gift card's code, one message per card. |
orders.bulk_status |
One bulk status change across a whole selection of orders. |
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, maintenance tasks run on the same tick:
- Expired stock holds are released.
- The abandoned-cart sweep runs when
cart.abandoned_enabledis on, capped at 25 carts per tick and stamping each one. - The low-stock sweep runs, stamping each variant so it warns once per slide under the threshold.
- 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. - The report rollup is kept up. The last 3 closed days are recomputed, so an overnight refund or correction is in the reports by morning. A store whose rollup is behind catches up 60 days per tick until it is current. See Reports and the rollup.
- The review-request sweep runs when
reviews.request_after_daysis set, capped at 25 orders per tick. - The retention schedule runs when any window is set. Every window is off until you set one. Run rows older than a year are purged. See The retention schedule.
- Stale anonymous wishlists are swept per
wishlists.anonymous_days: lists whose cookie is long gone. - Expired quotes are settled, so the outstanding list agrees with the page. Nothing depends on it; a quote past its deadline reads as expired as soon as anybody looks.
A sweep or rollup failure is reported and logged, never fatal. Output looks like:
Jobs: 3 processed, 0 failed. Holds released: 1. Carts purged: 0. Webhook rows swept: 0. Report days rolled up: 3.
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.
Running two workers is safe: each job is processed exactly once. 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. |
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 ignore 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, write orders to CSV or JSON with --orders, or write the whole store into one bundle with --bundle.
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; attribute definitions; products; every variant; option axes and their values; category, tag and attribute assignments; image references; and translated text. Orders, customers, coupons and settings are not included.
Four things matter before you edit an export 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
Export orders
bin/plugin kahunacart export --orders orders.csv
bin/plugin kahunacart export --orders --scope=line_items --format=json lines.json
bin/plugin kahunacart export --orders --set=Accounts --from=2026-01-01 --to=2026-01-31 january.csv
bin/plugin kahunacart export --orders --list-columns --scope=orders
| Option | Effect |
|---|---|
--orders |
Export orders instead of the catalog. |
--scope |
orders, one row per order, or line_items, one row per line. Default orders. |
--set |
The name of a saved column set. Wins over --columns; using both is refused. |
--columns |
Comma-separated column names, in the order you want them. |
--format |
csv or json. Default csv. |
--from, --to |
Completion dates, YYYY-MM-DD. --to includes the whole of that day. |
--status |
Comma-separated order statuses. Every status but cart by default. |
--list-columns |
Print the column vocabulary for --scope and stop. |
This is the same exporter the admin's Export button calls, so both produce the same columns in the same order. Refunded orders are included, money is decimal strings in each order's own currency, dates are ISO 8601 UTC, and orders.export.max_rows caps a run. See Export orders with your own columns.
Export the whole store
bin/plugin kahunacart export --bundle=store.zip --passwords --secrets
bin/plugin kahunacart export --bundle=store.zip --except=orders,credit --no-files
bin/plugin kahunacart export --bundle=2026.zip --only=orders --from=2026-01-01 --to=2026-12-31
bin/plugin kahunacart export --list-sets
| Option | Effect |
|---|---|
--bundle |
Write the whole store into this .zip instead of the catalog. .zip is added when the name has none. |
--only |
Comma-separated part ids to export. Every part by default. |
--except |
Comma-separated part ids to leave out. An id that does not exist is refused, so a typo never quietly exports less. |
--no-media |
Leave the product and category picture files out, which leaves out the media part. The bundle keeps their file names. |
--no-files |
Leave the digital product files out, which leaves out the files part. |
--passwords |
Include customers' hashed sign-in passwords and their own Grav accounts. The hashes are not sealed with the passphrase, so handle the bundle like a database backup. |
--secrets |
Include every key and secret, sealed in secrets.enc with a passphrase of at least 8 characters. The command asks for it twice, or reads KAHUNACART_EXPORT_PASSPHRASE. |
--from, --to |
With --bundle, only orders placed between these dates, YYYY-MM-DD. |
--list-sets |
Print every part this store can export, grouped, with its id, row count and the parts it depends on, and stop. |
The bundle is written in one go, part by part, with a line printed for each. That makes the command the way to export a store too big for the job worker's time limit. The export is recorded in Import & Export → History as run from the command line. A bundle written inside user/data/kahunacart/exports can be downloaded from the admin and is deleted after export.keep_days. One written anywhere else is yours to keep. What a bundle holds, and what it leaves out, is on Export and restore.
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. |
--strict |
Refuse a product that names an attribute the file never declared, instead of creating it as a text attribute. See Attributes in seed and catalog files. |
--mapping=<name> |
Read the file as a CSV through a saved column mapping, named as it is named in the admin, instead of as a YAML catalog file. |
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.
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, rows first and
parent:second, so a file may list a child before its parent. - Existing images are left alone, alt text included.
- Attribute definitions are brought into line with the file: created if missing, updated to match if already here. The type is the one field that can be refused; an attribute products already answer keeps its type, and the rest of the row is still updated.
- Attribute answers are merged per product. A slug in the file is written, one the file does not mention is left alone.
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, translations, sales, attribute definitions and attribute values 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.
Pictures the store doesn't have are named before anything is written. The file records where each product's pictures and media files sit (media:) and where each category's picture sits (image:), never the files themselves. Before a dry run reports, and before a real import starts, the command looks for each of those paths in user/images/kahunacart/products and user/images/kahunacart/categories, and warns about the ones that are missing, by product or category slug. It lists the first twenty and says how many more there are. It is a warning, not a refusal: the import goes ahead, and those products show no picture until the files are copied into place. An embedded video has no file and is not checked.
A file with no products still imports when it carries categories, attributes, manufacturers, custom fields or sales. A file with none of these is refused.
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.
Import a CSV through a saved mapping
--mapping= switches import to the CSV importer: the same engine and the same saved mappings the admin importer uses.
bin/plugin kahunacart import products.csv --mapping="Woo products"
bin/plugin kahunacart import products.csv --mapping="Woo products" --dry-run
The mapping says which entity the file describes, which column is which field, which key rows are matched on, and whether an existing row is updated or skipped. Save one from KahunaCart → Import & Export in the admin. A name that does not exist makes the command list the saved names instead.
A command-line import runs in the foreground, prints its progress, and records a run exactly as the admin's queued import does. An interrupted run resumes when you run the same command again, without duplicating anything. It exits 1 when any row failed, and prints the first ten failures with their line numbers.
--dry-run rehearses the import inside a transaction that is always rolled back, then prints a per-row report. Nothing is written.
See Importing for the entities, the fields, the match keys and the value formats.
Restore a store bundle
A file ending in .zip is read as a store bundle.
bin/plugin kahunacart import store.zip --full --dry-run
bin/plugin kahunacart import store.zip --full
bin/plugin kahunacart import staging.zip --merge --only=catalog,media,promotions
| Option | Effect |
|---|---|
--merge |
Add what the bundle has to this store. Rows matched by their natural key are updated, the rest are created with new ids, and nothing is deleted. Rows with no key of their own, such as a customer's addresses and group memberships or the shipping methods and tax rates under a zone, are only added under a parent the same merge created. An existing customer keeps their addresses, and an existing zone keeps its methods and rates. A catalog merge needs the same base currency on both stores. |
--full |
Restore everything exactly as it was, ids included, into a store with no orders and no customers. It runs once: afterwards the store has both, and another full restore is refused. |
--only |
Comma-separated part ids to restore. Every part in the bundle that can be restored here, by default. |
--except |
Comma-separated part ids to leave out. Leaving out every part is refused, rather than read as "restore everything". |
--dry-run |
Rehearse the database work inside a transaction that is rolled back, and print the report. Nothing is written. Files, accounts, settings files and the rebuilds only happen on the real run, so read its report too. |
--merge or --full is required, and naming both is refused: adding to a store and replacing an empty one are different jobs. When the bundle carries secrets, the command asks for the passphrase or reads KAHUNACART_EXPORT_PASSPHRASE. Leave it empty to restore without them.
The report prints what was created, updated, skipped and failed for each part, what could not run, the keys to enter again, and, when the store moved, the old site's checklist and its redirects: block. The command exits 1 when any row failed. See Export and restore.
squarespace
Bring a whole Squarespace store and site across from a folder of its exports: products, customers, orders, pages and the blog.
bin/plugin kahunacart squarespace ~/Downloads/squarespace --dry-run
bin/plugin kahunacart squarespace ~/Downloads/squarespace --weight-unit=lb --dimension-unit=in
| Flag | Effect |
|---|---|
--dry-run |
Convert and check everything, and write nothing to the store or the pages. |
--weight-unit= |
The unit the Squarespace store weighs in: lb, oz, kg or g. Required when the products file has weights. |
--dimension-unit= |
The unit it measures in: in, cm or mm. Required when the products file has sizes. |
--only= |
Comma-separated steps to run: products, customers, orders, site. |
--no-images |
Don't download pictures. |
--tidy-titles |
Title Case the product titles, move a quoted name to the front and drop "Available in …" tails. Give it your brand (--tidy-titles="Acme") to take the brand off the front of every title. |
--products=, --orders=, --customers=, --site= |
Name the file for a step when the folder has more than one of that kind. |
The converted files (catalog.yaml, customers.csv, orders.csv and redirects.yaml) go in a kahunacart/ folder inside the export folder, so you can open them and see exactly what was imported. It exits 1 when any step failed. See Importing → Moving from Squarespace for which exports to download and what comes across.
seed
Fill an empty catalog with a demo store.
bin/plugin kahunacart seed
bin/plugin kahunacart seed --purge
bin/plugin kahunacart seed --zones --currencies=EUR,GBP
| Option | Effect |
|---|---|
--purge |
Remove what the seed file names instead of creating it. Lists everything first and asks. |
--zones |
Also seed tax and shipping zones for a US store with EU and UK customers. With --purge, remove exactly those zones again. |
--strict |
Refuse a product that names an attribute the file never declared, instead of creating it as a text attribute. See Attributes in seed and catalog files. |
--currencies <codes> |
Also price the demo in these other currencies, a comma-separated list from EUR, GBP, CAD, AUD, CHF and JPY. Writes the prices and one fixed-amount sale, never configuration. See below. |
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.
Two of the products show a backorder: the Tide Line Beanie's Kelp colour at zero with a backorder note, and the Saltwater Socks with one pair left, so two in the cart read "1 of 2 on backorder". See Telling the shopper it is on backorder.
--currencies. seed --currencies=EUR,GBP gives the demo a price in each of those currencies, so a store that sells in more than one has something to look at. It works on a store whose base currency is USD, and it refuses a code it has no rate for, naming the ones it has. Each amount is the base amount times one fixed rate (EUR 0.92, GBP 0.79, CAD 1.37, AUD 1.52, CHF 0.88, JPY 150), tidied so it looks typed and not converted: a price goes to the nearest price ending .99, a coupon amount or a sale amount to a whole number, and in yen both go to the nearest ten. The rate isn't kept and nothing converts later. It prices every variant and its compare-at price, the pay-what-you-want maximum, the seeded coupons' fixed amounts and minimum order totals (the FREESHIP minimum is the demo's free-shipping threshold), and the cost of the shipping methods --zones seeds. It adds one sale with a fixed amount off, Presets Deal, because the demo's own sales are percentages and a percentage needs nothing in another currency. Three products, coldwater-cool-box, saltwater-socks and tide-chart-2027, are left without a euro price on purpose, so "Not available in EUR" is on show. The seed has no priced add-ons, deposits or collection points, so there is nothing to give those.
It never edits configuration. The amounts are written either way and stay hidden until the store sells in the currency, so when a currency isn't in currencies.enabled yet the command prints the currencies: block to add. Run bin/plugin kahunacart index --rebuild once the currencies are on. It fills and never overwrites: an amount the store already has is left alone, so running it twice changes nothing and a price somebody typed survives, and it adds the other currencies to products the seed made on an earlier run. Without the flag the command writes exactly what it always wrote. --purge takes the amounts and the sale away with the rest. See Demo store.
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 and seeded attribute definitions are removed only when nothing else uses 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.
Attributes in seed and catalog files
seed and import read product attributes from the same two places: an attributes: list at the top of the file that declares the definitions, and an attributes: map on each product that answers them by slug.
attributes:
- slug: material
label: Material
type: select
group: Materials
filterable: true
options:
- Organic Cotton
- Merino Wool
- slug: fabric-weight
label: Fabric Weight
type: number
unit: gsm
group: Materials
filterable: true
products:
- slug: reef-runner-tee
attributes:
material: organic-cotton
fabric-weight: 185
machine-washable: true
care: |
Machine wash cold, hang dry.
The attributes: block is optional.
| Key | Behaviour |
|---|---|
slug |
Optional. Derived from label when it is absent. |
label |
Required unless slug is given. |
type |
One of the nine attribute types. Defaults to text. |
options |
Required for select and multiselect. A plain label per entry, or a {slug, label} mapping. |
unit |
A number's unit. Nulled on the other eight types. |
group |
The specification-table heading. group in the file, group_label in the database. |
position |
Optional. Defaults to the order the file lists them in. |
filterable |
Defaults to false. Refused on textarea and url. |
filter_panel |
Defaults to true. Whether a filterable attribute is drawn in the filter panel; off keeps it linked from product pages and honoured in URLs. |
show_on_product |
Defaults to true. |
translations |
Per-language label and group_label overrides. Written by export, read by import. |
A product's attributes: map is keyed by attribute slug. Values go through the same validation as the admin and the API, so a value the type will not take stops the run.
export writes filterable, filter_panel and show_on_product on every attribute, even at their defaults.
An attribute the file never declared
A product may name an attribute with no entry in the attributes: block. By default the command creates it as a text attribute, labels it from the slug, and says so:
note created attribute 'part-number' as text (named by reef-runner-tee, not declared in the attributes block)
A minted definition is always text, so a part number that is all digits does not become a number attribute. Change the type in the admin while the attribute is still unanswered.
--strict turns that warning into a refusal on both seed and import, naming the slug and what the file does define. Use it for a CI import of a file that is supposed to be complete.
Re-running the seeder
seed creates definitions and never updates them, so a definition a merchant has edited is left alone on a second run.
A product that already exists is still skipped, with one exception: the answers it does not yet have are filled in, and it reports filled {slug} (N attribute value(s)). Answers somebody typed are never overwritten.
What --purge removes
The seeded products go first, taking their answers with them. Then each attribute definition the seed file declares is checked:
- A definition nothing answers any more is deleted, and the command prints
removed attribute {slug}. - A definition your own products still answer is kept, values and all, and the command prints
kept attribute {slug} (N product(s) of your own still answer it).
The final line reports products, categories, tags, attribute definitions and attribute values removed.
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 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.
coupons:generate
Mint a batch of unique discount codes into a campaign.
bin/plugin kahunacart coupons:generate --list
bin/plugin kahunacart coupons:generate --group=3 --count=500
bin/plugin kahunacart coupons:generate --group="Autumn insert" --count=10000 --csv > codes.csv
| Option | Default | Effect |
|---|---|---|
--list |
off | Print the campaigns with their code counts and redemptions, then exit. |
--group, -g |
none | Which campaign to mint into: an id, or the campaign's name. A name survives across environments where an id does not. |
--count, -c |
1 |
How many codes, between 1 and 50,000. |
--csv |
off | Print only the codes, one per line under a code header, to redirect straight into a file. |
Codes are drawn from Crockford base32 (the alphabet less I, L, O and U, so a code survives being handwritten and read back) using the campaign's prefix and pattern. The whole batch is checked for collisions and written in one transaction.
A batch larger than half the pattern's keyspace is refused, and the refusal names the pattern so you can widen it.
The campaign itself is created in the admin or through the API, not here.
The exit code is 1 on an unknown campaign, a count out of range, or a pattern too narrow to hold the batch. See Coupons.
orders:reindex
Rebuild the admin order search: the search_text column, the engine text index, or both.
bin/plugin kahunacart orders:reindex
bin/plugin kahunacart orders:reindex --text
bin/plugin kahunacart orders:reindex --index
| Option | Default | Effect |
|---|---|---|
--text |
off | Only rewrite kahunacart_orders.search_text from the order rows, a thousand at a time. |
--index |
off | Only re-decide which text index this engine can offer, and build it. |
| neither | — | Both, which is what you want after a restore or an import. |
Run it when an importer or a SQL script wrote orders directly into the tables, when a release changed what is searchable, or when you restored the database onto a different engine. After a restore onto a different engine, the recorded index no longer belongs to the engine it is sitting on, so search falls back to a LIKE scan until this runs.
The last line names the index the store ended up with, or warns that the store is on the portable LIKE scan. bin/plugin kahunacart status says the same thing without doing any work.
Safe on a live store. The text rewrite is one transaction per batch, so an interrupted run leaves every finished row correct; re-running it is the whole recovery. See Orders and the admin.
index
Build, drain or report on the catalog index: the table behind every listing, facet count, sort and search.
bin/plugin kahunacart index
bin/plugin kahunacart index --rebuild
bin/plugin kahunacart index --dirty --chunk=250
| Option | Default | Effect |
|---|---|---|
--rebuild |
off | Empty the index and build it again from the catalog, in chunks. |
--dirty |
off | Rebuild only the products marked dirty, then stop. |
--chunk=<n> |
1000 |
Products rebuilt per transaction, the same size catalog.index.chunk sets for the background rebuild. Clamped between 1 and 10,000. |
--json |
off | Print a machine-readable summary instead of a report. |
neither --rebuild nor --dirty |
— | Report what is there and change nothing. |
The no-flag report is a table: whether the index is enabled, the catalog.index.mode in force, whether it has ever been built, whether any listing is reading it, its rows and products, how many products are queued, and the text index this database engine gave the store.
Enabled yes
Mode auto
Built yes
Answering listings no (no sale, no price list, no search term)
Rows 12,480
Products 9,910
Queued 0
Search index sqlite_fts5
The search index is one of sqlite_fts5, mysql_fulltext, pgsql_tsvector, or prefix on an engine that offers none of the three.
Note
Answering listings: no is usually correct rather than broken. In the default catalog.index.mode: auto the index is joined only when a sale, a price list or a search term needs it, so a store running no promotions answers its listings with live queries. See The catalog index.
Run --rebuild when:
- You have just updated and want the index for the first time.
- A big import, a SQL script or another tool wrote products directly into the tables.
- You restored the database, especially onto a different engine, whose recorded text index no longer applies.
Run --dirty after a bulk edit or a saved sale when you do not want to wait for the next work tick. That tick drains catalog.index.per_tick products a minute on its own.
A rebuild finishes with a count of products indexed, the rows and products in the index, anything still queued, the search index in use, and the elapsed time. The exit code is 1 only when the schema is out of date.
On a store that sells in more than one currency it adds a line for the extra currencies: whether the index has prices for every one of them, or which were added since it was built, with the command to run. The --json report carries the same under currencies, and a rebuild reports how many price rows it wrote. A store with one currency prints none of this. See More than one currency.
Safe on a live store. A listing hitting a half-built index falls back to live queries rather than to an empty shop, and a killed run leaves finished products correct and the rest queued; run it again to finish.
search:reindex
Rebuild the storefront product search index from the catalog.
bin/plugin kahunacart search:reindex
The command takes no arguments and no options.
Most stores never need it. The index follows every product write on its own, and a store whose index has gone missing rebuilds it on the next search. Run it when none of that applies:
- Something wrote products around the plugin: a SQL script, a restored dump, a migration from another platform,
bench:seed. Detection is a high-water mark on the highest product id, not a checksum, so a bulkUPDATEin place goes unnoticed. - A release changed what goes into a document. The index holds the text as it was read, so a version that starts indexing a new field covers only products saved after the update.
- You would rather pay for the build now than on a shopper's first search. A deploy that does not carry
user/dataacross arrives with no index at all. Put this in the deploy step.
It prints the path it is building, a progress line every 2,500 products, then how many products went in and how long it took. Drafts and unlisted products are not indexed, so the count is the number of products a shopper could browse to. A store with nothing published gets a note saying so, not an error.
On a store that is not on the yetisearch rung — search.products.driver set to like or sql, or a PHP build with no pdo_sqlite and FTS5 — it warns and does nothing: there is no file index to build. bin/plugin kahunacart index --rebuild rebuilds the catalog index's own search instead. See Storefront for the whole ladder.
Safe on a live store, with one caveat: the old index file is removed before the new one is built, so for the second or so the build takes, another request's search falls through to the portable LIKE scan.
search:embed
Embed the product search index for semantic search now, in this shell.
bin/plugin kahunacart search:embed
bin/plugin kahunacart search:embed --reset
| Flag | Default | Effect |
|---|---|---|
--reset |
off | Forget every stored vector first, so the whole catalog is embedded again. |
You don't have to run this. The job worker embeds products by itself after a reindex or a product save. The command is for when you'd rather watch: the first run after you switch semantic search on, a quick check that the API key works, or a deploy step.
It sends the waiting products to the provider in batches of search.products.semantic.batch_limit, prints each batch, and keeps going until nothing is waiting. It has no time limit, unlike the job. On the first error the provider returns, it prints the error and exits 1, which makes it the fastest way to find out what's wrong with a key or a model name.
Changing model, dimensions or a prefix doesn't need --reset, because every product is marked waiting by itself. --reset is for a provider that sent back bad vectors under a model name that didn't change. Every product sent is a call to the provider, and a hosted API charges for those.
When nothing is left waiting, it measures the noise gate if it needs measuring, as search:calibrate does, and prints the min_margin in use. It warns and does nothing when semantic search is off or the store isn't on the yetisearch engine.
search:calibrate
Measure semantic search's noise gate on this catalog now, and print the working.
bin/plugin kahunacart search:calibrate
A search that means nothing scores higher against a big catalog than a small one, so the min_margin that keeps nonsense out of a small store lets it into a big one. This command searches for about ninety nonsense strings (made-up words, keyboard mashes, placeholder Latin and everyday phrases nobody shops for), each one embedded exactly as a shopper's search would be, and measures how far each one's best product card stands above the median card. A string that keywords actually find in your catalog isn't nonsense there, so it's left out, and the command names it.
It prints the spread of those margins (minimum, median, mean, standard deviation, 95th percentile and maximum) and the min_margin chosen from them: the mean plus calibration_strictness standard deviations, two by default. It also says whether the gate is using it. It is while search.products.semantic.calibration is auto. With off, your configured min_margin applies and the measurement is only reported.
You don't need to run it. The worker calibrates by itself at the end of an embedding run whenever the numbers are out of date: there are none yet, the model or query_frame changed, or the catalog grew or shrank by more than a quarter. This command is for seeing the numbers.
The first run with a model embeds the probes in one call to the provider, a fraction of a cent on a hosted API, and caches them, so running it again costs nothing until the index is rebuilt. It exits 1 while products are still waiting to be embedded or on a provider error, and does nothing on a catalog of fewer than ten products, where the gate never applies. See Storefront → Tuning semantic search.
reports:rebuild
Recompute the daily report rollup: kahunacart_order_stats and kahunacart_product_stats.
bin/plugin kahunacart reports:rebuild
bin/plugin kahunacart reports:rebuild --from=2026-01-01 --to=2026-01-31
| Option | Default | Effect |
|---|---|---|
--from=YYYY-MM-DD |
The store's first trading day | First day to recompute, inclusive. |
--to=YYYY-MM-DD |
Yesterday | Last day to recompute, inclusive. |
Today is never rolled up, because the reports scan it live, so --to is clamped to yesterday.
Recomputing is idempotent: a day's row is rebuilt from that day's rows rather than adjusted by a delta. Safe on a live store and in a deploy script.
Run it after an import, after a restore that carried the orders and not the rollup, or after editing the lines of an order on a day that had already closed. See Reports and the rollup for when that is needed.
The last line says how many days were recomputed and the date the rollup is now authoritative through.
downloads:check
Report every product file a customer could not actually be handed.
bin/plugin kahunacart downloads:check
bin/plugin kahunacart downloads:check --all
| Option | Default | Effect |
|---|---|---|
--all |
off | Also list the files that are fine, one per line. |
It prints the allowed roots first — downloads.path plus anything in downloads.allowed_roots — then a table of the problems, with the file id, product, name, stored path and reason:
| Problem | What happened |
|---|---|
missing |
The row points at a path with nothing on the end of it. A customer clicking that link spends a download and gets an error page. |
outside roots |
The bytes exist, but somewhere the allowlist does not name, so delivery refuses them. Usually a store that moved its files and changed downloads.path without adding the old location. |
The command is read-only. It exits 1 when it found a problem and 0 otherwise, including on a store with no product files, so it works as a deploy check.
Tip
Run it after moving the files root, and after any deploy that changes downloads.path or downloads.allowed_roots. See Hardening → Restrict where downloadable files may live.
retention:run
Run the data-retention sweep now: archive placed orders whose payment never arrived, archive orders whose payment failed, anonymise completed sales, and erase inactive customers.
bin/plugin kahunacart retention:run --dry-run
bin/plugin kahunacart retention:run
bin/plugin kahunacart retention:run --limit=1000
| Option | Effect |
|---|---|
--dry-run |
Report what would happen and change nothing. The log rows are still written. |
--limit=N |
Rows one rule may touch this run, overriding retention.chunk. |
Every window is off until you set one, so on an unconfigured store this prints which keys to set and exits 0. Otherwise the output is a table of rule, window, cutoff date, rows examined, rows affected and any error, followed by the order numbers each rule touched.
Completed orders are anonymised rather than deleted, so the store's revenue figures do not move. Trashing means archiving, and the two trash rules delete nothing.
Caution
A store switching the schedule on for the first time has years of orders in scope, and there is no undo. Always --dry-run first.
The same four rules run from the worker tick, so a store with the scheduler installed needs this command only to check its settings or to catch up in a hurry. See The retention schedule.
orders:reassign
Move every order from one customer to another, with its history, its notes, its refunds and its download grants.
bin/plugin kahunacart orders:reassign --from=42 --to=57 --dry-run
bin/plugin kahunacart orders:reassign -[email protected] -[email protected]
| Option | Effect |
|---|---|
--from |
The customer to move orders from: an id or an email address. |
--to |
The customer to move them to: an id or an email address. |
--dry-run |
List exactly what would move and write nothing. |
Transactions, refund lines and everything else hanging off an order travel with it. Each order gains a history entry naming the move.
The exit code is 1 on an unmatched --from or --to, or when the two resolve to the same customer. See Reassign orders to another customer.
bench:seed
Fill the store with a benchmark-sized catalog, customer list and order history.
bin/plugin kahunacart bench:seed
bin/plugin kahunacart bench:seed --products=1000 --customers=5000 --orders=20000
bin/plugin kahunacart bench:seed --json > seed.json
bin/plugin kahunacart bench:seed --purge
| Option | Default | Effect |
|---|---|---|
--products |
5000 |
How many products to create. |
--customers |
25000 |
How many customers to create. |
--orders |
250000 |
How many orders to create. |
--seed |
20260830 |
The integer every value derives from. |
--now |
2026-08-30T00:00:00Z |
The instant the store is seeded as of. Every date is an offset from it. |
--batch |
1000 |
Rows per INSERT transaction. |
--purge |
off | Remove everything the benchmark seeded, and stop. |
--force |
off | Seed even though the store holds orders the benchmark did not write. |
--json |
off | Print a machine-readable summary instead of a report. |
The default size takes about 30 seconds on an M-series Mac with SQLite and writes roughly 4.5 million rows, about 1.5 GB. --purge takes it back out in about 12 seconds.
Every row the seeder writes carries a bench- slug, a BENCH- order number, a BENCH coupon code or an address at bench.kahunacart.test, so --purge finds exactly its own rows and leaves everything else alone. The command counts the orders that are not its own before it starts and exits 1 when it finds any, unless you pass --force.
Caution
Never run this against a live store. It is reversible, but a quarter of a million orders is not something to discover by accident.
See Benchmarking for what the seeded store contains and how to compare the three database engines.
bench:queries
Time the queries behind the storefront and the admin against whatever the store now holds.
bin/plugin kahunacart bench:queries
bin/plugin kahunacart bench:queries --iterations=100 --warmup=10
bin/plugin kahunacart bench:queries --json > queries.json
| Option | Default | Effect |
|---|---|---|
--iterations |
50 |
How many timed runs per query. |
--warmup |
5 |
How many untimed runs first. |
--json |
off | Print a machine-readable document instead of a table. |
It prints fourteen queries with min, p50, p95 and max in milliseconds and the row count each returned. Each calls the same repository and service methods the storefront and the admin call, in process, so the figure is the database alone, without PHP, Twig or the web server.
The command is read-only in effect: the cart it prices is removed afterwards and the stock it takes is put back, so a second run measures the same store as the first.
Both --json documents carry the plugin version, the git commit, the engine and its version, the PHP version, the seed and the instant.
check
The installation doctor: up to sixteen checks, each with a suggested fix. Every check has its own section in Going live, which is where a finding points.
bin/plugin kahunacart check
bin/plugin kahunacart check --offline
bin/plugin kahunacart check --json
bin/plugin kahunacart check --report > support.txt
| Option | Default | Effect |
|---|---|---|
--offline |
off | Skip the two network probes: the provider connection checks, and a ping per active outbound subscription. |
--json |
off | Print the findings as JSON. |
--report |
off | Print the redacted support report — findings, environment and configuration with every secret-looking key replaced — to paste into a ticket. |
Thirteen always run: the database engine and its strict mode, pending migrations, the scheduler, the mail transport, the worker environment, download roots, trusted proxies, the site address, the store status, provider credentials, webhook reachability, template drift and the report rollup.
While the shop is mounted at the site root, the store route check joins them. While agentic.enabled is on, agentic readiness joins them, and while semantic product search is on, so does the semantic search check.
The report rollup is the one check that can only warn. The rollup was last built against an older set of measures, usually because the store upgraded past the migration that added cost of goods. The figures stay correct, just worked out from the orders table rather than read from a day row. Fix it with bin/plugin kahunacart reports:rebuild. The dashboard says the same through its The report rollup needs rebuilding alert. See Analytics and Dashboard alerts.
The exit code is 0 when every check passes or warns and 1 when any fails, so a deploy script can gate on it. A store with demo.readonly on skips the network probes whether you asked for --offline or not. Nothing writes apart from the pings, which land in the delivery log, so it is safe to re-run. The admin's Health screen runs the same checks.
reset
Wipe the test data before going live and keep every setting. See Going live → Before the checks.
bin/plugin kahunacart reset --orders --keep-settings
bin/plugin kahunacart reset --orders --customers --keep-settings
bin/plugin kahunacart reset --orders --keep-settings --keep-numbers --force
| Option | Default | Effect |
|---|---|---|
--orders |
off | Empty every table that hangs off an order. |
--customers |
off | Empty the customer rows with their sign-in accounts, addresses, wishlists and reviews. |
--keep-settings |
— | Required. It makes the command line say out loud what survives. |
--keep-numbers |
off | Leave the order-number sequence where it is instead of restarting it. |
--force |
off | Answer the confirmation yes. |
--orders empties orders, lines, adjustments, transactions, refund items, shipments, history, order changes, download grants, stock holds, coupon usages, credit notes, gift cards, the ledger, the report rollups, the job queue, both webhook logs, rate-limit counters, and the retention and import runs. With --customers alone the orders stay and become guest orders.
Products, categories, tax, shipping, coupons, payment methods and settings are never on either list. Coupon usage counters go back to zero, the merchant-notification dedupe stamps are cleared, and the order-number sequence restarts at order_number_start unless you pass --keep-numbers.
The plan — every table with its row count — is printed first, and the question defaults to no.
Caution
This is not reversible. Take a backup first. It is refused outright on a store with demo.readonly on.
templates:drift
List every theme override of a shipped template that has fallen behind it.
bin/plugin kahunacart templates:drift
bin/plugin kahunacart templates:drift --all
bin/plugin kahunacart templates:drift --json
| Option | Default | Effect |
|---|---|---|
--all |
off | List every shipped template with the state of its override, not only the drifted ones. |
--json |
off | Print the rows as JSON. |
An override is drifted when its @version stamp is lower than the shipped template's, or when it carries no stamp at all. Both versions are printed. The exit code is 1 when anything is drifted, so it can gate a deploy after a plugin update. See Theming.
jobs:list
The job queue from a shell. It reads the same rows as the admin's Jobs screen.
bin/plugin kahunacart jobs:list
bin/plugin kahunacart jobs:list --state=failed --limit=100
bin/plugin kahunacart jobs:list --type=order.confirmation_email --json
| Option | Default | Effect |
|---|---|---|
--state=<state> |
every state | One of pending, running, complete, failed or cancelled. |
--type=<type> |
every type | One job type, such as order.confirmation_email. |
--limit=<n> |
25 |
How many jobs to list. |
--json |
off | Print the page as JSON, which is the only form that includes each job's arguments. |
Each row carries the state, when the job was queued and started, how long it took, how many attempts it has had, and the last error. Finished jobs are swept after jobs.retention_days, so a job that has aged out of this list is not a job that vanished.
jobs:run
Run one queued job now, in this process, whatever it was waiting for.
bin/plugin kahunacart jobs:run 4821
| Argument | Effect |
|---|---|
id |
The job id, from jobs:list. Required. |
A failed job is granted one more attempt, and the outcome is printed. Use it when a customer says they never got their confirmation and you have the job id.
jobs:cancel
Cancel a pending job, or ask a running one to stop.
bin/plugin kahunacart jobs:cancel 4821
| Argument | Effect |
|---|---|
id |
The job id, from jobs:list. Required. |
A pending job is cancelled outright. A running one is asked to stop at the next transaction boundary, so a bulk status change stops between orders rather than mid-write.
webhooks:subscriptions
Outbound webhook subscriptions and their delivery log. This is the other direction from webhooks, which is the log of what payment providers send to the store. See Outbound webhooks.
bin/plugin kahunacart webhooks:subscriptions
bin/plugin kahunacart webhooks:subscriptions --topics
bin/plugin kahunacart webhooks:subscriptions --create --name="Warehouse" --topic=order.completed --url=https://wms.example/hook
bin/plugin kahunacart webhooks:subscriptions --ping=3
bin/plugin kahunacart webhooks:subscriptions --deliveries=3 --limit=100
bin/plugin kahunacart webhooks:subscriptions --redeliver=9912
bin/plugin kahunacart webhooks:subscriptions --purge
| Option | Default | Effect |
|---|---|---|
--topics |
off | Print every topic a subscription may name, and stop. |
--limit=<n> |
25 |
How many rows to list. |
--create |
off | Create a subscription from --name, --topic and --url. |
--name=<text> |
— | With --create: what to call it. |
--topic=<topic> |
— | With --create: order.completed, order.*, or event.<Name>. |
--url=<url> |
— | With --create: the https endpoint to POST to. |
--ping=<id> |
— | Send a signed ping to that subscription. A 2xx makes it active. |
--pause=<id> |
— | Pause that subscription. |
--delete=<id> |
— | Remove that subscription. |
--deliveries[=<id>] |
off | List recent delivery attempts, for every subscription or for one. |
--redeliver=<id> |
— | Queue that delivery row's occurrence again, as a new attempt. |
--purge |
off | Delete delivery attempts past webhooks.retention_days and stop. |
--create prints the signing secret once, as the admin does; copy it before the command exits. --deliveries is one row per attempt rather than per occurrence. --purge does now what the worker's tick does on its own.
lang:export
Write the translation source — languages/en.yaml with its comments stripped — for upload to the community translation project, or report how complete each language pack is.
bin/plugin kahunacart lang:export # the source as YAML, to stdout
bin/plugin kahunacart lang:export source.yaml # or to a file
bin/plugin kahunacart lang:export --format=json # as a flat key-to-string map
bin/plugin kahunacart lang:export --status # one row per pack
bin/plugin kahunacart lang:export --status --missing=de # the keys de.yaml still lacks
| Argument or option | Default | Effect |
|---|---|---|
file |
stdout | Where to write it. Omit it, or pass -, to print to stdout. |
--format |
yaml |
yaml or json. |
--status |
off | Print the coverage table — translated, missing, stale, per language — and stop. |
--missing=<code> |
— | With --status, list the untranslated keys for one language, and any stale keys it carries. |
A key whose English value is intentionally blank is not counted as a string to translate. See Translating KahunaCart.
config:export
Write this store's configuration — the plugin's YAML and the commercial policy the database holds — to a JSON document.
bin/plugin kahunacart config:export # to stdout, so it can be piped
bin/plugin kahunacart config:export --out=store.json
bin/plugin kahunacart config:export --out=tax.json --include=tax,shipping
bin/plugin kahunacart config:export --out=store.json --exclude=report_views
| Option | Default | Effect |
|---|---|---|
--out=<file> |
stdout | Where to write it. |
--include=<sections> |
every section | Only these sections, comma-separated. |
--exclude=<sections> |
none | Everything but these sections, comma-separated. |
The six sections are plugin, settings, tax, shipping, promotions and report_views. A name that is not one of those is refused, with the list.
| Section | Holds |
|---|---|
plugin |
The plugin's own YAML configuration, every key on Configuration |
settings |
The kahunacart_settings table: invoice counters, feature flags, property labels and the rest |
tax |
Tax zones, rates and their rules |
shipping |
Shipping zones, methods, rates and pickup locations |
promotions |
Coupons, sales and price lists |
report_views |
Saved report views and per-user column choices |
This is not the catalog, which is export, and not orders or customers. Nor is it the store status (store.status and its message), which belongs to the site. See Configuration → Store status.
Important
Secrets leave as environment references, never as values: every key the exporter recognises as a secret is written as $KAHUNACART_…. The document is safe to commit, and the store it lands on reads the real value from its own environment. See Configuration → Environment variables for the eight patterns that count as a secret.
Two rows are never exported: reports.stats_through and reports.stats_schema. They describe the state of this store's own rollup.
config:import
Apply a document written by config:export.
bin/plugin kahunacart config:import store.json --dry-run # say what would happen, write nothing
bin/plugin kahunacart config:import store.json
| Argument or option | Default | Effect |
|---|---|---|
file |
— | The export document to apply. Required. |
--dry-run |
off | Report the plan and write nothing at all. |
Four guarantees:
--dry-runwrites nothing. The transaction is never opened and the YAML file is not touched. Run it first on anything you did not export yourself.- The document is validated end to end before anything is written. Every problem is reported together, and a document with any problem writes nothing.
- The import runs inside one transaction, and is idempotent. Applying the same document twice gives the same store as applying it once.
- A section the document does not carry is not touched. A document holding only tax replaces only tax.
And the store stays as open as it was. Whatever the plugin section says under store, the file keeps this site's store.status and store.status_message, so importing staging's configuration into the live store neither pauses nor opens it.
The YAML half gets a stamped backup beside it before it is written, and the command prints where. A file cannot join a database transaction, so that is the half to put back by hand. Comments in the configuration file are not preserved; the values are.
feeds:build
Build the Google Merchant Center and OpenAI product feeds now, into the same place the background job builds them, so the files are served straight away.
bin/plugin kahunacart feeds:build # every enabled feed, every language
bin/plugin kahunacart feeds:build --feed=google # one feed: google, google-checkout or openai
bin/plugin kahunacart feeds:build --lang=de # one language
bin/plugin kahunacart feeds:build --currency=EUR # one currency, on a store that sells in more than one
bin/plugin kahunacart feeds:build --out=/var/feeds # also copy the files there, for sftp or rsync
| Option | Default | Effect |
|---|---|---|
--feed |
every enabled feed | Build one feed: google, google-checkout or openai. A feed named here is built even while it is switched off, which is how a store that only pushes the OpenAI file runs with feeds.enabled off. |
--lang |
every language | Build one of the site's languages. |
--currency |
every currency | Build one currency's files, on a store that sells in more than one currency. |
--out |
none | Also copy the files to this directory, one subdirectory per language on a multilanguage site and one per currency other than the store's own. |
It prints each file with its language, row count, the start of its SHA-256 and where it was written. Only a build with none of --feed, --lang and --currency deletes the files of feeds, languages and currencies that are no longer on. See Product feeds.
agentic:keys
Show the UCP signing keys, and make the first one when there is none.
bin/plugin kahunacart agentic:keys
bin/plugin kahunacart agentic:keys --rotate
--rotate adds a new key and retires the current one, which stays in /.well-known/ucp for seven days so platforms can still verify webhooks it signed. The command prints each key's id, when it was made and whether it is current or retired, then either the profile URL or every readiness problem still keeping /.well-known/ucp at 404. The keys are encrypted with the site secret. If that secret changed and the keys can no longer be read, the command says so and --rotate replaces them. See UCP checkout → Signing keys.
Scaffolding: the make: commands
Five commands that write a working starting point, so you edit code rather than create it.
make:provider, make:shipping-method, make:discount-operation, make:admin-section
Scaffold a complete, installable KahunaCart add-on plugin.
bin/plugin kahunacart make:provider warranty
bin/plugin kahunacart make:shipping-method regional --title="Regional Courier"
bin/plugin kahunacart make:discount-operation bundle --vendor=acme --author="A Merchant"
bin/plugin kahunacart make:admin-section returns --dir=user/plugins/kahunacart-returns --force
| Argument or option | Default | Effect |
|---|---|---|
name |
— | The name after kahunacart-, such as warranty. Lowercase letters, digits and single dashes. Required. |
--dir=<path> |
user/plugins/kahunacart-<name> |
Where to write it. |
--title=<text> |
the name in title case | The human name. |
--description=<text> |
— | One sentence for the blueprint and composer.json. |
--author=<name> |
— | The author name for the blueprint. |
--vendor=<name> |
— | The Composer vendor name. |
--force |
off | Overwrite files that already exist. |
All four take the same arguments and write the same skeleton: the plugin class, its config defaults, a blueprint with the admin form, a language file, a composer.json, a phpunit.xml, a test bootstrap that works before composer install, a README, a CHANGELOG and a .gitignore. Each also writes the one class that does the work, and a test that proves it does.
| Command | The class it writes | What it registers |
|---|---|---|
make:provider |
classes/<Name>Provider.php |
A payment provider, on onKahunaCartRegisterProviders |
make:shipping-method |
classes/<Name>ShippingMethod.php |
A shipping rate calculator |
make:discount-operation |
classes/<Name>Operation.php |
A discount operation, on the operation registry |
make:admin-section |
classes/<Name>Section.php and admin-next/sections/<name>.js |
An admin screen, on onKahunaCartAdminSections |
The generated test passes as written. Run it, then replace the TODO markers:
bin/plugin kahunacart make:provider warranty
cd user/plugins/kahunacart-warranty && composer install && vendor/bin/phpunit
Then run bin/grav clear, enable the plugin under Plugins, and check bin/plugin kahunacart status for what KahunaCart now sees registered. See Provider developer guide for what each kind has to implement.
make:migration
Write the next numbered, up-only migration file.
bin/plugin kahunacart make:migration warranty_registrations
bin/plugin kahunacart make:migration warranty_registrations --plugin=kahunacart-warranty
| Argument or option | Default | Effect |
|---|---|---|
name |
— | What it does, in lowercase and underscores, such as warranty_registrations. Required. |
--plugin=<slug> |
the base plugin | The add-on slug whose migrations/ directory gets the file. |
--dir=<path> |
— | The migrations directory to write into. Overrides the default. |
--force |
off | Overwrite a file that already exists. |
The number is the next after the highest already in that directory, and a gap left by a reserved number is never filled.
An add-on's migration carries the add-on's own prefix, warranty_0001_registrations.php, because migration names are global across every plugin sharing the store's tracking table.
Apply it with bin/plugin kahunacart migrate. An add-on has to register its own migrations/ directory on onKahunaCartRegisterMigrations first.
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.
On a store with a user/env/<host>/ folder, the cron line --install prints runs the scheduler, and the worker it starts, as the cli environment, which reads user/config and never that folder. Admin Next saves settings into the folder when it exists, so the worker can end up sending email with settings the site does not use. Start the cron line with GRAV_ENVIRONMENT=<host>, set it in the root .env, or keep the settings in user/config:
* * * * * cd /path/to/grav && GRAV_ENVIRONMENT=shop.example.com php bin/grav scheduler 1>> /dev/null 2>&1
bin/plugin kahunacart check says when this is happening. See Going live → Worker environment.
Newsletters add-on commands
The Newsletters add-on has its own command namespace, bin/plugin kahunacart-newsletters. Everything else it needs is already a base plugin command: migrations are migrate, the worker that runs its imports and sends its campaigns is work, and its tables appear in the plan reset prints.
| Command | What it does |
|---|---|
bin/plugin kahunacart-newsletters deliverability:check |
Check why this store's mail is or is not landing, and answer 1 if anything failed |
bin/plugin kahunacart-newsletters export:csv |
Write the subscriber list as a CSV, with the same filters the admin table takes |
bin/plugin kahunacart-newsletters import:csv |
Import a contact list from a CSV, deduplicating by address and honoring suppressions |
bin/plugin kahunacart-newsletters import:customers |
Put the store's own customers on a list, inviting them or adding them as subscribed |
bin/plugin kahunacart-newsletters seed |
Fill the newsletter with a demo audience, campaigns and automations (idempotent; --purge removes it again) |
bin/plugin kahunacart-newsletters segments:count |
Recount segments now, rather than waiting for the worker |
bin/plugin kahunacart-newsletters segments:preview |
Show how many people a segment is about, who is left out, and ten of them |
bin/plugin kahunacart-newsletters webhook:replay |
Run a saved provider webhook payload through its provider and say what it would do |
Warning
reset --customers empties everything the newsletter owns except the suppression list: the addresses this store has been told never to mail again. Remove one on the Suppressions screen instead.
Subscriptions add-on commands
The Subscriptions add-on has one command of its own, for demo stores.
| Command | What it does |
|---|---|
bin/plugin kahunacart-subscriptions seed |
Fill the store with demo subscriptions, plans and renewal orders (idempotent; --purge removes them again) |
Shipping add-on commands
The Shipping add-on's commands are under bin/plugin kahunacart-shipping. Its migrations, jobs and resets go through the base plugin's migrate, work and reset. See Developers.
| Command | What it does |
|---|---|
bin/plugin kahunacart-shipping demo:tick |
Move every open demo parcel to wherever its clock says it is |
bin/plugin kahunacart-shipping seed |
Fill the store with the demo carrier's parcels — boxes, a ship-from address, two carriers, and two months of orders with tracking on them (idempotent; --purge removes it again) |
bin/plugin kahunacart-shipping webhook:register |
Register this store's inbound tracking webhook with the aggregator (adopts an existing one; --reset replaces it) |
bin/plugin kahunacart-shipping webhook:secret |
Generate and save the tracking webhook's secret — the random part of its address, and what the aggregator's signature is derived from |
Bookings add-on commands
The Bookings add-on's commands are under bin/plugin kahunacart-bookings. See Developers.
| Command | What it does |
|---|---|
bin/plugin kahunacart-bookings seed |
Set a whole business up from nothing — people, hours, products and rules |
bin/plugin kahunacart-bookings slots |
When a booking product can be booked, and with --explain, why a time is not offered |
Rentals add-on commands
The Rentals add-on's commands are under bin/plugin kahunacart-rentals. See Developers.
| Command | What it does |
|---|---|
bin/plugin kahunacart-rentals nights |
Which nights a property can be slept in, and what a stay would cost |
bin/plugin kahunacart-rentals seed |
Set a whole property up from nothing — hours, product, rules, rate card and a few stays |
Catalogue export and import keep sale operations’ named money arguments in argument_amounts. The amount argument uses amounts, and fixed quantity-tier amounts stay beside their rungs. See Currencies → In the catalogue file.
seed --purge also checks and removes seeded foreign amounts written before a currency was enabled or after it was disabled. It does this even on a store currently selling only in the base currency.
Related
- Configuration
- Products and catalog
- The catalog index
- Storefront
- Importing
- Sync
- Orders and the admin
- Customers, wishlists, reviews and retention
- Reports and the rollup
- Analytics
- Dashboard alerts
- Theming
- Provider developer guide
- Coupons
- Hardening
- Benchmarking
- Newsletters
- Troubleshooting
- Agentic commerce
- Currencies