From ab8ebaff19e013817d3db8ef924ee61d691ddaf9 Mon Sep 17 00:00:00 2001 From: Ariel Caplan Date: Sun, 28 Jun 2026 19:34:23 +0300 Subject: [PATCH 1/6] Add --country flag to store create dev (validation/scaffolding only) Extracts and shares the country-code validation already used by store create preview: countryFlag, isCountryCode, and a shared invalidCountryCodeMessage in flags.ts, plus devStoreFlags.country. The flag is validated, plumbed into createDevStore options, and shown in JSON/success output, but is NOT yet sent to the BP createAppDevelopmentStore mutation -- the published schema does not expose a country argument until shop/world#671185 (part of shop/world#22968) merges. GraphQL wiring is a trivial follow-up. Command is hidden; no changeset (not functional end-to-end yet). Assisted-By: devx/bf777827-dc69-4993-9b11-c401dc19c4be --- ...06-25-001-country-flag-store-create-dev.md | 42 +++++++++++++++ ...02-country-flag-draft-pr-without-schema.md | 52 +++++++++++++++++++ packages/cli/oclif.manifest.json | 9 ++++ .../src/cli/commands/store/create/dev.test.ts | 41 +++++++++++++++ .../src/cli/commands/store/create/dev.ts | 8 ++- .../src/cli/commands/store/create/preview.ts | 4 +- packages/store/src/cli/flags.ts | 24 ++++++++- .../src/cli/services/store/create/dev.test.ts | 28 ++++++++++ .../src/cli/services/store/create/dev.ts | 7 +++ 9 files changed, 211 insertions(+), 4 deletions(-) create mode 100644 decisions/2026-06-25-001-country-flag-store-create-dev.md create mode 100644 decisions/2026-06-28-002-country-flag-draft-pr-without-schema.md diff --git a/decisions/2026-06-25-001-country-flag-store-create-dev.md b/decisions/2026-06-25-001-country-flag-store-create-dev.md new file mode 100644 index 00000000000..ad905293773 --- /dev/null +++ b/decisions/2026-06-25-001-country-flag-store-create-dev.md @@ -0,0 +1,42 @@ +# 2026-06-25-001 — `--country` flag for `store create dev` (country piece of shop/world#22968) + +## Context +`shopify store create dev` needs a `--country` flag, mirroring `store create preview`, +which already accepts and validates a country code. The Business Platform +`createAppDevelopmentStore` mutation does not yet expose a `country` argument — that +backend change is an open (unmerged) PR in `shop/world`. The CLI fetches the published +BP schema from `shop/world` `main` via `bin/get-graphql-schemas`, and CI enforces a +graphql-codegen freshness check. + +## Decision +1. Extract and share the country validation logic in `packages/store/src/cli/flags.ts`: + - `countryFlag(env)` factory (already existed, now exported) builds a normalized + (trim + uppercase) `--country` flag. + - `isCountryCode(value)` (already existed) validates the two-letter shape. + - New `invalidCountryCodeMessage` constant so every store-creation command emits the + same error. + - New `devStoreFlags.country` (env `SHOPIFY_FLAG_STORE_COUNTRY`), alongside + `previewStoreFlags.country`. +2. Add `--country` to the `store create dev` command with shared validation, threaded + into `createDevStore` options and surfaced in JSON/success output. +3. Do **not** wire `country` into the `createAppDevelopmentStore` mutation variables or + the `.graphql` operation yet — left a code comment marking the single insertion point. +4. No changeset: the command is `hidden` and the flag is not yet functional end-to-end. + +## Rationale +- The shareable validation/flag logic is mergeable now and unblocks the CLI side ahead + of the backend. +- Adding `country` to the `.graphql` operation now would break the CI graphql-codegen + freshness check, because the published schema (`shop/world` main) lacks the argument + until the backend PR merges. Passing it in mutation `variables` would also be a + TypeScript error against the generated variables type. +- Referencing the flag via `devStoreFlags.country` (member expression) instead of a + direct `countryFlag(...)` call inside the command's `flags` block avoids a crash in the + `@shopify/cli/command-flags-with-env` lint rule, matching the existing + `previewStoreFlags.country` / `storeFlags['organization-id']` pattern. + +## Alternatives rejected +- Hand-editing the generated `create_app_development_store.ts` to add `country`: would be + reverted by `pnpm graphql-codegen` and fail the CI freshness check. +- Adding `country` to the `.graphql` operation now: breaks codegen against the live schema. +- Inlining the validation/error string per-command: duplicates logic the task asked to share. diff --git a/decisions/2026-06-28-002-country-flag-draft-pr-without-schema.md b/decisions/2026-06-28-002-country-flag-draft-pr-without-schema.md new file mode 100644 index 00000000000..ad99552d647 --- /dev/null +++ b/decisions/2026-06-28-002-country-flag-draft-pr-without-schema.md @@ -0,0 +1,52 @@ +# 2026-06-28-002 — Ship `--country` flag as a draft PR without the BP schema wiring + +## Context +The `--country` flag for `store create dev` (see 2026-06-25-001) was tophatted locally +against a local Business Platform (`~/world/trees/pool-2`) whose `createAppDevelopmentStore` +mutation was patched to accept an optional `country: String` argument (Ariel's PR +`shop/world#671185`). The tophat involved three `TOPHAT ONLY` manual overrides that wired +`country` onto the GraphQL operation and into the mutation variables: +- `packages/store/src/cli/api/graphql/business-platform-organizations/mutations/create_app_development_store.graphql` +- `packages/store/src/cli/api/graphql/business-platform-organizations/generated/create_app_development_store.ts` +- `packages/store/src/cli/services/store/create/dev.ts` + +The tophat was verified working end-to-end locally after restarting the BP web process +(`overmind restart web` in `core/shopify`) to reload the memoized GraphQL-Ruby schema, and +attaching an `ApiClient` to the `shopify-cli` ServiceApp in the local DB. + +`shop/world#671185` is NOT yet merged to `main`. The CLI's `pnpm graphql-codegen` fetches +the published schema from `shop/world` `main`, and CI enforces a graphql-codegen freshness +check. + +## Decision +Open the CLI change as a **draft PR without the GraphQL schema wiring**: +1. Reverted all three `TOPHAT ONLY` overrides so the `.graphql` operation and generated + `.ts` are byte-identical to `HEAD` (passes the codegen freshness check). +2. Kept the full flag scaffolding: `--country` flag, shared `countryFlag` / + `isCountryCode` / `invalidCountryCodeMessage`, `devStoreFlags.country`, plumbing into + `createDevStore` options, and `country` in JSON/success output. +3. The service no longer sends `country` to BP; replaced the tophat comment with a NOTE + marking the single insertion point for when the backend lands. +4. No changeset: the command is `hidden` and `country` is not functional end-to-end. + +## Rationale +- A single CI-green PR cannot send `country` on the wire until the published schema has + the argument; keeping the tophat edits would leave CI red until the BP PR merges. +- Shipping the flag scaffolding now (green) gets the shareable validation/flag logic + reviewed and landed, decoupled from the backend timeline. +- The wiring is a trivial follow-up once `shop/world#671185` merges to `main`. + +## Follow-up (once shop/world#671185 merges to main) +1. `pnpm graphql-codegen:get-graphql-schemas && pnpm graphql-codegen` +2. Add `$country: String` to `create_app_development_store.graphql` and the + `createAppDevelopmentStore(country: $country)` argument. +3. Replace the NOTE in `services/store/create/dev.ts` with `country: options.country`. +4. Add a changeset and unhide the command when the broader feature is ready. + +## Alternatives rejected +- **Single stacked/draft PR keeping the `TOPHAT ONLY` edits** (Option A): accurately + communicates the dependency but leaves CI red on the codegen freshness check until the + BP PR merges. Rejected in favor of a green draft per Ariel's preference. +- **Keeping `country: options.country` in the service while reverting the generated + variables type**: TypeScript error — the object literal would reference a property the + reverted `CreateAppDevelopmentStoreMutationVariables` type no longer declares. diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index d6882fba5b4..1164463f8b5 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -6316,6 +6316,15 @@ "descriptionWithMarkdown": "Creates a new app development store in your organization.", "enableJsonFlag": false, "flags": { + "country": { + "description": "Two-letter country code for the store, such as US, CA, or GB.", + "env": "SHOPIFY_FLAG_STORE_COUNTRY", + "hasDynamicHelp": false, + "multiple": false, + "name": "country", + "required": false, + "type": "option" + }, "feature-preview": { "description": "The handle of a feature preview to enable on the new development store.", "env": "SHOPIFY_FLAG_STORE_FEATURE_PREVIEW", diff --git a/packages/store/src/cli/commands/store/create/dev.test.ts b/packages/store/src/cli/commands/store/create/dev.test.ts index 28916e4c998..5e77f815bcc 100644 --- a/packages/store/src/cli/commands/store/create/dev.test.ts +++ b/packages/store/src/cli/commands/store/create/dev.test.ts @@ -41,6 +41,7 @@ describe('store create dev command', () => { plan: 'plus', featurePreview: undefined, withDemoData: false, + country: undefined, json: false, }) }) @@ -54,6 +55,7 @@ describe('store create dev command', () => { plan: 'plus', featurePreview: undefined, withDemoData: false, + country: undefined, json: true, }) }) @@ -77,10 +79,48 @@ describe('store create dev command', () => { plan: 'basic', featurePreview: 'extended_variants', withDemoData: true, + country: undefined, json: false, }) }) + test('normalizes and passes the --country flag through to the service', async () => { + await StoreCreateDev.run([ + '--name', + 'my-test-store', + '--plan', + 'plus', + '--organization-id', + '12345', + '--country', + 'ca', + ]) + + expect(createDevStore).toHaveBeenCalledWith(expect.objectContaining({country: 'CA'})) + }) + + test('rejects an invalid --country value without calling the service', async () => { + const mockExit = vi.spyOn(process, 'exit').mockImplementation((() => { + throw new Error('process.exit') + }) as never) + + await expect( + StoreCreateDev.run([ + '--name', + 'my-test-store', + '--plan', + 'plus', + '--organization-id', + '12345', + '--country', + 'USA', + ]), + ).rejects.toThrow() + expect(createDevStore).not.toHaveBeenCalled() + + mockExit.mockRestore() + }) + test('prompts for the name when --name is omitted in an interactive environment', async () => { vi.mocked(storeNamePrompt).mockResolvedValue('prompted-store') @@ -141,6 +181,7 @@ describe('store create dev command', () => { expect(StoreCreateDev.flags.plan).toBeDefined() expect(StoreCreateDev.flags['feature-preview']).toBeDefined() expect(StoreCreateDev.flags['with-demo-data']).toBeDefined() + expect(StoreCreateDev.flags.country).toBeDefined() expect(StoreCreateDev.flags.json).toBeDefined() }) diff --git a/packages/store/src/cli/commands/store/create/dev.ts b/packages/store/src/cli/commands/store/create/dev.ts index 3ba6dfbda3e..a2db8d104ba 100644 --- a/packages/store/src/cli/commands/store/create/dev.ts +++ b/packages/store/src/cli/commands/store/create/dev.ts @@ -1,7 +1,7 @@ import {createDevStore} from '../../../services/store/create/dev.js' import {devStorePlanHandles, DevStorePlan} from '../../../services/store/constants.js' import {storeNamePrompt, storePlanPrompt} from '../../../prompts/store.js' -import {storeFlags} from '../../../flags.js' +import {devStoreFlags, invalidCountryCodeMessage, isCountryCode, storeFlags} from '../../../flags.js' import {selectOrg} from '@shopify/organizations' import Command from '@shopify/cli-kit/node/base-command' import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli' @@ -40,12 +40,17 @@ export default class StoreCreateDev extends Command { default: false, env: 'SHOPIFY_FLAG_STORE_WITH_DEMO_DATA', }), + country: devStoreFlags.country, } async run(): Promise { const {flags} = await this.parse(StoreCreateDev) this.failMissingNonTTYFlags(flags, ['name', 'organization-id', 'plan']) + if (flags.country !== undefined && !isCountryCode(flags.country)) { + this.error(invalidCountryCodeMessage) + } + const organization = await selectOrg(flags['organization-id']?.toString()) const name = flags.name ?? (await storeNamePrompt()) const plan = (flags.plan as DevStorePlan | undefined) ?? (await storePlanPrompt()) @@ -57,6 +62,7 @@ export default class StoreCreateDev extends Command { plan, featurePreview: flags['feature-preview'], withDemoData: flags['with-demo-data'], + country: flags.country, json: flags.json, }) } catch (error) { diff --git a/packages/store/src/cli/commands/store/create/preview.ts b/packages/store/src/cli/commands/store/create/preview.ts index f49c1ae4e2f..125efdb122a 100644 --- a/packages/store/src/cli/commands/store/create/preview.ts +++ b/packages/store/src/cli/commands/store/create/preview.ts @@ -1,4 +1,4 @@ -import {isCountryCode, previewStoreFlags} from '../../../flags.js' +import {invalidCountryCodeMessage, isCountryCode, previewStoreFlags} from '../../../flags.js' import {type CreatePreviewStoreResult, createPreviewStoreCommand} from '../../../services/store/create/preview/index.js' import {writeCreatePreviewStoreResult} from '../../../services/store/create/preview/result.js' import StoreCommand from '../../../utilities/store-command.js' @@ -37,7 +37,7 @@ export default class StoreCreatePreview extends StoreCommand { const {flags} = await this.parse(StoreCreatePreview) if (flags.country !== undefined && !isCountryCode(flags.country)) { - this.error('Country must be a two-letter country code, for example: US.') + this.error(invalidCountryCodeMessage) } const result = await renderSingleTask({ diff --git a/packages/store/src/cli/flags.ts b/packages/store/src/cli/flags.ts index 49ce9304c0c..a3ba1550495 100644 --- a/packages/store/src/cli/flags.ts +++ b/packages/store/src/cli/flags.ts @@ -3,7 +3,14 @@ import {normalizeBulkOperationId} from '@shopify/cli-kit/node/api/bulk-operation import {resolvePath} from '@shopify/cli-kit/node/path' import {Flags} from '@oclif/core' -function countryFlag(env: string) { +/** + * Builds a reusable `--country` flag. The value is normalized to an uppercase, + * trimmed string so downstream validation and the backend receive a consistent + * two-letter country code. + * + * @param env - The environment variable that can supply the flag's value. + */ +export function countryFlag(env: string) { return Flags.string({ description: 'Two-letter country code for the store, such as US, CA, or GB.', env, @@ -12,14 +19,29 @@ function countryFlag(env: string) { }) } +/** + * Returns true when the value is a two-letter (ISO 3166-1 alpha-2 shaped) + * country code. Assumes the value has already been normalized to uppercase by + * `countryFlag`'s parser. + */ export function isCountryCode(value: string): boolean { return /^[A-Z]{2}$/.test(value) } +/** + * Error message shown when a `--country` flag value is not a two-letter code. + * Shared so every store-creation command reports the same guidance. + */ +export const invalidCountryCodeMessage = 'Country must be a two-letter country code, for example: US.' + export const previewStoreFlags = { country: countryFlag('SHOPIFY_FLAG_PREVIEW_STORE_COUNTRY'), } +export const devStoreFlags = { + country: countryFlag('SHOPIFY_FLAG_STORE_COUNTRY'), +} + export const storeFlags = { store: Flags.string({ char: 's', diff --git a/packages/store/src/cli/services/store/create/dev.test.ts b/packages/store/src/cli/services/store/create/dev.test.ts index c94971468dd..cf755cc7465 100644 --- a/packages/store/src/cli/services/store/create/dev.test.ts +++ b/packages/store/src/cli/services/store/create/dev.test.ts @@ -161,6 +161,34 @@ describe('createDevStore', () => { expect(outputResult).toHaveBeenCalledWith(expect.stringContaining('"demoData": true')) }) + test('includes the country in JSON output when provided', async () => { + vi.mocked(businessPlatformOrganizationsRequestDoc) + .mockResolvedValueOnce(defaultMutationResult) + .mockResolvedValueOnce({ + organization: {id: '123', storeCreation: {status: 'COMPLETE'}}, + }) + + await createDevStore({name: 'test-store', organization: defaultOrg, plan: 'basic', country: 'CA', json: true}) + + expect(outputResult).toHaveBeenCalledWith(expect.stringContaining('"country": "CA"')) + }) + + test('includes the country in the success output when provided', async () => { + vi.mocked(businessPlatformOrganizationsRequestDoc) + .mockResolvedValueOnce(defaultMutationResult) + .mockResolvedValueOnce({ + organization: {id: '123', storeCreation: {status: 'COMPLETE'}}, + }) + + await createDevStore({name: 'test-store', organization: defaultOrg, plan: 'plus', country: 'CA', json: false}) + + expect(renderSuccess).toHaveBeenCalledWith( + expect.objectContaining({ + body: expect.arrayContaining(['Country: CA']), + }), + ) + }) + test('outputs JSON when --json flag is set', async () => { vi.mocked(businessPlatformOrganizationsRequestDoc) .mockResolvedValueOnce(defaultMutationResult) diff --git a/packages/store/src/cli/services/store/create/dev.ts b/packages/store/src/cli/services/store/create/dev.ts index 9929f0fcd9e..8c515e431e5 100644 --- a/packages/store/src/cli/services/store/create/dev.ts +++ b/packages/store/src/cli/services/store/create/dev.ts @@ -22,6 +22,7 @@ interface CreateDevStoreOptions { organization: Organization featurePreview?: string withDemoData?: boolean + country?: string json: boolean } @@ -64,6 +65,10 @@ export async function createDevStore(options: CreateDevStoreOptions): Promise Date: Thu, 2 Jul 2026 00:51:57 +0300 Subject: [PATCH 2/6] Send country code when creating a dev store The --country flag was collected and shown in output but never sent to the createAppDevelopmentStore mutation. Wire it through as the 'country' argument now that the backend supports it. Assisted-By: devx/de0d96e7-211c-4a8b-991d-240b9f675823 --- .../generated/create_app_development_store.ts | 11 +++++++ .../create_app_development_store.graphql | 3 +- .../src/cli/services/store/create/dev.test.ts | 25 +++++++++++++++- .../src/cli/services/store/create/dev.ts | 30 +++++++++++-------- 4 files changed, 54 insertions(+), 15 deletions(-) diff --git a/packages/store/src/cli/api/graphql/business-platform-organizations/generated/create_app_development_store.ts b/packages/store/src/cli/api/graphql/business-platform-organizations/generated/create_app_development_store.ts index ed940362975..dc279d48a68 100644 --- a/packages/store/src/cli/api/graphql/business-platform-organizations/generated/create_app_development_store.ts +++ b/packages/store/src/cli/api/graphql/business-platform-organizations/generated/create_app_development_store.ts @@ -8,6 +8,7 @@ export type CreateAppDevelopmentStoreMutationVariables = Types.Exact<{ priceLookupKey: Types.Scalars['String']['input'] prepopulateTestData?: Types.InputMaybe developerPreviewHandle?: Types.InputMaybe + country?: Types.InputMaybe }> export type CreateAppDevelopmentStoreMutation = { @@ -46,6 +47,11 @@ export const CreateAppDevelopmentStore = { variable: {kind: 'Variable', name: {kind: 'Name', value: 'developerPreviewHandle'}}, type: {kind: 'NamedType', name: {kind: 'Name', value: 'String'}}, }, + { + kind: 'VariableDefinition', + variable: {kind: 'Variable', name: {kind: 'Name', value: 'country'}}, + type: {kind: 'NamedType', name: {kind: 'Name', value: 'String'}}, + }, ], selectionSet: { kind: 'SelectionSet', @@ -74,6 +80,11 @@ export const CreateAppDevelopmentStore = { name: {kind: 'Name', value: 'developerPreviewHandle'}, value: {kind: 'Variable', name: {kind: 'Name', value: 'developerPreviewHandle'}}, }, + { + kind: 'Argument', + name: {kind: 'Name', value: 'country'}, + value: {kind: 'Variable', name: {kind: 'Name', value: 'country'}}, + }, ], selectionSet: { kind: 'SelectionSet', diff --git a/packages/store/src/cli/api/graphql/business-platform-organizations/mutations/create_app_development_store.graphql b/packages/store/src/cli/api/graphql/business-platform-organizations/mutations/create_app_development_store.graphql index 4c2b7277baf..b6da18eb81e 100644 --- a/packages/store/src/cli/api/graphql/business-platform-organizations/mutations/create_app_development_store.graphql +++ b/packages/store/src/cli/api/graphql/business-platform-organizations/mutations/create_app_development_store.graphql @@ -1,9 +1,10 @@ -mutation CreateAppDevelopmentStore($shopName: String!, $priceLookupKey: String!, $prepopulateTestData: Boolean, $developerPreviewHandle: String) { +mutation CreateAppDevelopmentStore($shopName: String!, $priceLookupKey: String!, $prepopulateTestData: Boolean, $developerPreviewHandle: String, $country: String) { createAppDevelopmentStore( shopName: $shopName priceLookupKey: $priceLookupKey prepopulateTestData: $prepopulateTestData developerPreviewHandle: $developerPreviewHandle + country: $country ) { shopAdminUrl shopDomain diff --git a/packages/store/src/cli/services/store/create/dev.test.ts b/packages/store/src/cli/services/store/create/dev.test.ts index cf755cc7465..1a0fa3b37dd 100644 --- a/packages/store/src/cli/services/store/create/dev.test.ts +++ b/packages/store/src/cli/services/store/create/dev.test.ts @@ -71,6 +71,7 @@ describe('createDevStore', () => { priceLookupKey: 'SHOPIFY_PLUS_APP_DEVELOPMENT', prepopulateTestData: false, developerPreviewHandle: undefined, + country: undefined, }, }), ) @@ -161,6 +162,22 @@ describe('createDevStore', () => { expect(outputResult).toHaveBeenCalledWith(expect.stringContaining('"demoData": true')) }) + test('passes the country to the mutation', async () => { + vi.mocked(businessPlatformOrganizationsRequestDoc) + .mockResolvedValueOnce(defaultMutationResult) + .mockResolvedValueOnce({ + organization: {id: '123', storeCreation: {status: 'COMPLETE'}}, + }) + + await createDevStore({name: 'test-store', organization: defaultOrg, plan: 'plus', country: 'CA', json: false}) + + expect(businessPlatformOrganizationsRequestDoc).toHaveBeenCalledWith( + expect.objectContaining({ + variables: expect.objectContaining({country: 'CA'}), + }), + ) + }) + test('includes the country in JSON output when provided', async () => { vi.mocked(businessPlatformOrganizationsRequestDoc) .mockResolvedValueOnce(defaultMutationResult) @@ -184,7 +201,13 @@ describe('createDevStore', () => { expect(renderSuccess).toHaveBeenCalledWith( expect.objectContaining({ - body: expect.arrayContaining(['Country: CA']), + customSections: [ + expect.objectContaining({ + body: expect.objectContaining({ + tabularData: expect.arrayContaining([['Country', 'CA']]), + }), + }), + ], }), ) }) diff --git a/packages/store/src/cli/services/store/create/dev.ts b/packages/store/src/cli/services/store/create/dev.ts index 8c515e431e5..0061599a46e 100644 --- a/packages/store/src/cli/services/store/create/dev.ts +++ b/packages/store/src/cli/services/store/create/dev.ts @@ -8,7 +8,7 @@ import { import {Organization} from '@shopify/organizations' import {businessPlatformOrganizationsRequestDoc} from '@shopify/cli-kit/node/api/business-platform' import {ensureAuthenticatedBusinessPlatform} from '@shopify/cli-kit/node/session' -import {renderSingleTask, renderSuccess} from '@shopify/cli-kit/node/ui' +import {renderSingleTask, renderSuccess, type InlineToken} from '@shopify/cli-kit/node/ui' import {outputContent, outputResult} from '@shopify/cli-kit/node/output' import {AbortError} from '@shopify/cli-kit/node/error' import {sleep} from '@shopify/cli-kit/node/system' @@ -65,10 +65,7 @@ export async function createDevStore(options: CreateDevStoreOptions): Promise Date: Wed, 8 Jul 2026 18:41:52 +0300 Subject: [PATCH 3/6] Validate --country in flag parse to share logic across commands Assisted-By: devx/7f40dd7e-8ac5-44cb-8a46-99677b33f144 --- .../src/cli/commands/store/create/dev.ts | 6 +--- .../src/cli/commands/store/create/preview.ts | 6 +--- packages/store/src/cli/flags.ts | 36 +++++++++---------- 3 files changed, 20 insertions(+), 28 deletions(-) diff --git a/packages/store/src/cli/commands/store/create/dev.ts b/packages/store/src/cli/commands/store/create/dev.ts index a2db8d104ba..b11b062a1ed 100644 --- a/packages/store/src/cli/commands/store/create/dev.ts +++ b/packages/store/src/cli/commands/store/create/dev.ts @@ -1,7 +1,7 @@ import {createDevStore} from '../../../services/store/create/dev.js' import {devStorePlanHandles, DevStorePlan} from '../../../services/store/constants.js' import {storeNamePrompt, storePlanPrompt} from '../../../prompts/store.js' -import {devStoreFlags, invalidCountryCodeMessage, isCountryCode, storeFlags} from '../../../flags.js' +import {devStoreFlags, storeFlags} from '../../../flags.js' import {selectOrg} from '@shopify/organizations' import Command from '@shopify/cli-kit/node/base-command' import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli' @@ -47,10 +47,6 @@ export default class StoreCreateDev extends Command { const {flags} = await this.parse(StoreCreateDev) this.failMissingNonTTYFlags(flags, ['name', 'organization-id', 'plan']) - if (flags.country !== undefined && !isCountryCode(flags.country)) { - this.error(invalidCountryCodeMessage) - } - const organization = await selectOrg(flags['organization-id']?.toString()) const name = flags.name ?? (await storeNamePrompt()) const plan = (flags.plan as DevStorePlan | undefined) ?? (await storePlanPrompt()) diff --git a/packages/store/src/cli/commands/store/create/preview.ts b/packages/store/src/cli/commands/store/create/preview.ts index 125efdb122a..c864e6610f0 100644 --- a/packages/store/src/cli/commands/store/create/preview.ts +++ b/packages/store/src/cli/commands/store/create/preview.ts @@ -1,4 +1,4 @@ -import {invalidCountryCodeMessage, isCountryCode, previewStoreFlags} from '../../../flags.js' +import {previewStoreFlags} from '../../../flags.js' import {type CreatePreviewStoreResult, createPreviewStoreCommand} from '../../../services/store/create/preview/index.js' import {writeCreatePreviewStoreResult} from '../../../services/store/create/preview/result.js' import StoreCommand from '../../../utilities/store-command.js' @@ -36,10 +36,6 @@ export default class StoreCreatePreview extends StoreCommand { public async run(): Promise { const {flags} = await this.parse(StoreCreatePreview) - if (flags.country !== undefined && !isCountryCode(flags.country)) { - this.error(invalidCountryCodeMessage) - } - const result = await renderSingleTask({ title: outputContent`Creating store`, task: async () => createPreviewStoreCommand({name: flags.name, country: flags.country}), diff --git a/packages/store/src/cli/flags.ts b/packages/store/src/cli/flags.ts index a3ba1550495..3bc233225da 100644 --- a/packages/store/src/cli/flags.ts +++ b/packages/store/src/cli/flags.ts @@ -1,12 +1,21 @@ import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn' import {normalizeBulkOperationId} from '@shopify/cli-kit/node/api/bulk-operations' import {resolvePath} from '@shopify/cli-kit/node/path' +import {AbortError} from '@shopify/cli-kit/node/error' import {Flags} from '@oclif/core' +// Error message shown when a `--country` flag value is not a two-letter code. +const invalidCountryCodeMessage = 'Country must be a two-letter country code, for example: US.' + +// Matches a two-letter (ISO 3166-1 alpha-2 shaped) country code after normalization. +function isCountryCode(value: string): boolean { + return /^[A-Z]{2}$/.test(value) +} + /** * Builds a reusable `--country` flag. The value is normalized to an uppercase, - * trimmed string so downstream validation and the backend receive a consistent - * two-letter country code. + * trimmed string and validated during parsing, so every command that uses this + * flag rejects invalid codes with the same error before its `run` body executes. * * @param env - The environment variable that can supply the flag's value. */ @@ -15,25 +24,16 @@ export function countryFlag(env: string) { description: 'Two-letter country code for the store, such as US, CA, or GB.', env, required: false, - parse: async (value) => value.trim().toUpperCase(), + parse: async (value) => { + const normalized = value.trim().toUpperCase() + if (!isCountryCode(normalized)) { + throw new AbortError(invalidCountryCodeMessage) + } + return normalized + }, }) } -/** - * Returns true when the value is a two-letter (ISO 3166-1 alpha-2 shaped) - * country code. Assumes the value has already been normalized to uppercase by - * `countryFlag`'s parser. - */ -export function isCountryCode(value: string): boolean { - return /^[A-Z]{2}$/.test(value) -} - -/** - * Error message shown when a `--country` flag value is not a two-letter code. - * Shared so every store-creation command reports the same guidance. - */ -export const invalidCountryCodeMessage = 'Country must be a two-letter country code, for example: US.' - export const previewStoreFlags = { country: countryFlag('SHOPIFY_FLAG_PREVIEW_STORE_COUNTRY'), } From 7654e851cc8a2473eed5c4bec4fc3a5d6e781242 Mon Sep 17 00:00:00 2001 From: Ariel Caplan Date: Wed, 8 Jul 2026 18:42:58 +0300 Subject: [PATCH 4/6] Remove decisions files Assisted-By: devx/7f40dd7e-8ac5-44cb-8a46-99677b33f144 --- ...06-25-001-country-flag-store-create-dev.md | 42 --------------- ...02-country-flag-draft-pr-without-schema.md | 52 ------------------- 2 files changed, 94 deletions(-) delete mode 100644 decisions/2026-06-25-001-country-flag-store-create-dev.md delete mode 100644 decisions/2026-06-28-002-country-flag-draft-pr-without-schema.md diff --git a/decisions/2026-06-25-001-country-flag-store-create-dev.md b/decisions/2026-06-25-001-country-flag-store-create-dev.md deleted file mode 100644 index ad905293773..00000000000 --- a/decisions/2026-06-25-001-country-flag-store-create-dev.md +++ /dev/null @@ -1,42 +0,0 @@ -# 2026-06-25-001 — `--country` flag for `store create dev` (country piece of shop/world#22968) - -## Context -`shopify store create dev` needs a `--country` flag, mirroring `store create preview`, -which already accepts and validates a country code. The Business Platform -`createAppDevelopmentStore` mutation does not yet expose a `country` argument — that -backend change is an open (unmerged) PR in `shop/world`. The CLI fetches the published -BP schema from `shop/world` `main` via `bin/get-graphql-schemas`, and CI enforces a -graphql-codegen freshness check. - -## Decision -1. Extract and share the country validation logic in `packages/store/src/cli/flags.ts`: - - `countryFlag(env)` factory (already existed, now exported) builds a normalized - (trim + uppercase) `--country` flag. - - `isCountryCode(value)` (already existed) validates the two-letter shape. - - New `invalidCountryCodeMessage` constant so every store-creation command emits the - same error. - - New `devStoreFlags.country` (env `SHOPIFY_FLAG_STORE_COUNTRY`), alongside - `previewStoreFlags.country`. -2. Add `--country` to the `store create dev` command with shared validation, threaded - into `createDevStore` options and surfaced in JSON/success output. -3. Do **not** wire `country` into the `createAppDevelopmentStore` mutation variables or - the `.graphql` operation yet — left a code comment marking the single insertion point. -4. No changeset: the command is `hidden` and the flag is not yet functional end-to-end. - -## Rationale -- The shareable validation/flag logic is mergeable now and unblocks the CLI side ahead - of the backend. -- Adding `country` to the `.graphql` operation now would break the CI graphql-codegen - freshness check, because the published schema (`shop/world` main) lacks the argument - until the backend PR merges. Passing it in mutation `variables` would also be a - TypeScript error against the generated variables type. -- Referencing the flag via `devStoreFlags.country` (member expression) instead of a - direct `countryFlag(...)` call inside the command's `flags` block avoids a crash in the - `@shopify/cli/command-flags-with-env` lint rule, matching the existing - `previewStoreFlags.country` / `storeFlags['organization-id']` pattern. - -## Alternatives rejected -- Hand-editing the generated `create_app_development_store.ts` to add `country`: would be - reverted by `pnpm graphql-codegen` and fail the CI freshness check. -- Adding `country` to the `.graphql` operation now: breaks codegen against the live schema. -- Inlining the validation/error string per-command: duplicates logic the task asked to share. diff --git a/decisions/2026-06-28-002-country-flag-draft-pr-without-schema.md b/decisions/2026-06-28-002-country-flag-draft-pr-without-schema.md deleted file mode 100644 index ad99552d647..00000000000 --- a/decisions/2026-06-28-002-country-flag-draft-pr-without-schema.md +++ /dev/null @@ -1,52 +0,0 @@ -# 2026-06-28-002 — Ship `--country` flag as a draft PR without the BP schema wiring - -## Context -The `--country` flag for `store create dev` (see 2026-06-25-001) was tophatted locally -against a local Business Platform (`~/world/trees/pool-2`) whose `createAppDevelopmentStore` -mutation was patched to accept an optional `country: String` argument (Ariel's PR -`shop/world#671185`). The tophat involved three `TOPHAT ONLY` manual overrides that wired -`country` onto the GraphQL operation and into the mutation variables: -- `packages/store/src/cli/api/graphql/business-platform-organizations/mutations/create_app_development_store.graphql` -- `packages/store/src/cli/api/graphql/business-platform-organizations/generated/create_app_development_store.ts` -- `packages/store/src/cli/services/store/create/dev.ts` - -The tophat was verified working end-to-end locally after restarting the BP web process -(`overmind restart web` in `core/shopify`) to reload the memoized GraphQL-Ruby schema, and -attaching an `ApiClient` to the `shopify-cli` ServiceApp in the local DB. - -`shop/world#671185` is NOT yet merged to `main`. The CLI's `pnpm graphql-codegen` fetches -the published schema from `shop/world` `main`, and CI enforces a graphql-codegen freshness -check. - -## Decision -Open the CLI change as a **draft PR without the GraphQL schema wiring**: -1. Reverted all three `TOPHAT ONLY` overrides so the `.graphql` operation and generated - `.ts` are byte-identical to `HEAD` (passes the codegen freshness check). -2. Kept the full flag scaffolding: `--country` flag, shared `countryFlag` / - `isCountryCode` / `invalidCountryCodeMessage`, `devStoreFlags.country`, plumbing into - `createDevStore` options, and `country` in JSON/success output. -3. The service no longer sends `country` to BP; replaced the tophat comment with a NOTE - marking the single insertion point for when the backend lands. -4. No changeset: the command is `hidden` and `country` is not functional end-to-end. - -## Rationale -- A single CI-green PR cannot send `country` on the wire until the published schema has - the argument; keeping the tophat edits would leave CI red until the BP PR merges. -- Shipping the flag scaffolding now (green) gets the shareable validation/flag logic - reviewed and landed, decoupled from the backend timeline. -- The wiring is a trivial follow-up once `shop/world#671185` merges to `main`. - -## Follow-up (once shop/world#671185 merges to main) -1. `pnpm graphql-codegen:get-graphql-schemas && pnpm graphql-codegen` -2. Add `$country: String` to `create_app_development_store.graphql` and the - `createAppDevelopmentStore(country: $country)` argument. -3. Replace the NOTE in `services/store/create/dev.ts` with `country: options.country`. -4. Add a changeset and unhide the command when the broader feature is ready. - -## Alternatives rejected -- **Single stacked/draft PR keeping the `TOPHAT ONLY` edits** (Option A): accurately - communicates the dependency but leaves CI red on the codegen freshness check until the - BP PR merges. Rejected in favor of a green draft per Ariel's preference. -- **Keeping `country: options.country` in the service while reverting the generated - variables type**: TypeScript error — the object literal would reference a property the - reverted `CreateAppDevelopmentStoreMutationVariables` type no longer declares. From dfaab400541dff360f41ddafe7748da64080f316 Mon Sep 17 00:00:00 2001 From: Ariel Caplan Date: Wed, 8 Jul 2026 18:48:50 +0300 Subject: [PATCH 5/6] Consolidate --country into a single shared flag constant Assisted-By: devx/7f40dd7e-8ac5-44cb-8a46-99677b33f144 --- packages/cli/oclif.manifest.json | 2 +- .../src/cli/commands/store/create/dev.ts | 4 +- .../src/cli/commands/store/create/preview.ts | 4 +- packages/store/src/cli/flags.ts | 43 +++++++------------ 4 files changed, 21 insertions(+), 32 deletions(-) diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index 1164463f8b5..8f332df0300 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -6423,7 +6423,7 @@ "flags": { "country": { "description": "Two-letter country code for the store, such as US, CA, or GB.", - "env": "SHOPIFY_FLAG_PREVIEW_STORE_COUNTRY", + "env": "SHOPIFY_FLAG_STORE_COUNTRY", "hasDynamicHelp": false, "multiple": false, "name": "country", diff --git a/packages/store/src/cli/commands/store/create/dev.ts b/packages/store/src/cli/commands/store/create/dev.ts index b11b062a1ed..6b651fa6f77 100644 --- a/packages/store/src/cli/commands/store/create/dev.ts +++ b/packages/store/src/cli/commands/store/create/dev.ts @@ -1,7 +1,7 @@ import {createDevStore} from '../../../services/store/create/dev.js' import {devStorePlanHandles, DevStorePlan} from '../../../services/store/constants.js' import {storeNamePrompt, storePlanPrompt} from '../../../prompts/store.js' -import {devStoreFlags, storeFlags} from '../../../flags.js' +import {countryFlag, storeFlags} from '../../../flags.js' import {selectOrg} from '@shopify/organizations' import Command from '@shopify/cli-kit/node/base-command' import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli' @@ -40,7 +40,7 @@ export default class StoreCreateDev extends Command { default: false, env: 'SHOPIFY_FLAG_STORE_WITH_DEMO_DATA', }), - country: devStoreFlags.country, + country: countryFlag, } async run(): Promise { diff --git a/packages/store/src/cli/commands/store/create/preview.ts b/packages/store/src/cli/commands/store/create/preview.ts index c864e6610f0..34c3ab927d3 100644 --- a/packages/store/src/cli/commands/store/create/preview.ts +++ b/packages/store/src/cli/commands/store/create/preview.ts @@ -1,4 +1,4 @@ -import {previewStoreFlags} from '../../../flags.js' +import {countryFlag} from '../../../flags.js' import {type CreatePreviewStoreResult, createPreviewStoreCommand} from '../../../services/store/create/preview/index.js' import {writeCreatePreviewStoreResult} from '../../../services/store/create/preview/result.js' import StoreCommand from '../../../utilities/store-command.js' @@ -30,7 +30,7 @@ export default class StoreCreatePreview extends StoreCommand { env: 'SHOPIFY_FLAG_PREVIEW_STORE_NAME', required: false, }), - country: previewStoreFlags.country, + country: countryFlag, } public async run(): Promise { diff --git a/packages/store/src/cli/flags.ts b/packages/store/src/cli/flags.ts index 3bc233225da..0529b035cf8 100644 --- a/packages/store/src/cli/flags.ts +++ b/packages/store/src/cli/flags.ts @@ -13,34 +13,23 @@ function isCountryCode(value: string): boolean { } /** - * Builds a reusable `--country` flag. The value is normalized to an uppercase, - * trimmed string and validated during parsing, so every command that uses this - * flag rejects invalid codes with the same error before its `run` body executes. - * - * @param env - The environment variable that can supply the flag's value. + * Reusable `--country` flag shared by every store-creation command. The value + * is normalized to an uppercase, trimmed string and validated during parsing, + * so invalid codes are rejected with the same error before a command's `run` + * body executes. */ -export function countryFlag(env: string) { - return Flags.string({ - description: 'Two-letter country code for the store, such as US, CA, or GB.', - env, - required: false, - parse: async (value) => { - const normalized = value.trim().toUpperCase() - if (!isCountryCode(normalized)) { - throw new AbortError(invalidCountryCodeMessage) - } - return normalized - }, - }) -} - -export const previewStoreFlags = { - country: countryFlag('SHOPIFY_FLAG_PREVIEW_STORE_COUNTRY'), -} - -export const devStoreFlags = { - country: countryFlag('SHOPIFY_FLAG_STORE_COUNTRY'), -} +export const countryFlag = Flags.string({ + description: 'Two-letter country code for the store, such as US, CA, or GB.', + env: 'SHOPIFY_FLAG_STORE_COUNTRY', + required: false, + parse: async (value) => { + const normalized = value.trim().toUpperCase() + if (!isCountryCode(normalized)) { + throw new AbortError(invalidCountryCodeMessage) + } + return normalized + }, +}) export const storeFlags = { store: Flags.string({ From 2d79e39f929c4f1fd9f969c3ec682867c1efe5b3 Mon Sep 17 00:00:00 2001 From: Ariel Caplan Date: Wed, 8 Jul 2026 21:31:23 +0300 Subject: [PATCH 6/6] Always render Admin row with N/A fallback in dev store success output Assisted-By: devx/7f40dd7e-8ac5-44cb-8a46-99677b33f144 --- .../src/cli/services/store/create/dev.test.ts | 28 +++++++++++++++++++ .../src/cli/services/store/create/dev.ts | 4 ++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/packages/store/src/cli/services/store/create/dev.test.ts b/packages/store/src/cli/services/store/create/dev.test.ts index 1a0fa3b37dd..750c602a0c3 100644 --- a/packages/store/src/cli/services/store/create/dev.test.ts +++ b/packages/store/src/cli/services/store/create/dev.test.ts @@ -212,6 +212,34 @@ describe('createDevStore', () => { ) }) + test('renders Admin as N/A in the success output when the admin URL is missing', async () => { + vi.mocked(businessPlatformOrganizationsRequestDoc) + .mockResolvedValueOnce({ + createAppDevelopmentStore: { + shopAdminUrl: null, + shopDomain: 'test-store.myshopify.com', + userErrors: [], + }, + }) + .mockResolvedValueOnce({ + organization: {id: '123', storeCreation: {status: 'COMPLETE'}}, + }) + + await createDevStore({name: 'test-store', organization: defaultOrg, plan: 'plus', json: false}) + + expect(renderSuccess).toHaveBeenCalledWith( + expect.objectContaining({ + customSections: [ + expect.objectContaining({ + body: expect.objectContaining({ + tabularData: expect.arrayContaining([['Admin', 'N/A']]), + }), + }), + ], + }), + ) + }) + test('outputs JSON when --json flag is set', async () => { vi.mocked(businessPlatformOrganizationsRequestDoc) .mockResolvedValueOnce(defaultMutationResult) diff --git a/packages/store/src/cli/services/store/create/dev.ts b/packages/store/src/cli/services/store/create/dev.ts index 0061599a46e..e92113204d6 100644 --- a/packages/store/src/cli/services/store/create/dev.ts +++ b/packages/store/src/cli/services/store/create/dev.ts @@ -149,7 +149,9 @@ export async function createDevStore(options: CreateDevStoreOptions): Promise