This skill is the foundation for every domain skill (shoplazza-discounts,
shoplazza-products, …): it owns all cross-cutting mechanics so domain skills never repeat
them, and every domain skill opens by requiring this file be read first. The authoritative
source for commands/params is shoplazza <svc> <cmd> --help and shoplazza schema — this
file teaches the mechanics, it does not memorize flags.
Reply in the user's language. Skill prose is English, but that never sets your reply language — mirror the user (Chinese question → Chinese answer), including questions you ask back, write-confirmation restatements, and error explanations. Keep CLI commands, flags, and field names verbatim (untranslated).
Access tiers
Start every operation at the highest tier that fits and stop as soon as one expresses it:
| Tier | Form | When |
|---|---|---|
| 1. Shortcut | shoplazza <svc> +<verb> [named flags] |
First choice. Named flags, smart defaults, structured errors. |
| 2. Spec leaf | shoplazza <svc> <cmd> --params '{…}' [--data '{…}'] |
Endpoints with no shortcut; one command per endpoint. |
| 3. api rest | shoplazza api rest <METHOD> <PATH> |
Raw fallback, full coverage. Only when the first two can't express it. |
In shoplazza <svc> --help, entries prefixed with + are shortcuts; the rest are spec leaves.
Output envelope (.data wrapper)
The structured-output contract, uniform across commands:
- Success → stdout:
{"ok":true,"data":<body>} - Failure → stderr:
{"ok":false,"error":{"type":…,"message":…}}, non-zero exit.
Response bodies nest under .data, so every jq path starts at .data (e.g.
.data.products[].id). Missing the .data prefix is the most common extraction mistake.
Exception — local commands have no envelope. auth status / auth scopes /
profile list print their object at the top level (no {ok,data}, no --jq support);
read fields directly (e.g. logged_in).
Common flags
| Flag | Notes |
|---|---|
--dry-run |
Print the request that would be sent, without sending it. Always --dry-run first for destructive / batch / money-spending writes. Read commands and shortcuts support it too. |
-q, --jq <expr> |
Filter JSON output with a jq expression. Outputs raw scalars by default (no surrounding quotes, just a trailing newline) — do not add -r: cobra parses -r as a separate flag and rejects the command. It is a single-string flag. |
--fields <f1,f2,…> |
Response field projection on a few shortcuts only — verified: products +search (comma-separated: products +search --fields id,title). Not universal — most commands have no --fields; check <shortcut> --help before using it. Names are response keys; the server may still return extra base fields and silently ignores unknown names. |
--page-limit <n> |
Page size, 1–250, on list shortcuts. The API default is 10 — see "Reading a filtered list" below. |
--format json|pretty|table |
Output format, default json. Use json for scripts/jq, pretty/table for humans. Global flag. |
--profile <name> |
Profile to use for this invocation (see "Profiles"). Global flag. |
Field projection: a few search shortcuts accept --fields (verify with --help); everywhere else, project/filter with --jq.
Reading a filtered list
A read that returns {"ok":true,…} is not evidence that the filter or the page you asked
for is the one you got. Two failure modes are silent, and both produce a confident wrong
answer rather than an error:
1. Truncation — the page default is 10. List endpoints return 10 records unless
--page-limit says otherwise (max 250), with has_more / cursor marking the rest.
has_more may come back null rather than false, so treat "not true" as the end.
Whenever the answer depends on completeness — "how many", "which ones", "all of X", any
total you compute yourself — pass --page-limit 250 and follow the cursor. For a pure count,
prefer the domain's +count shortcut over counting rows.
2. Silently-dropped filters — the API ignores query params it does not recognise. It does not 400. A flag whose param name has drifted comes back 200 with the unfiltered set, which reads exactly like "this customer really has that many orders".
So before a filtered read becomes an answer or feeds a write, confirm the filter landed:
shoplazza <svc> +search --<filter> <value> --dry-run # 1. which param actually goes on the wire
shoplazza <svc> +count # 2. unfiltered total, for comparison
shoplazza <svc> +count --<filter> <value> # 3. filtered total — must be < the unfiltered one
A filtered result equal to the unfiltered total is the tell. Confirm it by spot-checking
that the returned records actually satisfy the filter (--jq over the field you filtered on).
If the filter is being dropped, schema <svc>.<cmd> --view request lists the params the
endpoint really accepts — reach for the spec leaf or api rest with the documented param.
Precision ladder — when the endpoint offers more than one way to filter on the same thing,
prefer the narrowest: a dedicated exact-match param (often an array, e.g. customer_emails,
skus, order_tags) beats a general keyword search, which beats hand-filtering with --jq
over a full page. Exact params say what they match; keyword search may match a field you did
not intend.
Which tier — read the user's verb. The phrasing tells you whether they mean a whole value or a fragment, and that decides exact vs keyword:
| User's signal | Meaning | Tier |
|---|---|---|
"is / means / equals / =", "number / ID / value is X", or a self-identifying whole string (a full email, a full SKU like ABC-001, a full phone) |
whole value | exact-match array param |
"contains / includes / like / similar to / starts with… / prefix / fragment / part", X*, contains / starts-with / like |
fragment | keyword search |
"is / equals" is the authoritative exact signal even when it sits right next to a field name — match the whole value, do not fuzzy-search it.
Report the basis, not just the number. When you answer from a list read, say which filter was applied and what the completeness basis is ("3 of 3, unfiltered total 18"). It makes a dropped filter visible to the user instead of invisible.
Locate before you act. A write targets a record by its id, but users name it by something else (an order number, a code, an email, a title). Resolve the target with a read first, then branch on the count:
- 0 matches → stop and say so; do not fall back to a broader write or invent an id.
- exactly 1 → proceed (still
--dry-run+ restate for a destructive / money op). - 2+ matches → list the candidates and ask which one. Never guess, and never act on "the first" silently.
A number is not an id (e.g. an order number vs the internal order_id) — feeding one where the other is expected hits the wrong record or errors. Resolve it through the read, don't assume.
Schema introspection
Before calling any spec leaf or api rest, check the schema to confirm params / body / response
shape — do not guess fields:
shoplazza schema # list all modules
shoplazza schema discounts # list a module's commands
shoplazza schema discounts.get # a command's params / body / response
shoplazza schema orders.create --view request # request side only (--view all|request|response)
api rest fallback
Only when the first two tiers can't express it. Write the resolved real URL (not a {…}
template):
shoplazza api rest GET /openapi/2026-01/products
shoplazza api rest GET /openapi/2026-01/products/gid_123 --params '{"page_size":10}'
shoplazza api rest POST /openapi/2026-01/discounts/automatic --data @body.json
Supports --params / --data (incl. - for stdin, @file for a file), --dry-run, -q/--jq.
Large integer ids → quote them in JSON. In --params / --data, pass a big numeric id as a
string ({"id":"669923000979557666"}), not a bare number — bare large ints get float-coerced
(→ 6.69…e+17) and the request is rejected. Applies anywhere an id exceeds ~15 digits
(e.g. shop redirects).
Installation
CLI preflight
If shoplazza is not found (command not found) or shoplazza --version fails, the CLI is
not installed. Installing changes the user's machine: say which method you'll use and wait for
a yes. Pick by what the machine already has:
| Method | Command | When |
|---|---|---|
| npm (recommended) | npm install -g shoplazza-cli |
Node.js ≥ 16 present (node -v) |
| Homebrew | brew install Shoplazza/tap/shoplazza-cli |
macOS / Linux with brew |
| Install script | curl -fsSL https://raw.githubusercontent.com/Shoplazza/shoplazza-cli/main/install.sh | bash |
macOS / Linux without Node; installs to /usr/local/bin (INSTALL_DIR=<dir> to change) |
| Release binary | download from https://github.com/Shoplazza/shoplazza-cli/releases |
any other platform; put it on PATH |
Then confirm with shoplazza --version and continue with the login flow below. If the install
fails — a sandbox blocks it, permission is denied, or it needs sudo — stop and hand the user
the exact command to run in their own terminal.
- One installer only. Two installs leave two
shoplazzabinaries onPATHrunning different versions;which -a shoplazzalists them. Keep one. - Update with the installer you used.
shoplazza updatealways reinstalls via npm — on a Homebrew or script install it adds that second copy. Usebrew upgrade shoplazza-clior re-run the script instead.
First-time setup, end to end
shoplazza --version # installed?
shoplazza auth login -s my-store.myshoplazza.com --domain products,orders # login + bind a store
shoplazza auth status # logged_in: true, the store under profiles
Run the login in the background and hand the user the authorization URL (Agent-driven login flow below). Ask which store and which modules the user needs; don't guess a store domain.
Authentication (auth)
Login-state preflight
Before the first store operation in a session, run shoplazza auth status (local, instant,
no envelope). Two independent things must both be true:
logged_in: true— account credentials exist. Iffalse→ run the login flow below.profilesnon-empty — a store is bound.logged_in: truewithprofiles: []still cannot reach any store API; ask which store and runauth login -s <store> --domain <modules>.
If you skip the preflight, an unauthenticated business command fails fast with a
validation error — "no profile configured" plus a hint naming the fix. Treat that the
same way: don't retry, guide the user through login first.
Login
Authorization has two layers: account-level (auth login obtains account credentials) and
store-level (exchange a store token for a specific store, stored in a profile).
# Account-level login (no store bound)
shoplazza auth login --domain products,orders # or --scope read_product,write_product
# Store-level login (select a store; an interactive login with -s must also pass --scope or --domain)
shoplazza auth login -s my-store.myshoplazza.com --domain discounts
--domain <module,…>: authorize by top-level command name; the CLI expands each into the OAuth scopes that module needs. Options:app, billing, checkout, customers, discounts, orders, products, shop, themes, webhook, all. Preferred — you don't memorize scope literals.--scope <literal,…>: fine-grained OAuth scopes (e.g.read_product,write_product), least-privilege.- Re-login REPLACES the grant (verified live: a 22-scope grant re-authorized with one
scope came back with one). Topping up permissions? Pass
--merge-scopesto carry the prior grant along (older CLIs lack the flag — build the union yourself viaauth scopes;--domainand--scopecombine in one login). Only omit it when the user explicitly wants to narrow permissions.
Agent-driven login flow
When acting as an AI agent to log the user in: run auth login in the background (it
blocks and polls until the user completes authorization or it times out — see
--poll-interval / --timeout, defaults 2s / 300s), extract the authorization URL from its
output and hand it to the user; the command returns on its own after the user authorizes.
Missing flags become on-screen questions when a terminal is present. auth login (and
app init) asks for what you left out only when stdin and stderr are both real terminals and
neither SHOPLAZZA_CLI_NO_INTERACTIVE nor CI is set (present and non-empty = set); otherwise a
missing flag is an error pointing at that flag. A harness that hands the child process a pty
passes that check and the command waits on a keypress that never comes — export
SHOPLAZZA_CLI_NO_INTERACTIVE=1 once for the session if yours does.
It only switches prompting off. There is no --interactive flag and no env var to force it
on — deliberately, since forcing it would hang for good wherever no terminal exists.
Re-login for a missing scope? Pass --merge-scopes. Scopes are replaced, not merged
(see Login above); requesting only the missing scope drops everything else mid-task. If the
flag is unknown (older CLI), fall back to building the union: auth scopes → granted, then
request granted + the new scopes in one login.
Non-interactive fast path (UAT)
If you already ran auth login on another machine and have an account UAT, skip the browser:
shoplazza auth login --uat <UAT> # or set env SHOPLAZZA_UAT=<UAT>
--uat is promptless whatever the terminal looks like — a second, whole-command off-switch on
top of the gate above. Browser logins driven from a pty-allocating harness still need
SHOPLAZZA_CLI_NO_INTERACTIVE=1.
Store switching and status
shoplazza auth status # auth state / identity / current store
shoplazza auth scopes # supported scopes + scopes granted to the account
shoplazza auth store use -s another-store.myshoplazza.com [--scope …] # exchange a token for that store, set as current profile
shoplazza auth logout # log out of the current store
Which command switches the store:
| Want | Command |
|---|---|
| Which store am I on? | auth status — the profile with current: true (store_domain) |
| A store used before (has a profile) | profile list → profile use --name <name> (--previous switches back) |
| A store with no profile yet | auth store use -s <store-domain> — creates or reuses its profile and makes it current |
| One command against another store | add --profile <name> to that command; the current profile stays |
auth store use and profile add need an account login first, and a profile's scopes must be
a subset of the account grant — missing scopes → auth login --domain <module> --merge-scopes,
then switch again.
Handling insufficient permissions
On 401/403 for a write, the error envelope's hint says what's missing. Grant it as directed:
shoplazza auth login --domain <that module> (or --scope <missing scope>), then retry. Run
auth scopes first to see what's already granted.
Profiles (multi-tenant)
A profile = one store execution context (its store token + scopes). Manage multiple stores by switching profiles:
shoplazza profile add --name prod-us -s my-store.myshoplazza.com [--scope …] [--use]
shoplazza profile list
shoplazza profile use --name prod-us # or --previous to switch back to the last one
shoplazza profile info [--name prod-us] # defaults to the current profile
shoplazza profile rename --from old --to new
shoplazza profile remove --name prod-us
shoplazza profile update --name prod-us --scope … # adjust a profile's scopes
Per-invocation override: the --profile <name> global flag beats the SHOPLAZZA_CLI_PROFILE
env var and the current profile.
Update notice
When the CLI detects a new version after running, it prints a one-line notice to stderr
(⚡ shoplazza-cli X is available …). It never appears in the JSON on stdout. Do not
silently ignore it: after finishing the user's current request, tell them the current /
latest version and offer to update:
shoplazza update # update via npm (--check only reports versions, no install)
# or: npm install -g shoplazza-cli@latest
Installed with Homebrew or the install script? Update the same way instead (see Installation).
(Set SHOPLAZZA_CLI_NO_UPDATE_CHECK to disable the check.)
update does three things and reports each in its JSON body:
| Field | Means |
|---|---|
updated |
the binary was reinstalled. false = already current, not a failure |
meta_updated / meta_revision |
the API metadata cache was rewritten, and at which revision |
skill_installed |
the Agent Skills are installed — nothing more |
skill_installed answers "are they there", not "were they just refreshed": shoplazza update
does refresh them when it is true, but update --check reports the same true while
refreshing nothing, and a refresh that failed also still reports true (with a warning on
stderr). Never tell the user their skills were updated on the strength of this field alone —
read the stderr lines (✓ Refreshed N skill(s)) for that.
The skills are opt-in: false means the user never installed them and update left them
alone rather than installing them. To install:
npx skills add Shoplazza/shoplazza-cli -g
(Set SHOPLAZZA_CLI_SKIP_SKILLS=1 to suppress the skill step; the field then disappears from the
body entirely — an absent field means "suppressed", false means "not installed".)
Safety protocol
shoplazza-cli has no lark-style exit-10 high-risk-write gate and no --yes confirmation
flag. Do not invent a confirmation protocol. The safety model is --dry-run → restate →
wait for the user's go-ahead:
--dry-runfirst for destructive (cancel / delete), batch, or fund-moving (billing charges) operations: print the request that would be sent, restate it, then STOP and end your turn. For entity/campaign creation via a+shortcut(e.g. creating a discount), follow the domain skill —+shortcutsare structured and safe to run directly once the required, money-affecting values are confirmed (never fabricated);--dry-runonly if the user asks.- Consent must be a user message, not your own judgment. Showing the dry-run is not consent; your own review of it is not consent. The real command may only run in a turn that comes after the user explicitly agrees (e.g. "confirm" / "yes / go ahead"). Running dry-run and the real write in the same turn is a protocol violation even if the dry-run output was perfect.
- Restate intent alongside the dry-run: plain language — which store, what action, which key params — so the user knows exactly what they are agreeing to.
- Do not fabricate money-spending values (discount amounts / caps / prices) — omit for the default or ask; see each domain skill's required-vs-ask.
- Rehearse writes against dev / stg stores only; never practice on prod.
Command discovery
shoplazza --help # top-level modules
shoplazza <svc> --help # a module's shortcuts + spec leaves
shoplazza <svc> <cmd> --help # a command's flags
shoplazza schema <svc>.<cmd> # params / body / response shape