Skip to content

Commit 438d2ee

Browse files
tizmagikclaude
andcommitted
Add shopify store create command for programmatic store creation
Enables creating Shopify stores directly from the CLI via Core's Signups GraphQL API. Supports both trial stores (`shopify store create`) and development stores (`shopify store create --dev`) using the StoreCreate and AppDevelopmentStoreCreate mutations respectively. The CLI authenticates with Identity via PKCE and sends the bearer token directly to the Signups API (no application token exchange needed). Flags: --name, --subdomain, --country (default: US), --dev, --json Prerequisites: - Core PR shop/world#586779 (authorizes CLI on Signups API) - Identity scope grant for `shop.create` - `app_development_store_create` approval scope for --dev flag Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 814a3b9 commit 438d2ee

19 files changed

Lines changed: 1050 additions & 1 deletion

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
shopify store create [flags]
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// This is an autogenerated file. Don't edit this file manually.
2+
export interface storecreate {
3+
/**
4+
* The country code for the store (e.g., US, CA, GB).
5+
* @environment SHOPIFY_FLAG_STORE_COUNTRY
6+
*/
7+
'-c, --country <value>'?: string
8+
9+
/**
10+
* Create a development store instead of a trial store.
11+
* @environment SHOPIFY_FLAG_STORE_DEV
12+
*/
13+
'--dev'?: ''
14+
15+
/**
16+
* Output the result as JSON. Automatically disables color output.
17+
* @environment SHOPIFY_FLAG_JSON
18+
*/
19+
'-j, --json'?: ''
20+
21+
/**
22+
* The name of the store.
23+
* @environment SHOPIFY_FLAG_STORE_NAME
24+
*/
25+
'-n, --name <value>'?: string
26+
27+
/**
28+
* Disable color output.
29+
* @environment SHOPIFY_FLAG_NO_COLOR
30+
*/
31+
'--no-color'?: ''
32+
33+
/**
34+
* The custom myshopify.com subdomain for the store.
35+
* @environment SHOPIFY_FLAG_STORE_SUBDOMAIN
36+
*/
37+
'--subdomain <value>'?: string
38+
39+
/**
40+
* Increase the verbosity of the output.
41+
* @environment SHOPIFY_FLAG_VERBOSE
42+
*/
43+
'--verbose'?: ''
44+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// This is an autogenerated file. Don't edit this file manually.
2+
import {ReferenceEntityTemplateSchema} from '@shopify/generate-docs'
3+
4+
const data: ReferenceEntityTemplateSchema = {
5+
name: 'store create',
6+
description: `Creates a new Shopify store associated with your account.
7+
8+
By default, creates a trial store. Use \`--dev\` to create a development store instead.`,
9+
overviewPreviewDescription: `Create a new Shopify store.`,
10+
type: 'command',
11+
isVisualComponent: false,
12+
defaultExample: {
13+
codeblock: {
14+
tabs: [
15+
{
16+
title: 'store create',
17+
code: './examples/store-create.example.sh',
18+
language: 'bash',
19+
},
20+
],
21+
title: 'store create',
22+
},
23+
},
24+
definitions: [
25+
{
26+
title: 'Flags',
27+
description: 'The following flags are available for the `store create` command:',
28+
type: 'storecreate',
29+
},
30+
],
31+
category: 'store',
32+
related: [
33+
],
34+
}
35+
36+
export default data

docs-shopify.dev/generated/generated_docs_data.json

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5855,6 +5855,107 @@
58555855
"category": "store",
58565856
"related": []
58575857
},
5858+
{
5859+
"name": "store create",
5860+
"description": "Creates a new Shopify store associated with your account.\n\nBy default, creates a trial store. Use `--dev` to create a development store instead.",
5861+
"overviewPreviewDescription": "Create a new Shopify store.",
5862+
"type": "command",
5863+
"isVisualComponent": false,
5864+
"defaultExample": {
5865+
"codeblock": {
5866+
"tabs": [
5867+
{
5868+
"title": "store create",
5869+
"code": "shopify store create [flags]",
5870+
"language": "bash"
5871+
}
5872+
],
5873+
"title": "store create"
5874+
}
5875+
},
5876+
"definitions": [
5877+
{
5878+
"title": "Flags",
5879+
"description": "The following flags are available for the `store create` command:",
5880+
"type": "storecreate",
5881+
"typeDefinitions": {
5882+
"storecreate": {
5883+
"filePath": "docs-shopify.dev/commands/interfaces/store-create.interface.ts",
5884+
"name": "storecreate",
5885+
"description": "",
5886+
"members": [
5887+
{
5888+
"filePath": "docs-shopify.dev/commands/interfaces/store-create.interface.ts",
5889+
"syntaxKind": "PropertySignature",
5890+
"name": "--dev",
5891+
"value": "\"\"",
5892+
"description": "Create a development store instead of a trial store.",
5893+
"isOptional": true,
5894+
"environmentValue": "SHOPIFY_FLAG_STORE_DEV"
5895+
},
5896+
{
5897+
"filePath": "docs-shopify.dev/commands/interfaces/store-create.interface.ts",
5898+
"syntaxKind": "PropertySignature",
5899+
"name": "--no-color",
5900+
"value": "\"\"",
5901+
"description": "Disable color output.",
5902+
"isOptional": true,
5903+
"environmentValue": "SHOPIFY_FLAG_NO_COLOR"
5904+
},
5905+
{
5906+
"filePath": "docs-shopify.dev/commands/interfaces/store-create.interface.ts",
5907+
"syntaxKind": "PropertySignature",
5908+
"name": "--subdomain <value>",
5909+
"value": "string",
5910+
"description": "The custom myshopify.com subdomain for the store.",
5911+
"isOptional": true,
5912+
"environmentValue": "SHOPIFY_FLAG_STORE_SUBDOMAIN"
5913+
},
5914+
{
5915+
"filePath": "docs-shopify.dev/commands/interfaces/store-create.interface.ts",
5916+
"syntaxKind": "PropertySignature",
5917+
"name": "--verbose",
5918+
"value": "\"\"",
5919+
"description": "Increase the verbosity of the output.",
5920+
"isOptional": true,
5921+
"environmentValue": "SHOPIFY_FLAG_VERBOSE"
5922+
},
5923+
{
5924+
"filePath": "docs-shopify.dev/commands/interfaces/store-create.interface.ts",
5925+
"syntaxKind": "PropertySignature",
5926+
"name": "-c, --country <value>",
5927+
"value": "string",
5928+
"description": "The country code for the store (e.g., US, CA, GB).",
5929+
"isOptional": true,
5930+
"environmentValue": "SHOPIFY_FLAG_STORE_COUNTRY"
5931+
},
5932+
{
5933+
"filePath": "docs-shopify.dev/commands/interfaces/store-create.interface.ts",
5934+
"syntaxKind": "PropertySignature",
5935+
"name": "-j, --json",
5936+
"value": "\"\"",
5937+
"description": "Output the result as JSON. Automatically disables color output.",
5938+
"isOptional": true,
5939+
"environmentValue": "SHOPIFY_FLAG_JSON"
5940+
},
5941+
{
5942+
"filePath": "docs-shopify.dev/commands/interfaces/store-create.interface.ts",
5943+
"syntaxKind": "PropertySignature",
5944+
"name": "-n, --name <value>",
5945+
"value": "string",
5946+
"description": "The name of the store.",
5947+
"isOptional": true,
5948+
"environmentValue": "SHOPIFY_FLAG_STORE_NAME"
5949+
}
5950+
],
5951+
"value": "export interface storecreate {\n /**\n * The country code for the store (e.g., US, CA, GB).\n * @environment SHOPIFY_FLAG_STORE_COUNTRY\n */\n '-c, --country <value>'?: string\n\n /**\n * Create a development store instead of a trial store.\n * @environment SHOPIFY_FLAG_STORE_DEV\n */\n '--dev'?: ''\n\n /**\n * Output the result as JSON. Automatically disables color output.\n * @environment SHOPIFY_FLAG_JSON\n */\n '-j, --json'?: ''\n\n /**\n * The name of the store.\n * @environment SHOPIFY_FLAG_STORE_NAME\n */\n '-n, --name <value>'?: string\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * The custom myshopify.com subdomain for the store.\n * @environment SHOPIFY_FLAG_STORE_SUBDOMAIN\n */\n '--subdomain <value>'?: string\n\n /**\n * Increase the verbosity of the output.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}"
5952+
}
5953+
}
5954+
}
5955+
],
5956+
"category": "store",
5957+
"related": []
5958+
},
58585959
{
58595960
"name": "store execute",
58605961
"description": "Executes an Admin API GraphQL query or mutation on the specified store using previously stored app authentication.\n\nRun `shopify store auth` first to create stored auth for the store.\n\nMutations are disabled by default. Re-run with `--allow-mutations` if you intend to modify store data.",

packages/cli-kit/src/private/node/session.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,16 @@ interface BusinessPlatformAPIOAuthOptions {
9292
scopes: BusinessPlatformScope[]
9393
}
9494

95+
/**
96+
* A scope supported by the Signups API.
97+
* The Signups API uses the Identity bearer token directly (no application token exchange).
98+
*/
99+
export type SignupsScope = 'shop-create'
100+
interface SignupsAPIOAuthOptions {
101+
/** List of scopes to request permissions for. */
102+
scopes: SignupsScope[]
103+
}
104+
95105
/**
96106
* It represents the authentication requirements and
97107
* is the input necessary to trigger the authentication
@@ -103,6 +113,7 @@ export interface OAuthApplications {
103113
partnersApi?: PartnersAPIOAuthOptions
104114
businessPlatformApi?: BusinessPlatformAPIOAuthOptions
105115
appManagementApi?: AppManagementAPIOauthOptions
116+
signupsApi?: SignupsAPIOAuthOptions
106117
}
107118

108119
export interface OAuthSession {
@@ -111,6 +122,7 @@ export interface OAuthSession {
111122
storefront?: string
112123
businessPlatform?: string
113124
appManagement?: string
125+
identity?: string
114126
userId: string
115127
}
116128

@@ -397,6 +409,10 @@ async function tokensFor(applications: OAuthApplications, session: Session): Pro
397409
tokens.appManagement = session.applications[appId]?.accessToken
398410
}
399411

412+
if (applications.signupsApi) {
413+
tokens.identity = session.identity.accessToken
414+
}
415+
400416
return tokens
401417
}
402418

@@ -413,7 +429,8 @@ function getFlattenScopes(apps: OAuthApplications): string[] {
413429
const storefront = apps.storefrontRendererApi?.scopes ?? []
414430
const businessPlatform = apps.businessPlatformApi?.scopes ?? []
415431
const appManagement = apps.appManagementApi?.scopes ?? []
416-
const requestedScopes = [...admin, ...partner, ...storefront, ...businessPlatform, ...appManagement]
432+
const signups = apps.signupsApi?.scopes ?? []
433+
const requestedScopes = [...admin, ...partner, ...storefront, ...businessPlatform, ...appManagement, ...signups]
417434
return allDefaultScopes(requestedScopes)
418435
}
419436

@@ -424,6 +441,9 @@ function getFlattenScopes(apps: OAuthApplications): string[] {
424441
* @returns An object containing the scopes for each application.
425442
*/
426443
function getExchangeScopes(apps: OAuthApplications): ExchangeScopes {
444+
// Note: signupsApi is intentionally excluded here. The Signups API uses the Identity bearer
445+
// token directly rather than an exchanged application token. Its scopes are included in
446+
// getFlattenScopes so they appear on the Identity token, but no exchange is needed.
427447
const adminScope = apps.adminApi?.scopes ?? []
428448
const partnerScope = apps.partnersApi?.scopes ?? []
429449
const storefrontScopes = apps.storefrontRendererApi?.scopes ?? []

packages/cli-kit/src/private/node/session/scopes.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ describe('allDefaultScopes', () => {
2626
])
2727
})
2828

29+
test('transforms shop-create scope to full URI', async () => {
30+
const got = allDefaultScopes(['shop-create'])
31+
32+
expect(got).toContain('https://api.shopify.com/auth/shop.create')
33+
})
34+
2935
test('includes App Management and Store Management', async () => {
3036
// When
3137
const got = allDefaultScopes([])

packages/cli-kit/src/private/node/session/scopes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ function scopeTransform(scope: string): string {
8181
return 'https://api.shopify.com/auth/organization.on-demand-user-access'
8282
case 'app-management':
8383
return 'https://api.shopify.com/auth/organization.apps.manage'
84+
case 'shop-create':
85+
return 'https://api.shopify.com/auth/shop.create'
8486
default:
8587
return scope
8688
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import {signupsRequest} from './signups.js'
2+
import {graphqlRequest} from './graphql.js'
3+
import {handleDeprecations} from './partners.js'
4+
import {signupsFqdn} from '../context/fqdn.js'
5+
import {beforeEach, describe, expect, test, vi} from 'vitest'
6+
7+
vi.mock('./graphql.js')
8+
vi.mock('../context/fqdn.js')
9+
10+
const signupsFqdnValue = 'shopify.com'
11+
const url = `https://${signupsFqdnValue}/services/signups/graphql`
12+
const mockedToken = 'identity-token'
13+
14+
beforeEach(() => {
15+
vi.mocked(signupsFqdn).mockResolvedValue(signupsFqdnValue)
16+
})
17+
18+
describe('signupsRequest', () => {
19+
test('calls graphqlRequest with correct parameters', async () => {
20+
vi.mocked(graphqlRequest).mockResolvedValue({storeCreate: {shopPermanentDomain: 'test.myshopify.com'}})
21+
const query = 'mutation StoreCreate($signup: ShopInput!) { storeCreate(signup: $signup) { shopPermanentDomain } }'
22+
const variables = {signup: {country: 'US'}}
23+
24+
await signupsRequest(query, mockedToken, variables)
25+
26+
expect(graphqlRequest).toHaveBeenCalledWith({
27+
query,
28+
api: 'Signups',
29+
url,
30+
token: mockedToken,
31+
variables,
32+
responseOptions: {onResponse: handleDeprecations},
33+
})
34+
})
35+
36+
test('calls graphqlRequest without variables when not provided', async () => {
37+
vi.mocked(graphqlRequest).mockResolvedValue({})
38+
const query = 'query { __schema { types { name } } }'
39+
40+
await signupsRequest(query, mockedToken)
41+
42+
expect(graphqlRequest).toHaveBeenCalledWith({
43+
query,
44+
api: 'Signups',
45+
url,
46+
token: mockedToken,
47+
variables: undefined,
48+
responseOptions: {onResponse: handleDeprecations},
49+
})
50+
})
51+
52+
test('returns the response from graphqlRequest', async () => {
53+
const expectedResponse = {storeCreate: {shopPermanentDomain: 'new-store.myshopify.com', polling: false}}
54+
vi.mocked(graphqlRequest).mockResolvedValue(expectedResponse)
55+
56+
const result = await signupsRequest('query', mockedToken)
57+
58+
expect(result).toEqual(expectedResponse)
59+
})
60+
})
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import {GraphQLVariables, graphqlRequest} from './graphql.js'
2+
import {handleDeprecations} from './partners.js'
3+
import {signupsFqdn} from '../context/fqdn.js'
4+
5+
async function setupRequest(token: string) {
6+
const api = 'Signups'
7+
const fqdn = await signupsFqdn()
8+
const url = `https://${fqdn}/services/signups/graphql`
9+
return {
10+
token,
11+
api,
12+
url,
13+
responseOptions: {onResponse: handleDeprecations},
14+
}
15+
}
16+
17+
/**
18+
* Executes a GraphQL query against the Signups API.
19+
* Uses the Identity bearer token directly (no application token exchange).
20+
*
21+
* @param query - GraphQL query to execute.
22+
* @param token - Identity access token.
23+
* @param variables - GraphQL variables to pass to the query.
24+
* @returns The response of the query of generic type <T>.
25+
*/
26+
export async function signupsRequest<T>(query: string, token: string, variables?: GraphQLVariables): Promise<T> {
27+
return graphqlRequest<T>({
28+
...(await setupRequest(token)),
29+
query,
30+
variables,
31+
})
32+
}

packages/cli-kit/src/public/node/context/fqdn.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,22 @@ export async function businessPlatformFqdn(): Promise<string> {
102102
}
103103
}
104104

105+
/**
106+
* It returns the Signups API service we should interact with.
107+
*
108+
* @returns Fully-qualified domain of the Signups service we should interact with.
109+
*/
110+
export async function signupsFqdn(): Promise<string> {
111+
const environment = serviceEnvironment()
112+
const productionFqdn = 'shopify.com'
113+
switch (environment) {
114+
case 'local':
115+
return new DevServerCore().host('shopify')
116+
default:
117+
return productionFqdn
118+
}
119+
}
120+
105121
/**
106122
* It returns the Identity service we should interact with.
107123
*

0 commit comments

Comments
 (0)