Skip to content

Add shopify store create preview command#7558

Draft
alfonso-noriega wants to merge 1 commit into
preview-store-session-discriminatorfrom
store-create-preview-command
Draft

Add shopify store create preview command#7558
alfonso-noriega wants to merge 1 commit into
preview-store-session-discriminatorfrom
store-create-preview-command

Conversation

@alfonso-noriega
Copy link
Copy Markdown
Contributor

@alfonso-noriega alfonso-noriega commented May 15, 2026

Stacked on top of #7557.

WHY are these changes introduced?

M1 of the Preview Store for AI Agent Surfaces initiative ships a CLI-first flow where an AI agent can ask the CLI to create a Shopify store, get back a storefront URL, and then keep iterating on the store via existing CLI commands — all without an account, an admin UI, or a browser hop.

Growth's prototype on shop/world#708792 adds the Core orchestrator (POST /services/preview-stores) that creates a placeholder identity, mints a real shpat_… Admin API token against the shopify-cli-connector-app, and returns a one-time-use magic link to admin. River put up a parallel CLI prototype (branch Shopify/cli@preview-store/prototype) that called this endpoint via a new top-level preview command group and required users to either pipe --json output into a file or pass --domain/--token flags into a custom preview execute clone of store execute.

This PR ports the prototype onto the production CLI's store namespace and wires it through the existing stored-session machinery so we can delete the parallel transport entirely:

  • shopify preview createshopify store create preview (matches the language used in the DevTools kickoff and slack threads).
  • shopify preview execute → simply use shopify store execute --store <permanent-domain> … because the new command writes a kind: 'preview' session into the same LocalStorage namespace that store execute reads from.
  • shopify preview claim is M2 work and is intentionally not included.

The schema work that lets kind: 'preview' round-trip safely is in #7557; this PR introduces the only producer of those sessions.

WHAT is this pull request doing?

New command — shopify store create preview (packages/store/src/cli/commands/store/create/preview.ts)

Flags:

Flag Env var Purpose
-n, --shop-name SHOPIFY_FLAG_PREVIEW_STORE_SHOP_NAME Subdomain prefix; auto-generated when omitted.
--email SHOPIFY_FLAG_PREVIEW_STORE_EMAIL Email for the placeholder identity; defaults to a Core-generated @previewstore.invalid address.
--country SHOPIFY_FLAG_PREVIEW_STORE_COUNTRY ISO country code for the new store. Defaults to "US".
--core-url SHOPIFY_FLAG_PREVIEW_STORE_CORE_URL Core orchestrator base URL. Defaults to the local rig (https://app.shop.dev).
--cli-username SHOPIFY_FLAG_PREVIEW_STORE_CLI_USERNAME Basic-auth username for the orchestrator.
--cli-secret SHOPIFY_FLAG_PREVIEW_STORE_CLI_SECRET Basic-auth secret for the orchestrator.
-j, --json SHOPIFY_FLAG_JSON Machine-readable output for AI agent harnesses.

Core HTTP client (services/store/create/preview/client.ts)

Uses shopifyFetch to POST /services/preview-stores with the snake_case body Core expects (shop_name, email, country). Translates the response into a camelCase type and rejects non-2xx responses, non-JSON bodies, and responses missing any of the five required identifiers. Defaults — https://app.shop.dev and the basic-auth pair preview-store-cli / preview-store-cli-dev — match the dev-only secret hardcoded in Services::PreviewStoresController and are explicitly flagged in code comments as "needs productionization before non-developer release".

Orchestrator (services/store/create/preview/index.ts)

After the client returns, the orchestrator persists the response as a stored store-auth session via setStoredStoreAppSession with:

  • kind: 'preview', preview: { placeholderAccountUuid, coreUrl, magicLinkUrl, magicLinkExpiresAt } — surfacing the metadata Add preview-store discriminator to stored store auth sessions #7557 introduced.
  • userId: \placeholder:`` — non-numeric and prefixed so analytics filters can isolate placeholder identities and there's no collision with PKCE-issued sessions (which use numeric Shopify user ids).
  • scopes: [] — Core does not surface the granted scope list; the array is a sentinel and is not consulted by store execute (which only validates against the live Admin API). Preview sessions never go through the recovery path that suggests --scopes.
  • magicLinkExpiresAt derived locally as acquiredAt + 30 minutes to match PreviewStores::Create::MAGIC_LINK_TTL.

recordStoreFqdnMetadata is called twice (unvalidated → validated) to mirror the analytics shape PKCE auth emits, and setLastSeenUserId is updated so the next store execute against the new store finds the right session.

Result presenter (services/store/create/preview/result.ts)

Text output renders the shop id, permanent domain, placeholder UUID, and magic link, plus a next-step suggestion to run shopify store execute --store <permanent-domain> --query '{ shop { name } }'. The admin token is intentionally not rendered in text mode to avoid accidental copy-paste leakage. JSON output (used by AI agents) does include it because the agent needs the full session shape.

Wiring

  • packages/store/src/index.ts registers 'store:create:preview'.
  • packages/cli/README.md and packages/cli/oclif.manifest.json regenerated via pnpm refresh-manifests.
  • The internal types StoredStoreSessionKind and StoredPreviewStoreSession from Add preview-store discriminator to stored store auth sessions #7557 stay module-internal; PreviewStoreCreateRequest and CreatePreviewStoreInput are also kept module-internal. (Knip's check is clean.)

Out of scope

  • shopify store create preview --reuse <fqdn> and the corresponding "re-mint expired token" code path that Add preview-store discriminator to stored store auth sessions #7557's preview-recovery message hints at. The token's lifetime is whatever Core assigns — the M1 prototype does not surface an expiry to the CLI, so we treat preview sessions as non-expiring until Growth wires that through.
  • shopify store list integration (raised in Daniel's thread). The session is stored where store list will read it from once that command lands; no extra work needed here.
  • Productionization of Core URL resolution and service-auth (currently dev defaults). Tracked separately.
  • Claim flow (M2).

How to test your changes?

Unit tests:

node node_modules/vitest/vitest.mjs run \
  packages/store/src/cli/services/store/create/preview/client.test.ts \
  packages/store/src/cli/services/store/create/preview/index.test.ts
pnpm --filter @shopify/store run lint
pnpm --filter @shopify/store run build
pnpm knip

End-to-end on a local Core rig (per River's demo and the M1 prototype setup in shop/world#708792):

  1. Bring up Core with the preview-store rig (dev rig preview-store-min against shop/world PR 708792 checked out locally).

  2. From this branch:

    pnpm shopify store create preview --shop-name my-preview --json | tee /tmp/preview.json
  3. The new store can immediately be queried — no shopify store auth needed:

    pnpm shopify store execute --store "$(jq -r .shopPermanentDomain /tmp/preview.json)" \
      --query '{ shop { name } }'
  4. Open magicLinkUrl from the JSON output in a fresh browser session to land in admin without an Identity login (one-time use, ~30 minutes).

The pre-existing 2 callback test failures in packages/store/src/cli/services/store/auth/callback.test.ts ("doesn't" vs "does not" string mismatch) are unchanged on main and unrelated to this PR.

Post-release steps

None.

Checklist

  • I've considered possible cross-platform impacts (Mac, Linux, Windows) — pure HTTP/IO logic, uses shopifyFetch and LocalStorage from cli-kit, no platform-specific surface.
  • I've considered possible documentation changes — packages/cli/README.md regenerated; user-facing docs on shopify.dev for store create preview to follow once the command moves out of M1 prototype defaults.
  • I've considered analytics changes to measure impact — recordStoreFqdnMetadata and setLastSeenUserId mirror the PKCE-auth shape; the placeholder:<uuid> userId prefix is intentional to make placeholder-issued sessions filterable in analytics.
  • The change is user-facing — I've identified the correct bump type (patch for bug fixes · minor for new features · major for breaking changes) and added a changeset with pnpm changeset addminor bump on @shopify/store and @shopify/cli (new top-level command).

Mints a Preview Store via Core's `/services/preview-stores` orchestrator and
persists the returned admin API token as a `kind: 'preview'` stored session
under the existing shopify-cli-store LocalStorage namespace. The new store can
be used immediately as a target for `shopify store execute --store
<permanent-domain>` with no PKCE flow and no browser interaction.

  - commands/store/create/preview.ts: oclif command (`shopify store create
    preview`) with --shop-name, --email, --country, --core-url, --cli-username,
    --cli-secret, --json flags.
  - services/store/create/preview/client.ts: Core HTTP client with snake_case
    contract, basic-auth, response narrowing.
  - services/store/create/preview/index.ts: orchestrator that calls the client,
    persists the kind: 'preview' session via setStoredStoreAppSession, derives a
    placeholder: <uuid> userId, computes the magic-link expiry locally, calls
    setLastSeenUserId.
  - services/store/create/preview/result.ts: text and JSON presenters; text
    output omits the admin token to avoid copy-paste leakage, JSON output
    surfaces it for agent harnesses.
  - Re-runs refresh-manifests so README and oclif.manifest.json reflect the
    new command surface.
  - Defaults: app.shop.dev, basic-auth preview-store-cli/preview-store-cli-dev
    matching the M1 prototype. These need productionization (real Core URL
    resolution, real service auth) before the command ships to a non-developer
    release channel \u2014 covered in a follow-up PR.
@alfonso-noriega alfonso-noriega requested review from a team as code owners May 15, 2026 13:11
Copy link
Copy Markdown
Contributor Author

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

@github-actions github-actions Bot added the Area: @shopify/cli @shopify/cli package issues label May 15, 2026
@alfonso-noriega alfonso-noriega marked this pull request as draft May 15, 2026 15:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area: @shopify/cli @shopify/cli package issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant