npm.io
0.8.0 • Published 23h ago

@artstorefronts/cli

Licence
MIT
Version
0.8.0
Deps
0
Vulns
0
Weekly
0

@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 git@github.com:Art-Storefronts/artstorefronts-cli.git
cd artstorefronts-cli
nvm use            # picks up .nvmrc → Node 20.x
npm install
node ./bin/asf.js --help

To put asf on your $PATH:

npm link
asf --help

The 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 benign Mozilla/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       upload

Run 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 deleteProduct but the input/payload types are DeleteCoreProductInput / DeleteCoreProductPayload — the "Core" prefix disambiguates from the older deleteSignatureProfileProduct. 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 filip+siteowner1@artstorefronts.com) 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.jpg

For 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.yml

The 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/mcp wraps the same GraphQL surface as an MCP server for end-customer agents. The two repos intentionally duplicate src/graphql/-style call patterns today — a future @asf/api extraction 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 / diff declarative YAML orchestrator (port from the original draft).
  • 0.3: shell completions (asf install --bash | --zsh), npm publish to private registry.
  • Later: anything that requires a new GraphQL operation belongs in a follow-up icono_pregame PR first — file it there, then add the CLI verb.

References