@artstorefronts/cli
v0.5.0
Published
ASF platform CLI — wraps the ASF GraphQL/REST API for scriptable site setup and admin automation.
Downloads
185
Readme
@asf/cli
A first-class TypeScript CLI that wraps the Art Storefronts GraphQL/REST API so site owners, support, and admin agents can drive a customer site from the shell.
The CLI is a thin shim over the live API — no business logic lives here. Every command names the GraphQL field or REST endpoint it wraps in its --help output so you can cross-reference the GraphQL runbook when you need to know exactly what's being sent.
Status
v0.2 — browser-consent OAuth + explicit --host. First-party Authorization Code + PKCE flow against the doorkeeper provider that lands on icono_pregame wss/graphql-api-unlocks. Access + refresh tokens stored in the OS keychain (macOS Keychain / Windows Credential Manager / Linux Secret Service). Environment variables (ASF_TOKEN, ASF_BASE_URL, ASF_ENV, ASF_EMAIL) and the --env flag are gone — every command takes --host instead (default artstorefronts.com).
v0.1 shipped the verb-per-mutation surface against the legacy email/password JWT path.
Install
git clone [email protected]:Art-Storefronts/artstorefronts-cli.git
cd artstorefronts-cli
nvm use # picks up .nvmrc → Node 20.x
npm install
node ./bin/asf.js --helpTo put asf on your $PATH:
npm link
asf --helpThe bin shim (bin/asf.js) runs sources via ts-node (transpileOnly: true) — no compile step is needed for development. Run npm run typecheck for tsc --noEmit, npm run lint for eslint, npm test for the vitest suite.
Auth
asf auth login # → artstorefronts.com (production)
asf auth login --host staging.artstorefronts.com # staging
asf auth login --host app.lvh.me:3000 # local dev
asf auth login --scope read_access # ask for read-only
# Non-interactive (CI / server-side automation) — no browser:
asf auth login --host staging.artstorefronts.com \
--client-id "$ASF_CLIENT_ID" --client-secret "$ASF_CLIENT_SECRET"asf auth login opens the system browser to https://{host}/oauth/authorize. You complete the consent screen with your existing Art Storefronts site_manager credentials; the CLI never sees your password. The resulting access + refresh tokens land in the OS keychain.
| Command | Effect |
|---|---|
| asf auth login | Authorization Code + PKCE; saves tokens to OS keychain. |
| asf auth login --client-id … --client-secret … | OAuth 2.0 client_credentials; no browser. Token authenticates as the credential's owner. |
| asf auth logout | Calls POST /oauth/revoke, then clears the local keychain entry. |
| asf whoami | GET /api/v1/users/me with the cached token. Add --json for machine output. |
Token refresh is automatic: on a 401 the client exchanges the cached refresh_token for a new access token and retries the request once. If the refresh fails, the entry is wiped so the next command guides you back to asf auth login. (client_credentials tokens carry no refresh token — re-run the login on expiry; it's a non-interactive one-liner.)
Non-interactive / CI (--client-id + --client-secret): mint a confidential credential once at https://{host}/account/api_credentials, then pass the pair to asf auth login (no browser, no password). The issued token authenticates as the credential's owner — a credential owned by an admin carries cross-site setup scope; one owned by a site owner is scoped to that owner's websites. Store the secret in your CI secret manager; the CLI caches the resulting token in the OS keychain (or a 0600 file fallback in headless containers).
Headless workstations (no browser, but a human user): pass --no-open-browser to asf auth login and the CLI prints the authorization URL for you to open from another machine.
Host selection
Every command takes --host <hostname> (default artstorefronts.com). No environment variables, no --env flag.
| --host | Resolves to |
|---|---|
| (omitted) | https://artstorefronts.com |
| staging.artstorefronts.com | https://staging.artstorefronts.com |
| app.lvh.me:3000 | http://app.lvh.me:3000 (HTTP for local dev hosts) |
| https://my.example.com | honored verbatim (overrides scheme inference) |
Local-dev shorthand applies HTTP automatically to any host whose authority includes lvh.me, localhost, or 127.0.0.1. Everything else gets HTTPS. Pass a full URL when you need to override that inference.
Rack::Attack note. The Art Storefronts stack 403s requests that arrive with curl's / undici's default
User-Agent. The CLI sends a benignMozilla/5.0 (compatible; asf-cli/...; ...)UA on every request so this never bites you. If you write a custom integration, do the same.
Verb surface
asf auth login | logout | whoami
asf websites list | update | update-subscribe-bar | update-announcement-bar
asf stores list | update-markups | update-default-sizes | update-medium-availabilities
asf pages list | update | delete
asf products create | bulk-create | update | add-photo | remove-photo | delete | bulk-delete
asf galleries create | add-products | remove-products | set-cover | reorder
asf contacts create | bulk-create | delete | bulk-delete
asf shipping update-datum | create-method | update-method | delete-method
asf assets uploadRun asf <verb> --help for examples and the exact GraphQL field each verb wraps.
Cascade-safe deletes
pages delete, products delete, and the bulk-delete variants cascade server-side via PageDeleter.purge / ProductDeleter. For pages delete against a store or blog page, the server refuses without --cascade-confirmation because the cascade also destroys categories, products, and ES indexes. Non-destructive alternative for a store page: asf pages update --id <page> --no-online hides it without destroying the catalog.
The GraphQL field is
deleteProductbut the input/payload types areDeleteCoreProductInput/DeleteCoreProductPayload— the "Core" prefix disambiguates from the olderdeleteSignatureProfileProduct. You won't normally see these names from the CLI; flagged here so the runbook reference makes sense if you ever inspect the wire.
Smoke test against the local dev stack
The canonical smoke target is filip's regression-testing website (id 3268, owner [email protected]) on the icono_pregame local dev stack. With OAuth landed, you just point at the local host and let the browser carry you through consent:
asf auth login --host app.lvh.me:3000 # browser → consent → tokens
asf whoami --host app.lvh.me:3000
asf products create --host app.lvh.me:3000 \
--website-id 3268 --store-id 7547 \
--name AGENT_SMOKE_AP --product-type ART_PRINT \
--image https://res.cloudinary.com/decosites/image/upload/v1426724962/image_quality_auditor_test_sml_el2jaa.jpgFor reference / curl-level work, the canonical smoke target story is still:
Bring the stack up via the workspace's icono-pregame-start-environment skill, then mint a JWT:
docker compose -f icono_pregame_environment/docker-compose.yml exec -T icono-pregame-agent bash -lc \
'docker exec -i icono_pregame-web-1 bundle exec rails runner "
website = Website.find(3268)
owner = website.owner
exp = ::Api::V1::Auth::TokenGeneration::JwtTokens.build_access_expires_at_from_now.to_i
payload = ::Api::V1::Auth::TokenGeneration::Jwt::Payload.new(
email: owner.email, website_id: website.id, user_id: owner.id, exp: exp
)
puts \"JWT=#{::Api::V1::Auth::TokenGeneration::JwtTokens.encode(payload: payload).success_or_raise!}\"
"' 2>&1 | grep '^JWT='Smoke flow:
export ASF_TOKEN="<minted JWT>"
export ASF_BASE_URL="http://app.lvh.me:3000"
asf whoami # → website_id: 3268
asf websites list --query filip-cloudinary-regression-testing # discover store/page ids
asf products create --website-id 3268 --store-id <APS> \
--name AGENT_SMOKE_AP --product-type ART_PRINT \
--image https://res.cloudinary.com/decosites/image/upload/v1426724962/image_quality_auditor_test_sml_el2jaa.jpg
asf products create --website-id 3268 --store-id <SS> \
--name AGENT_SMOKE_STD --product-type STANDARD --quantity 5 \
--image https://res.cloudinary.com/decosites/image/upload/v1426724962/image_quality_auditor_test_sml_el2jaa.jpg
asf galleries create --website-id 3268 --name "Smoke Gallery"
asf galleries add-products --page-id <gal> --product-ids <p1>,<p2>
asf products delete --id <p1>
asf pages delete --id <gal>Full smoke evidence for each release lives under artifacts/asf-cli/<release>/smoke/ in the artstore-agent workspace.
Architecture
artstorefronts-cli/
bin/asf.js # Node shim — loads ts-node + dispatches to src/cli.ts
src/
cli.ts # clipanion Cli + command registration
base.ts # BaseCommand: --env flag + buildClient()
utils/
env.ts # AsfEnv + base URL resolution
config.ts # ~/.config/asf/credentials.json read/write
api.ts # fetch wrapper: REST + GraphQL with JWT + Mozilla UA
commands/
auth/{login,whoami,logout}.ts
assets/upload.ts
websites/{list,update,update-subscribe-bar,update-announcement-bar}.ts
stores/{list,update-markups,update-default-sizes,update-medium-availabilities}.ts
pages/{list,update,delete}.ts
products/{create,bulk-create,update,add-photo,remove-photo,delete,bulk-delete}.ts
galleries/{create,add-products,remove-products,set-cover,reorder}.ts
contacts/{create,bulk-create,delete,bulk-delete}.ts
shipping/{update-datum,create-method,update-method,delete-method}.ts
.github/workflows/ci.ymlThe single source of truth for the API surface is the backend — this package imports nothing from the Rails app and talks only over HTTPS.
Sibling repo.
@asf/mcpwraps the same GraphQL surface as an MCP server for end-customer agents. The two repos intentionally duplicatesrc/graphql/-style call patterns today — a future@asf/apiextraction will collapse them.
Roadmap
- 0.1 (this release): verb-per-mutation surface, JWT auth, smoke against local dev stack.
- 0.2:
asf setup pull / apply / diffdeclarative YAML orchestrator (port from the original draft). - 0.3: shell completions (
asf install --bash | --zsh),npm publishto private registry. - Later: anything that requires a new GraphQL operation belongs in a follow-up
icono_pregamePR first — file it there, then add the CLI verb.
References
- GraphQL runbook (canonical curl reference)
- icono_pregame PR #4912 — the GraphQL surface this CLI wraps
- icono_pregame PR #4913 — the original CLI draft this repo was extracted from
- Jira: ART-8461 (initial CLI release), parent ART-8460
