@artstorefronts/cli
v0.2.0
Published
ASF platform CLI — wraps the ASF GraphQL/REST API for scriptable site setup and admin automation.
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-onlyasf 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 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.
Headless workstations (no browser): pass --no-open-browser to asf auth login and the CLI prints the authorization URL for you to open from another machine. Long-lived headless CI without an interactive workstation will land on the device-authorization-grant flow in v0.3.
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
