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
@github-actions
Copy link
Copy Markdown
Contributor

Differences in type declarations

We detected differences in the type declarations generated by Typescript for this branch compared to the baseline ('main' branch). Please, review them to ensure they are backward-compatible. Here are some important things to keep in mind:

  • Some seemingly private modules might be re-exported through public modules.
  • If the branch is behind main you might see odd diffs, rebase main into this branch.

New type declarations

We found no new type declarations in this PR

Existing type declarations

packages/cli-kit/dist/public/node/hooks/postrun.d.ts
@@ -1,3 +1,7 @@
+/**
+ * Postrun hook — uses dynamic imports to avoid loading heavy modules (base-command, analytics)
+ * at module evaluation time. These are only needed after the command has already finished.
+ */
 import { Hook } from '@oclif/core';
 /**
  * Check if post run hook has completed.
@@ -5,21 +9,6 @@ import { Hook } from '@oclif/core';
  * @returns Whether post run hook has completed.
  */
 export declare function postRunHookHasCompleted(): boolean;
-/**
- * Wait for the postrun hook to finish (so auto-upgrade has a chance to run) and then
- * tree-kill the current process tree before exiting.
- *
- * Long-running interactive commands like  need this when the user terminates
- * the command via  or Ctrl+C. The dev sub-processes such as servers and watchers keep
- * the event loop alive, so even after oclif's postrun hook completes the node process
- * won't exit on its own and we have to  the process tree. We must not do that
- * before the postrun hook has actually finished running auto-upgrade, otherwise we would
- * kill the upgrade mid-way while  is still running.
- *
- * The flag  is flipped at the very end of the hook after
- *  resolves, so polling it here is safe.
- */
-export declare function waitForPostRunHookAndExit(): void;
 export declare const hook: Hook.Postrun;
 /**
  * Auto-upgrades the CLI after a command completes, if a newer version is available.
packages/cli-kit/dist/private/node/ui/components/AutocompletePrompt.d.ts
@@ -20,13 +20,6 @@ export interface AutocompletePromptProps<T> {
     abortSignal?: AbortSignal;
     infoMessage?: InfoMessageProps['message'];
     groupOrder?: string[];
-    /**
-     * Throttle window in milliseconds applied to the search callback. Defaults to 400ms,
-     * which is appropriate for remote/paginated backends. In-memory consumers (where the
-     * search callback resolves synchronously) can pass 0 for instant filtering on every
-     * keystroke.
-     */
-    searchDebounceMs?: number;
 }
-declare function AutocompletePrompt<T>({ message, choices, infoTable, onSubmit, search, hasMorePages: initialHasMorePages, abortSignal, infoMessage, groupOrder, searchDebounceMs, }: React.PropsWithChildren<AutocompletePromptProps<T>>): ReactElement | null;
+declare function AutocompletePrompt<T>({ message, choices, infoTable, onSubmit, search, hasMorePages: initialHasMorePages, abortSignal, infoMessage, groupOrder, }: React.PropsWithChildren<AutocompletePromptProps<T>>): ReactElement | null;
 export { AutocompletePrompt };
\ No newline at end of file

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