From 970e05e99d5f83fed38aec153cf1ae1f9d875b64 Mon Sep 17 00:00:00 2001 From: Gonzalo Riestra Date: Mon, 25 May 2026 14:23:15 +0200 Subject: [PATCH] Add non-interactive app dev prompt bypasses --- packages/app/src/cli/commands/app/dev.ts | 14 ++++ .../src/cli/services/app/config/link.test.ts | 19 +++++ .../app/src/cli/services/app/config/link.ts | 2 +- packages/app/src/cli/services/dev.ts | 4 + .../src/cli/services/dev/select-store.test.ts | 21 +++++ .../app/src/cli/services/dev/select-store.ts | 13 +++- .../src/cli/services/store-context.test.ts | 47 +++++++++++ .../app/src/cli/services/store-context.ts | 24 +++++- packages/app/src/cli/utilities/mkcert.test.ts | 35 +++++++++ packages/app/src/cli/utilities/mkcert.ts | 4 +- packages/cli/README.md | 78 ++++++++++--------- packages/cli/oclif.manifest.json | 14 ++++ 12 files changed, 231 insertions(+), 44 deletions(-) diff --git a/packages/app/src/cli/commands/app/dev.ts b/packages/app/src/cli/commands/app/dev.ts index fc9e47f9681..c6ca5aaa442 100644 --- a/packages/app/src/cli/commands/app/dev.ts +++ b/packages/app/src/cli/commands/app/dev.ts @@ -57,6 +57,12 @@ export default class Dev extends AppLinkedCommand { default: false, exclusive: ['tunnel-url'], }), + 'install-mkcert': Flags.boolean({ + description: + 'Install and use mkcert to generate localhost certificates when --use-localhost is enabled. Use --no-install-mkcert to fail instead of prompting when certificates are missing.', + env: 'SHOPIFY_FLAG_INSTALL_MKCERT', + allowNo: true, + }), 'localhost-port': Flags.integer({ description: 'Port to use for localhost.', env: 'SHOPIFY_FLAG_LOCALHOST_PORT', @@ -75,6 +81,12 @@ export default class Dev extends AppLinkedCommand { 'The file path or URL. The file path is to a file that you want updated on idle. The URL path is where you want a webhook posted to report on file changes.', env: 'SHOPIFY_FLAG_NOTIFY', }), + 'convert-transfer-disabled-store': Flags.boolean({ + description: + 'Convert the selected development store to a transfer-disabled store without prompting. Use --no-convert-transfer-disabled-store to fail instead of prompting.', + env: 'SHOPIFY_FLAG_CONVERT_TRANSFER_DISABLED_STORE', + allowNo: true, + }), 'graphiql-port': Flags.integer({ hidden: true, description: 'Local port of the GraphiQL development server.', @@ -121,6 +133,7 @@ export default class Dev extends AppLinkedCommand { appContextResult, storeFqdn: flags.store, forceReselectStore: flags.reset, + transferDisabledStoreConversion: flags['convert-transfer-disabled-store'], }) const devOptions: DevOptions = { @@ -137,6 +150,7 @@ export default class Dev extends AppLinkedCommand { notify: flags.notify, graphiqlPort: flags['graphiql-port'], graphiqlKey: flags['graphiql-key'], + installMkcert: flags['install-mkcert'], tunnel: tunnelMode, } diff --git a/packages/app/src/cli/services/app/config/link.test.ts b/packages/app/src/cli/services/app/config/link.test.ts index d06ff3ce9e8..6735e3e60c1 100644 --- a/packages/app/src/cli/services/app/config/link.test.ts +++ b/packages/app/src/cli/services/app/config/link.test.ts @@ -1186,6 +1186,25 @@ describe('link', () => { }) }) + test('when remote app exists and supports dev sessions then include automatically_update_urls_on_dev = true', async () => { + await inTemporaryDirectory(async (tmp) => { + // Given + const developerPlatformClient = buildDeveloperPlatformClient({supportsDevSessions: true}) + const options: LinkOptions = { + directory: tmp, + developerPlatformClient, + } + await mockLoadOpaqueAppWithApp(tmp) + vi.mocked(fetchOrCreateOrganizationApp).mockResolvedValue(mockRemoteApp({developerPlatformClient})) + + // When + const {configuration} = await link(options) + + // Then + expect(configuration.build?.automatically_update_urls_on_dev).toBe(true) + }) + }) + test('replace arrays content with the remote one', async () => { await inTemporaryDirectory(async (tmp) => { // Given diff --git a/packages/app/src/cli/services/app/config/link.ts b/packages/app/src/cli/services/app/config/link.ts index 23d10efa0ca..18093fe8ff2 100644 --- a/packages/app/src/cli/services/app/config/link.ts +++ b/packages/app/src/cli/services/app/config/link.ts @@ -388,7 +388,7 @@ function buildOptionsForGeneratedConfigFile(options: { } = options const buildOptions = { ...(linkedAppWasNewlyCreated ? {include_config_on_deploy: true} : {}), - ...(defaultToUpdateUrlsOnDev && linkedAppWasNewlyCreated ? {automatically_update_urls_on_dev: true} : {}), + ...(defaultToUpdateUrlsOnDev ? {automatically_update_urls_on_dev: true} : {}), ...(linkedAppAndClientIdFromFileAreInSync ? existingBuildOptions : {}), } if (isEmpty(buildOptions)) { diff --git a/packages/app/src/cli/services/dev.ts b/packages/app/src/cli/services/dev.ts index 2798403311b..21e67a56498 100644 --- a/packages/app/src/cli/services/dev.ts +++ b/packages/app/src/cli/services/dev.ts @@ -71,6 +71,7 @@ export interface DevOptions { notify?: string graphiqlPort?: number graphiqlKey?: string + installMkcert?: boolean } export async function dev(commandOptions: DevOptions) { @@ -152,6 +153,7 @@ async function prepareForDev(commandOptions: DevOptions): Promise { tunnel, tunnelClient, remoteApp.configuration, + commandOptions.installMkcert, ) app.webs = webs @@ -324,6 +326,7 @@ async function setupNetworkingOptions( tunnelOptions: TunnelMode, tunnelClient?: TunnelClient, remoteAppConfig?: AppConfigurationUsedByCli, + installMkcert?: boolean, ) { const {backendConfig, frontendConfig} = frontAndBackendConfig(webs) @@ -361,6 +364,7 @@ async function setupNetworkingOptions( if (tunnelOptions.mode === 'use-localhost') { const {keyContent, certContent, certPath} = await generateCertificate({ appDirectory, + install: installMkcert, }) reverseProxyCert = { diff --git a/packages/app/src/cli/services/dev/select-store.test.ts b/packages/app/src/cli/services/dev/select-store.test.ts index 35bb89ac27c..41d0341f808 100644 --- a/packages/app/src/cli/services/dev/select-store.test.ts +++ b/packages/app/src/cli/services/dev/select-store.test.ts @@ -114,6 +114,27 @@ describe('selectStore', async () => { expect(confirmConversionToTransferDisabledStorePrompt).toHaveBeenCalled() }) + test('converts store without prompting when conversion mode is always', async () => { + // Given + vi.clearAllMocks() + vi.mocked(selectStorePrompt).mockResolvedValueOnce(STORE2) + const developerPlatformClient = testDeveloperPlatformClient({clientName: ClientName.Partners}) + const convertToTransferDisabledStore = vi.spyOn(developerPlatformClient, 'convertToTransferDisabledStore') + + // When + const got = await selectStore( + {stores: [STORE1, STORE2], hasMorePages: false}, + ORG1, + developerPlatformClient, + 'always', + ) + + // Then + expect(got).toEqual(STORE2) + expect(confirmConversionToTransferDisabledStorePrompt).not.toHaveBeenCalled() + expect(convertToTransferDisabledStore).toHaveBeenCalled() + }) + test('choosing not to convert to transfer-disabled forces another prompt', async () => { // Given vi.mocked(selectStorePrompt).mockResolvedValueOnce(STORE2) diff --git a/packages/app/src/cli/services/dev/select-store.ts b/packages/app/src/cli/services/dev/select-store.ts index d02c8969ee0..f6882b333ad 100644 --- a/packages/app/src/cli/services/dev/select-store.ts +++ b/packages/app/src/cli/services/dev/select-store.ts @@ -15,6 +15,8 @@ import {firstPartyDev} from '@shopify/cli-kit/node/context/local' import {AbortError, BugError, CancelExecution} from '@shopify/cli-kit/node/error' import {outputSuccess} from '@shopify/cli-kit/node/output' +export type TransferDisabledStoreConversionMode = 'prompt-first' | 'always' | 'never' + /** * Select store from list or * If a cachedStoreName is provided, we check if it is valid and return it. If it's not valid, ignore it. @@ -29,6 +31,7 @@ export async function selectStore( storesSearch: Paginateable<{stores: OrganizationStore[]}>, org: Organization, developerPlatformClient: DeveloperPlatformClient, + conversionMode: TransferDisabledStoreConversionMode = 'prompt-first', ): Promise { const showDomainOnPrompt = developerPlatformClient.clientName === ClientName.AppManagement let onSearchForStoresByName @@ -61,7 +64,7 @@ export async function selectStore( store, org.id, developerPlatformClient, - 'prompt-first', + conversionMode, ) while (!storeIsValid) { // eslint-disable-next-line no-await-in-loop @@ -70,7 +73,7 @@ export async function selectStore( throw new CancelExecution() } // eslint-disable-next-line no-await-in-loop - storeIsValid = await convertToTransferDisabledStoreIfNeeded(store, org.id, developerPlatformClient, 'prompt-first') + storeIsValid = await convertToTransferDisabledStoreIfNeeded(store, org.id, developerPlatformClient, conversionMode) } return store @@ -128,7 +131,7 @@ export async function convertToTransferDisabledStoreIfNeeded( store: OrganizationStore, orgId: string, developerPlatformClient: DeveloperPlatformClient, - conversionMode: 'prompt-first' | 'never', + conversionMode: TransferDisabledStoreConversionMode, ): Promise { if (store.transferDisabled || firstPartyDev()) return true @@ -149,6 +152,10 @@ export async function convertToTransferDisabledStoreIfNeeded( await convertStoreToTransferDisabled(store, orgId, developerPlatformClient) return true } + case 'always': { + await convertStoreToTransferDisabled(store, orgId, developerPlatformClient) + return true + } case 'never': { throw new AbortError( 'The store you specified is not transfer-disabled', diff --git a/packages/app/src/cli/services/store-context.test.ts b/packages/app/src/cli/services/store-context.test.ts index 6b3bd1e0306..45341312dee 100644 --- a/packages/app/src/cli/services/store-context.test.ts +++ b/packages/app/src/cli/services/store-context.test.ts @@ -116,6 +116,7 @@ describe('storeContext', () => { {stores: allStores, hasMorePages: false}, mockOrganization, mockDeveloperPlatformClient, + 'prompt-first', ) expect(result).toEqual(mockStore) }) @@ -138,11 +139,57 @@ describe('storeContext', () => { {stores: allStores, hasMorePages: false}, mockOrganization, mockDeveloperPlatformClient, + 'prompt-first', ) expect(result).toEqual(mockStore) }) }) + test('converts an explicitly provided store when conversion is requested', async () => { + await inTemporaryDirectory(async (dir) => { + vi.mocked(fetchStore).mockResolvedValue(mockStore) + await prepareAppFolder(mockApp, dir) + + await storeContext({ + appContextResult, + storeFqdn: 'explicit-store.myshopify.com', + forceReselectStore: false, + transferDisabledStoreConversion: true, + }) + + expect(convertToTransferDisabledStoreIfNeeded).toHaveBeenCalledWith( + mockStore, + mockOrganization.id, + mockDeveloperPlatformClient, + 'always', + ) + }) + }) + + test('passes conversion preference when selecting a store', async () => { + await inTemporaryDirectory(async (dir) => { + const appWithoutCachedStore = testAppLinked() + await prepareAppFolder(appWithoutCachedStore, dir) + const allStores = [mockStore] + + vi.mocked(mockDeveloperPlatformClient.devStoresForOrg).mockResolvedValue({stores: allStores, hasMorePages: false}) + vi.mocked(selectStore).mockResolvedValue(mockStore) + + await storeContext({ + appContextResult: {...appContextResult, app: appWithoutCachedStore}, + forceReselectStore: false, + transferDisabledStoreConversion: true, + }) + + expect(selectStore).toHaveBeenCalledWith( + {stores: allStores, hasMorePages: false}, + mockOrganization, + mockDeveloperPlatformClient, + 'always', + ) + }) + }) + test('throws an error when fetchStore fails', async () => { vi.mocked(fetchStore).mockRejectedValue(new Error('Store not found')) diff --git a/packages/app/src/cli/services/store-context.ts b/packages/app/src/cli/services/store-context.ts index a0df8b837eb..662656e00bd 100644 --- a/packages/app/src/cli/services/store-context.ts +++ b/packages/app/src/cli/services/store-context.ts @@ -1,5 +1,9 @@ import {fetchStore} from './dev/fetch.js' -import {convertToTransferDisabledStoreIfNeeded, selectStore} from './dev/select-store.js' +import { + convertToTransferDisabledStoreIfNeeded, + selectStore, + type TransferDisabledStoreConversionMode, +} from './dev/select-store.js' import {LoadedAppContextOutput} from './app-context.js' import {OrganizationStore} from '../models/organization.js' import {Store} from '../utilities/developer-platform-client.js' @@ -20,6 +24,7 @@ interface StoreContextOptions { forceReselectStore: boolean storeFqdn?: string storeTypes?: Store[] + transferDisabledStoreConversion?: boolean } /** @@ -34,6 +39,7 @@ export async function storeContext({ storeFqdn, forceReselectStore, storeTypes = ['APP_DEVELOPMENT'], + transferDisabledStoreConversion, }: StoreContextOptions): Promise { const {app, organization, developerPlatformClient} = appContextResult let selectedStore: OrganizationStore @@ -51,17 +57,18 @@ export async function storeContext({ // Check if we're filtering to dev stores only const isDevStoresOnly = storeTypes.length === 1 && storeTypes[0] === 'APP_DEVELOPMENT' + const conversionMode = transferDisabledStoreConversionMode(transferDisabledStoreConversion, Boolean(storeFqdnToUse)) if (storeFqdnToUse) { selectedStore = await fetchStore(organization, storeFqdnToUse, developerPlatformClient, storeTypes) - // never automatically convert a store provided via the command line + // Explicit stores keep the previous fail-fast behavior unless conversion is also explicit. if (isDevStoresOnly) { - await convertToTransferDisabledStoreIfNeeded(selectedStore, organization.id, developerPlatformClient, 'never') + await convertToTransferDisabledStoreIfNeeded(selectedStore, organization.id, developerPlatformClient, conversionMode) } } else { // If no storeFqdn is provided, fetch all stores for the organization and let the user select one. const allStores = await developerPlatformClient.devStoresForOrg(organization.id) - selectedStore = await selectStore(allStores, organization, developerPlatformClient) + selectedStore = await selectStore(allStores, organization, developerPlatformClient, conversionMode) } await logMetadata(selectedStore, forceReselectStore) @@ -78,6 +85,15 @@ export async function storeContext({ return selectedStore } +function transferDisabledStoreConversionMode( + transferDisabledStoreConversion: boolean | undefined, + storeFqdnToUse: boolean, +): TransferDisabledStoreConversionMode { + if (transferDisabledStoreConversion === true) return 'always' + if (transferDisabledStoreConversion === false || storeFqdnToUse) return 'never' + return 'prompt-first' +} + async function logMetadata(selectedStore: OrganizationStore, resetUsed: boolean) { await metadata.addPublicMetadata(() => ({ cmd_app_reset_used: resetUsed, diff --git a/packages/app/src/cli/utilities/mkcert.test.ts b/packages/app/src/cli/utilities/mkcert.test.ts index 79ed64b2634..15cd63bdacc 100644 --- a/packages/app/src/cli/utilities/mkcert.test.ts +++ b/packages/app/src/cli/utilities/mkcert.test.ts @@ -216,6 +216,41 @@ describe('mkcert', () => { expect(downloadGitHubRelease).not.toHaveBeenCalled() }) + testWithTempDir('generates a certificate without prompting when install is true', async ({tempDir}) => { + const appDirectory = tempDir + vi.mocked(generateCertificatePrompt).mockClear() + vi.mocked(exec).mockClear() + + vi.mocked(exec).mockImplementation(async () => { + await mkdir(joinPath(appDirectory, '.shopify')) + await writeFile(joinPath(appDirectory, '.shopify', 'localhost-key.pem'), 'key') + await writeFile(joinPath(appDirectory, '.shopify', 'localhost.pem'), 'cert') + }) + + await generateCertificate({ + appDirectory, + install: true, + platform: 'linux', + }) + + expect(generateCertificatePrompt).not.toHaveBeenCalled() + expect(exec).toHaveBeenCalled() + }) + + testWithTempDir('fails without prompting when install is false and certificates are missing', async ({tempDir}) => { + vi.mocked(generateCertificatePrompt).mockClear() + vi.mocked(exec).mockClear() + const generatePromise = generateCertificate({ + appDirectory: tempDir, + install: false, + platform: 'linux', + }) + + await expect(generatePromise).rejects.toThrow(AbortError) + expect(generateCertificatePrompt).not.toHaveBeenCalled() + expect(exec).not.toHaveBeenCalled() + }) + testWithTempDir('skips certificate generation if the certificate already exists', async ({tempDir}) => { const appDirectory = tempDir await mkdir(joinPath(appDirectory, '.shopify')) diff --git a/packages/app/src/cli/utilities/mkcert.ts b/packages/app/src/cli/utilities/mkcert.ts index b5f97f70dcd..ed181bc3f72 100644 --- a/packages/app/src/cli/utilities/mkcert.ts +++ b/packages/app/src/cli/utilities/mkcert.ts @@ -121,6 +121,7 @@ async function downloadMkcertLicense(dotShopifyPath: string): Promise] [--client-id | -c ] [--localhost-port - ] [--no-color] [--no-update] [--notify ] [--path ] [--reset | ] - [--skip-dependencies-installation] [-s ] [--subscription-product-url ] [-t ] - [--theme-app-extension-port ] [--use-localhost | [--tunnel-url | ]] [--verbose] - -FLAGS - -c, --config= [env: SHOPIFY_FLAG_APP_CONFIG] The name of the app configuration. - -s, --store= [env: SHOPIFY_FLAG_STORE] Store URL. Must be an existing development or - Shopify Plus sandbox store. - -t, --theme= [env: SHOPIFY_FLAG_THEME] Theme ID or name of the theme app extension host - theme. - --checkout-cart-url= [env: SHOPIFY_FLAG_CHECKOUT_CART_URL] Resource URL for checkout UI extension. - Format: "/cart/{productVariantID}:{productQuantity}" - --client-id= [env: SHOPIFY_FLAG_CLIENT_ID] The Client ID of your app. - --localhost-port= [env: SHOPIFY_FLAG_LOCALHOST_PORT] Port to use for localhost. - --no-color [env: SHOPIFY_FLAG_NO_COLOR] Disable color output. - --no-update [env: SHOPIFY_FLAG_NO_UPDATE] Uses the app URL from the toml file instead an - autogenerated URL for dev. - --notify= [env: SHOPIFY_FLAG_NOTIFY] The file path or URL. The file path is to a file - that you want updated on idle. The URL path is where you want a webhook posted - to report on file changes. - --path= [env: SHOPIFY_FLAG_PATH] The path to your app directory. - --reset [env: SHOPIFY_FLAG_RESET] Reset all your settings. - --skip-dependencies-installation [env: SHOPIFY_FLAG_SKIP_DEPENDENCIES_INSTALLATION] Skips the installation of - dependencies. Deprecated, use workspaces instead. - --subscription-product-url= [env: SHOPIFY_FLAG_SUBSCRIPTION_PRODUCT_URL] Resource URL for subscription UI - extension. Format: "/products/{productId}" - --theme-app-extension-port= [env: SHOPIFY_FLAG_THEME_APP_EXTENSION_PORT] Local port of the theme app - extension development server. - --tunnel-url= [env: SHOPIFY_FLAG_TUNNEL_URL] Use a custom tunnel, it must be running before - executing dev. Format: "https://my-tunnel-url:port". - --use-localhost [env: SHOPIFY_FLAG_USE_LOCALHOST] Service entry point will listen to - localhost. A tunnel won't be used. Will work for testing many app features, - but not those that directly invoke your app (E.g: Webhooks) - --verbose [env: SHOPIFY_FLAG_VERBOSE] Increase the verbosity of the output. + $ shopify app dev [--checkout-cart-url ] [--client-id | -c ] + [--convert-transfer-disabled-store] [--install-mkcert] [--localhost-port ] [--no-color] [--no-update] + [--notify ] [--path ] [--reset | ] [--skip-dependencies-installation] [-s ] + [--subscription-product-url ] [-t ] [--theme-app-extension-port ] [--use-localhost | + [--tunnel-url | ]] [--verbose] + +FLAGS + -c, --config= [env: SHOPIFY_FLAG_APP_CONFIG] The name of the app configuration. + -s, --store= [env: SHOPIFY_FLAG_STORE] Store URL. Must be an existing development or + Shopify Plus sandbox store. + -t, --theme= [env: SHOPIFY_FLAG_THEME] Theme ID or name of the theme app extension host + theme. + --checkout-cart-url= [env: SHOPIFY_FLAG_CHECKOUT_CART_URL] Resource URL for checkout UI + extension. Format: "/cart/{productVariantID}:{productQuantity}" + --client-id= [env: SHOPIFY_FLAG_CLIENT_ID] The Client ID of your app. + --[no-]convert-transfer-disabled-store [env: SHOPIFY_FLAG_CONVERT_TRANSFER_DISABLED_STORE] Convert the selected + development store to a transfer-disabled store without prompting. Use + --no-convert-transfer-disabled-store to fail instead of prompting. + --[no-]install-mkcert [env: SHOPIFY_FLAG_INSTALL_MKCERT] Install and use mkcert to generate + localhost certificates when --use-localhost is enabled. Use + --no-install-mkcert to fail instead of prompting when certificates are + missing. + --localhost-port= [env: SHOPIFY_FLAG_LOCALHOST_PORT] Port to use for localhost. + --no-color [env: SHOPIFY_FLAG_NO_COLOR] Disable color output. + --no-update [env: SHOPIFY_FLAG_NO_UPDATE] Uses the app URL from the toml file instead + an autogenerated URL for dev. + --notify= [env: SHOPIFY_FLAG_NOTIFY] The file path or URL. The file path is to a + file that you want updated on idle. The URL path is where you want a + webhook posted to report on file changes. + --path= [env: SHOPIFY_FLAG_PATH] The path to your app directory. + --reset [env: SHOPIFY_FLAG_RESET] Reset all your settings. + --skip-dependencies-installation [env: SHOPIFY_FLAG_SKIP_DEPENDENCIES_INSTALLATION] Skips the installation + of dependencies. Deprecated, use workspaces instead. + --subscription-product-url= [env: SHOPIFY_FLAG_SUBSCRIPTION_PRODUCT_URL] Resource URL for subscription + UI extension. Format: "/products/{productId}" + --theme-app-extension-port= [env: SHOPIFY_FLAG_THEME_APP_EXTENSION_PORT] Local port of the theme app + extension development server. + --tunnel-url= [env: SHOPIFY_FLAG_TUNNEL_URL] Use a custom tunnel, it must be running + before executing dev. Format: "https://my-tunnel-url:port". + --use-localhost [env: SHOPIFY_FLAG_USE_LOCALHOST] Service entry point will listen to + localhost. A tunnel won't be used. Will work for testing many app + features, but not those that directly invoke your app (E.g: Webhooks) + --verbose [env: SHOPIFY_FLAG_VERBOSE] Increase the verbosity of the output. DESCRIPTION Run the app. diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index db618ce811f..cff44545da2 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -919,6 +919,13 @@ "name": "config", "type": "option" }, + "convert-transfer-disabled-store": { + "allowNo": true, + "description": "Convert the selected development store to a transfer-disabled store without prompting. Use --no-convert-transfer-disabled-store to fail instead of prompting.", + "env": "SHOPIFY_FLAG_CONVERT_TRANSFER_DISABLED_STORE", + "name": "convert-transfer-disabled-store", + "type": "boolean" + }, "graphiql-key": { "description": "Key used to authenticate GraphiQL requests. By default, a key is automatically derived from the app secret. Use this flag to override with a custom key.", "env": "SHOPIFY_FLAG_GRAPHIQL_KEY", @@ -937,6 +944,13 @@ "name": "graphiql-port", "type": "option" }, + "install-mkcert": { + "allowNo": true, + "description": "Install and use mkcert to generate localhost certificates when --use-localhost is enabled. Use --no-install-mkcert to fail instead of prompting when certificates are missing.", + "env": "SHOPIFY_FLAG_INSTALL_MKCERT", + "name": "install-mkcert", + "type": "boolean" + }, "localhost-port": { "description": "Port to use for localhost.", "env": "SHOPIFY_FLAG_LOCALHOST_PORT",