From vctl
Generic GraphQL CLI (vctl) for the Vendure admin API across environments. Use for reads, writes, business shop exports, and any admin-API operation. Portable — works from any machine with HTTPS access to the backend; kubectl optional.
How this skill is triggered — by the user, by Claude, or both
Slash command
/vctl:remote-vendure-accessWhen to use
- Running GraphQL queries or mutations against the Vendure admin API - Exporting a business shop to JSON for local analysis (audit, diff, regen-slugs) - Schema discovery — input types, mutation signatures, return shapes - Batch data fixes via admin API (set-batch, apply-diff) - Asset and side-order-group lifecycle helpers - Running or managing saved operations / reusable fragments (recurring diagnostics)
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Generic GraphQL client for the Vendure admin API. Designed so agents can run _any_ mutation without hardcoding each operation: the selection set in the query tells the server what to return, so the CLI is a pure passthrough.
Generic GraphQL client for the Vendure admin API. Designed so agents can run any mutation without hardcoding each operation: the selection set in the query tells the server what to return, so the CLI is a pure passthrough.
Need raw SQL instead? See the sibling remote-database-access skill — for hygiene writes (user.identifier, authentication_method), cross-row diagnostic queries, and DBA work that has no admin-API surface. That skill requires kubectl. This one does not.
⚠️ Read
references/backend-quirks.mdbefore bulk price work, collection edits, or side-order surgery. It documents the money-critical tax-inclusive pricing semantics (writepriceWithTax, notprice), the customized schema (Collection.isRootis not queryable), how collection membership actually works (product-id-filterJSON arrays), and side-order-group full-array rewrite rules.
📘 Skim
references/domain-core.mdbefore reaching fordescribe. It primes you on the ~8 types that drive most admin-API work here (Business,Conversation,AIChannel,SideOrderGroup,ServiceSchedule, plus theirUpdate*Inputshapes) with narrative + schema fragments.describeis still the source of truth for the long tail.
📤 Output streams: the
PROD/STAGINGbanner and all progress/info lines go to stderr; stdout is clean JSON only. Capture JSON with2>/dev/null. Neversed '1d'to "strip the banner" — there is none on stdout, so you'd delete the first line of real JSON.
| Tool | Purpose |
|---|---|
scripts/vctl | Vendure admin API CLI — auth, channel routing, batching, type-introspection |
VCTL=".agents/skills/remote-vendure-access/scripts/vctl"
On Windows, use the bundled wrapper:
$VCTL = "skills\remote-vendure-access\scripts\vctl.cmd"
# Generic GraphQL — the main interface
$VCTL <profile> gql '<query>'
$VCTL <profile> gql -f mutation.gql --var productId=123
$VCTL <profile> gql '{products(options:{take:5}){items{id name}}}'
# Authentication (session cached per profile)
$VCTL <profile> login # force re-auth
$VCTL <profile> whoami # verify session
$VCTL <profile> clear-session # log out
describeReads libs/vendure/types/admin/src/lib/vendure-types-admin.ts (committed to the repo, regenerated by yarn generate-types). No port-forward or auth needed — schema lookups are instant and offline-capable.
$VCTL describe UpdateProductInput # input type fields
$VCTL describe updateProduct # mutation signature
$VCTL describe businesses # query signature
$VCTL describe SideOrderGroupType # enum values
$VCTL describe UpdateBusinessResult # union members
# Force live API introspection (when run outside the repo, or to bypass stale cache)
$VCTL <profile> describe updateProduct --live
# Dump full schema (always live)
$VCTL <profile> schema -o schema.json
The generated TS file mirrors what the frontend uses — if a type exists there, it works on the live API. The walk-up search finds the file from anywhere inside the marketplace checkout; if not found, vctl falls back to live __schema introspection automatically.
Stored in ~/.config/vctl/profiles.json:
{
"staging": {
"user": "superadmin",
"password": "…",
"kubectl_tunnel": { "namespace": "qlax-staging", "service": "orderbro-de-backend", "remote_port": 80 }
},
"prod": { "user": "…", "password": "…", "url": "https://api.example.com" }
}
kubectl_tunnel — auto-starts a port-forward; useful inside the office VPN, not portableurl — direct HTTPS; works from any machine with backend network access, no kubectl requiredVENDURE_URL / VENDURE_USER / VENDURE_PASSWORD) or flags (--url, --user, --password)bash $VCTL configure <profile> walks you through setup$VCTL <profile> channel pizzeria-delice-dinslaken # set default channel
$VCTL <profile> gql --channel <token> '...' # one-off override
$VCTL <profile> channel clear # unset
Update a product name (explicit selection set):
$VCTL staging gql '
mutation($id:ID!,$name:String!){
updateProduct(input:{id:$id, translations:[{languageCode:de,name:$name}]}){
id name
}
}' --var id=123 --var name='"New Name"'
Batch update variants (updateProductVariants takes array):
$VCTL staging gql '
mutation($inputs:[UpdateProductVariantInput!]!){
updateProductVariants(input:$inputs){ id name enabled price }
}' --vars '{"inputs":[{"id":"1","price":500},{"id":"2","price":600}]}'
Use describe to discover any mutation's input shape before writing the call.
run / op / frag — saved operations & fragmentsA library of reusable GraphQL, so a recurring diagnostic becomes one command instead of a re-typed query. Two pieces:
run....Name and inlined automatically at run time.Stored as plain .graphql files in two roots, searched repo-first:
.agents/skills/remote-vendure-access/library/ # committed, travels with the skill
fragments/ operations/
~/.config/vctl/library/{fragments,operations}/ # personal overlay (--user)
Repo wins name collisions — a personal op can't silently shadow a shared one. All of run/op/frag are offline (no profile, port-forward, or auth) except run without --dry-run, which executes.
$VCTL <profile> run sog-dup-audit --channel <token>
$VCTL run sog-dup-audit --var 'options={"take":50}' --dry-run # preview, offline
--vars blob → individual --var.--dry-run prints the resolved document (fragments inlined) + merged variables without executing — review before it fires.$VCTL op list # name, source (repo/user), description
$VCTL op show sog-dup-audit # resolved document + defaults
$VCTL op save my-audit -f query.graphql --var 'options={"take":1000}' --desc 'what it does'
$VCTL op save my-audit 'query Q($id:ID!){ product(id:$id){id name} }' --user
$VCTL op rm my-audit [--user]
--desc is stored as a leading # comment and shown in op list.--var on save sets default variables (written to <name>.vars.json).save defaults to the repo library (reviewable/committable); --user targets your personal overlay.$VCTL frag list # name + the type each is `on`
$VCTL frag show SogWithItems
$VCTL frag save SogWithItems -f frag.graphql [--user]
$VCTL frag rm SogWithItems [--user]
Fragments an operation references are resolved transitively and appended to the document. A missing fragment fails run/op show with unresolved fragment(s): …; saving an op that references a not-yet-created fragment only warns.
| Name | Kind | Purpose |
|---|---|---|
sog-dup-audit | operation | every SOG + its side-orders, to find side-order IDs duplicated across SOGs (the 4× overcharge bug). Pass --channel. |
SogWithItems | fragment | SideOrderGroup shape used by sog-dup-audit |
This is the home for the cross-row diagnostics that previously needed raw SQL — checked in, reproducible, runnable from any machine with backend access.
set — shorthand for common field updatesFor high-frequency single-field updates, set avoids raw GraphQL and auto-resolves translation IDs for name/slug.
$VCTL <profile> set <entity> <field> <id> <value>
| Entity | Supported fields | Example |
|---|---|---|
product | name, slug, enabled, featuredAssetId, assetIds | set product name 11746 'Pizza Piccante' |
variant | name, sku, enabled, price, featuredAssetId, assetIds | set variant price 17566 950 |
collection | name, slug, featuredAssetId, assetIds | set collection name 1632 'Pizza' |
sog | name | set sog name 412 'Beilagen' |
enabled accepts true/false/1/0price is integer cents (e.g. 950 = 9,50 €). ⚠️ Channels are tax-inclusive (pricesIncludeTax: true): the value you write is the GROSS price the customer pays. variant.price (read) is NET; variant.priceWithTax (read) is GROSS. To keep a customer price unchanged, write the old priceWithTax, never the old price (re-grossing the net silently changes the price). After a variant price write, set echoes the resulting price/priceWithTax for sanity-checking. See references/backend-quirks.md.name / slug auto-resolve de translation id (override with --lang)featuredAssetId accepts Asset ID or null to unlinkassetIds accepts JSON array of IDs; [] clears list--auto derives slug from entity's current name in target languagegqlexport — business shop snapshotSnapshots an entire business (collections + products + variants + prices + side-order groups + relationship mappings) to JSON. Pure admin API — no SQL, no kubectl required.
$VCTL <profile> export <slug-or-id> [-o output.json]
Examples:
$VCTL prod export pizzeria-delice-dinslaken # → pizzeria-delice-dinslaken_shop.json
$VCTL prod export 125 -o /tmp/delice.json # by ID, custom output path
$VCTL staging export pizza-enso-de -o enso_staging.json
{
"business": { "id", "name", "slug", "channelId", "type", "isOpen", ... },
"collections": [{ "id", "name", "slug", "parentId", "variantIds", "featuredAssetId", "assetIds", ... }],
"products": [{
"id", "name", "slug", "enabled", "featuredAssetId", "assetIds",
"variants": [{
"id", "name", "sku", "enabled", "price",
"featuredAssetId", "assetIds", "sideOrderGroupIds"
}]
}],
"sideOrderGroups": [{
"id", "name", "type", "minChoices", "maxChoices",
"sideOrders": [{ "id", "name", "price", "isEnabled", "maxChoices" }]
}],
"stats": { "collections", "products", "variants", "sideOrderGroups", "sideOrderItems" }
}
Names come from de translations. The snapshot is what audit, regen-slugs, and apply-diff consume.
sog — SideOrderGroup lifecycleCreates, deletes, edits items inside SideOrderGroup, plus looks up which variants reference a given SOG. Uses libs/vendure/template-plugin admin API.
$VCTL prod sog create --name X --type SINGLE|MULTIPLE --min N --max M \
--items '[{"name":"Spaghetti"},{"name":"Penne"}]'
$VCTL prod sog where-used <sog-id> [--json]
$VCTL prod sog delete <sog-id> [--reassign <id,id,…>] [--dry-run]
$VCTL prod sog items add <sog-id> --items '[{"name":"Soße Extra"}]'
$VCTL prod sog items remove <sog-id> --ids 504,505,506
$VCTL prod sog items set <sog-id> <item-id> [--price <cents>] [--name <str>] [--enabled true|false]
delete is atomic: every variant referencing the SOG is rewired to --reassign (or has the ref removed if omitted), and deleteSideOrderGroup runs in the same aliased mutation. All-or-nothing.items add/remove rewrite the whole sideOrders array via updateSideOrderGroup (template-plugin accepts only full-array writes).items set patches one side-order in place by id (price/name/enabled) and rewrites the array for you — no need to hand-assemble the full array for a single edit. Existing ids are preserved. Journaled as sog-items-set.variant.customFields.sideOrderGroupsIds (plural Groups before Ids). CLI uses that exact GraphQL path.dangling-reference audit rule flags variants whose sideOrderGroupIds (snapshot field) points at a SOG that no longer exists.assets — bulk asset cleanupDelete orphan Asset entities and/or unlink featuredAsset/assets[] references from every product in a collection.
$VCTL prod assets list --collection <id[,id…]>
$VCTL prod assets delete <id1,id2,…> [--force]
$VCTL prod assets unlink --collection <id[,id…]> [--dry-run]
$VCTL prod assets purge --collection <id[,id…]> [--dry-run]
delete wraps deleteAssets(input:{assetIds,force}); --force defaults to true (matches admin UI).unlink fans out to every product in given collection(s) and sets featuredAssetId=null, assetIds=[] on products and variants. Each update journaled as assets-unlink.purge = unlink + orphan-safe delete. After unlink, a single channel-wide fetch checks which previously-referenced assets are still referenced elsewhere; only ones that aren't are deleted.regen-slugs — bulk slug regenerationScans a business for product or collection slugs that disagree with slugify(name) and fixes them via set-batch. Vendure's dedup-suffix slugs (e.g. pizza-piccante-2) are left alone — overwriting risks collisions.
$VCTL prod regen-slugs product <business-slug> [--dry-run]
$VCTL prod regen-slugs collection <business-slug>
Slugify is a byte-equivalent port of libs/helper/src/slugify.ts.
set-pv — rename single-variant product + its variant$VCTL prod set-pv name <product-id> <value>
set instead)collection — membership helpersCollection membership is a manual product-id-filter whose productIds is a JSON-string array — creating a product does not add it to any collection. These helpers read the current filter, merge/diff the ids, and re-write the full filters array (preserving any other filters), so you don't risk clobbering the list by hand.
$VCTL prod collection add-product <collId> <prodId…>
$VCTL prod collection remove-product <collId> <prodId…>
product-id-filter filters on the collection are preserved verbatim; combineWithAnd is carried over.collection-membership (before/after id lists). See references/backend-quirks.md.product — journaled delete$VCTL prod product delete <id…> [--channel <token>]
Wraps deleteProducts. Unlike a raw gql delete, each deletion is journaled (product-delete) so it shows up in log.
log — mutation journalEvery successful write from set, set-pv, set-batch, apply-diff, regen-slugs, assets-* helpers, and sog-* lifecycle subcommands is appended as JSONL to ~/.cache/vctl/journal-<profile>.jsonl. Raw gql mutations are intentionally not journaled — no structured before/after to extract. If log is empty despite recent writes, those writes went through gql; switch to set/set-batch/apply-diff for auditability.
$VCTL prod log [--last N] [--since 7d] [--entity product] [--field name] [--json]
apply-diff — push changes between two snapshotsSupports the "export → edit JSON → push back" workflow. Diffs two shop snapshots and applies differences via set-batch.
$VCTL prod apply-diff <before.json> <after.json> [--dry-run]
Tracked fields: product.{name,slug,enabled,featuredAssetId,assetIds}, variant.{name,sku,enabled,price,featuredAssetId,assetIds}, collection.{name,slug,featuredAssetId,assetIds}, sog.name, plus side-order {name,price,isEnabled,maxChoices} inside their SOG. Side-order changes collapse to one updateSideOrderGroup(input:{id, sideOrders:[…]}) per touched SOG.
audit — data-quality rulesRuns the standard validation rule set against a business shop snapshot. Internally calls export to fetch the snapshot, then applies rules.
$VCTL prod audit <business-slug-or-id> [--category X,Y] [--json]
| Rule | Detects |
|---|---|
whitespace | leading/trailing spaces in product / variant / collection / side-order names |
name-mismatch | single-variant product name ≠ variant name |
all-disabled | product with variants but all disabled |
disabled-parent | disabled product still has enabled variants |
duplicates | duplicate names among products, SOGs, or side-orders within a SOG |
negative-price | side-order price < 0 |
negative-choices | SOG minChoices or maxChoices < 0 |
missing-image | product with no featuredAsset and no assets[] |
dangling-reference | variant's sideOrderGroupIds points at a non-existent SOG |
duplicate-sku | variant SKU collisions |
Exits non-zero when any finding surfaces. --category whitespace,duplicates runs a subset.
set-batch — bulk updates from JSON fileFor N similar updates (bulk renames, whitespace fixes, re-enables), set-batch collapses a sequential for op in ops: set … loop into two HTTP round-trips.
$VCTL prod set-batch -f fixes.json [--chunk-size 100] [--continue-on-error]
Input file is a JSON array of ops:
[
{ "entity": "variant", "id": "17304", "field": "name", "value": "Salat Dello Chef" },
{ "entity": "product", "id": "11812", "field": "name", "value": "Chicken Nuggets (6 Stück)" },
{ "entity": "variant", "id": "17566", "field": "enabled", "value": true },
{ "entity": "variant", "id": "17566", "field": "price", "value": 1150 },
{ "entity": "sog", "id": "412", "field": "name", "value": "Beilagen" },
{
"entity": "sog",
"id": "412",
"field": "sideOrders",
"value": [{ "id": 504, "name": "Käserand", "price": 200, "isEnabled": true }]
}
]
Supported (entity, field) pairs mirror set plus one batch-only op:
product / variant / collection: name, slug, enabled, sku, featuredAssetId, assetIds (as applicable), and variant.price.sog / name.sog / sideOrders — a full-array rewrite of a SideOrderGroup's items (the template-plugin only accepts the whole array). Send every item you want to keep; new items get client-assigned ids. This is the same path apply-diff uses to bundle side-order edits. For a single-item change without re-sending the array, use sog items set (below).⚠️ variant.price is GROSS (tax-inclusive channel) — write priceWithTax, not price. See references/backend-quirks.md.
Channel scoping: variant and sog writes are channel-scoped — set the channel first (vctl <profile> channel <token>) or pass --channel. set-batch warns if a channel-scoped op runs with no channel set.
get / list — read helpers$VCTL prod get <entity> <id> # single entity, JSON
$VCTL prod list <entity> [--filter k=v] [--limit N] [--json]
Entities: product, variant, collection, sog. For richer selection sets, use gql directly.
~/.cache/vctl/session-<profile>UNAUTHORIZED/FORBIDDEN/"not logged in" errors)~/.cache/vctl/types-<mtime>-vendure-types-admin.ts.json — invalidated automatically when yarn generate-types re-writes the source filekubectl exec. Required for hygiene writes the admin API doesn't expose (user.identifier, authentication_method), cross-row diagnostic SQL, and DBA work.yarn generate-types workflow that produces the file describe readsCreates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub qlaxde/agent-plugins --plugin vctl