From ce3407153d69eac2b0ab034d9eced8d86e70c507 Mon Sep 17 00:00:00 2001 From: Ariel Caplan Date: Mon, 29 Jun 2026 19:04:51 +0300 Subject: [PATCH 1/3] Add `shopify store open` command Opens a store's storefront in the default browser. Reuses the store info lookup so preview stores open their tokenized access URL. Assisted-By: devx/14c34c82-e316-418f-9255-474db36c1b87 --- .changeset/store-open-command.md | 6 +++ .../store/src/cli/commands/store/open.test.ts | 17 +++++++ packages/store/src/cli/commands/store/open.ts | 25 ++++++++++ .../store/src/cli/services/store/open.test.ts | 49 +++++++++++++++++++ packages/store/src/cli/services/store/open.ts | 48 ++++++++++++++++++ packages/store/src/index.ts | 2 + 6 files changed, 147 insertions(+) create mode 100644 .changeset/store-open-command.md create mode 100644 packages/store/src/cli/commands/store/open.test.ts create mode 100644 packages/store/src/cli/commands/store/open.ts create mode 100644 packages/store/src/cli/services/store/open.test.ts create mode 100644 packages/store/src/cli/services/store/open.ts diff --git a/.changeset/store-open-command.md b/.changeset/store-open-command.md new file mode 100644 index 00000000000..6765ca106d8 --- /dev/null +++ b/.changeset/store-open-command.md @@ -0,0 +1,6 @@ +--- +'@shopify/cli': minor +'@shopify/store': minor +--- + +Add `shopify store open` to open a store's storefront in your default browser. diff --git a/packages/store/src/cli/commands/store/open.test.ts b/packages/store/src/cli/commands/store/open.test.ts new file mode 100644 index 00000000000..51a4a5c227e --- /dev/null +++ b/packages/store/src/cli/commands/store/open.test.ts @@ -0,0 +1,17 @@ +import StoreOpen from './open.js' +import {openStore} from '../../services/store/open.js' +import {describe, expect, test, vi} from 'vitest' + +vi.mock('../../services/store/open.js') + +describe('store open command', () => { + test('passes the store flag through to the service', async () => { + await StoreOpen.run(['--store', 'shop.myshopify.com']) + + expect(openStore).toHaveBeenCalledWith({store: 'shop.myshopify.com'}) + }) + + test('defines the expected flags', () => { + expect(StoreOpen.flags.store).toBeDefined() + }) +}) diff --git a/packages/store/src/cli/commands/store/open.ts b/packages/store/src/cli/commands/store/open.ts new file mode 100644 index 00000000000..57cdff7345a --- /dev/null +++ b/packages/store/src/cli/commands/store/open.ts @@ -0,0 +1,25 @@ +import {openStore} from '../../services/store/open.js' +import StoreCommand from '../../utilities/store-command.js' +import {storeFlags} from '../../flags.js' +import {globalFlags} from '@shopify/cli-kit/node/cli' + +export default class StoreOpen extends StoreCommand { + static summary = 'Open your Shopify store in the default web browser.' + + static descriptionWithMarkdown = `Opens the storefront for a store you have access to in your default web browser.` + + static description = this.descriptionWithoutMarkdown() + + static examples = ['<%= config.bin %> <%= command.id %> --store shop.myshopify.com'] + + static flags = { + ...globalFlags, + store: storeFlags.store, + } + + public async run(): Promise { + const {flags} = await this.parse(StoreOpen) + + await openStore({store: flags.store}) + } +} diff --git a/packages/store/src/cli/services/store/open.test.ts b/packages/store/src/cli/services/store/open.test.ts new file mode 100644 index 00000000000..11b7c6aec46 --- /dev/null +++ b/packages/store/src/cli/services/store/open.test.ts @@ -0,0 +1,49 @@ +import {openStore} from './open.js' +import {getStoreInfo} from './info/index.js' +import {openURL} from '@shopify/cli-kit/node/system' +import {renderInfo} from '@shopify/cli-kit/node/ui' +import {beforeEach, describe, expect, test, vi} from 'vitest' + +vi.mock('./info/index.js') +vi.mock('@shopify/cli-kit/node/system') +vi.mock('@shopify/cli-kit/node/ui') + +describe('openStore', () => { + beforeEach(() => { + vi.mocked(openURL).mockResolvedValue(true) + }) + + test('opens the canonical storefront URL for a regular store', async () => { + vi.mocked(getStoreInfo).mockResolvedValue({subdomain: 'shop.myshopify.com'}) + + await openStore({store: 'shop.myshopify.com'}) + + expect(getStoreInfo).toHaveBeenCalledWith({store: 'shop.myshopify.com'}) + expect(openURL).toHaveBeenCalledWith('https://shop.myshopify.com') + expect(renderInfo).toHaveBeenCalledWith( + expect.objectContaining({headline: expect.stringContaining('Opening the storefront')}), + ) + }) + + test('prefers the preview-store access URL when present', async () => { + vi.mocked(getStoreInfo).mockResolvedValue({ + subdomain: 'preview.myshopify.com', + accessUrl: 'https://preview.myshopify.com/?token=abc', + }) + + await openStore({store: 'preview.myshopify.com'}) + + expect(openURL).toHaveBeenCalledWith('https://preview.myshopify.com/?token=abc') + }) + + test('prints the URL manually when the browser does not open', async () => { + vi.mocked(getStoreInfo).mockResolvedValue({subdomain: 'shop.myshopify.com'}) + vi.mocked(openURL).mockResolvedValue(false) + + await openStore({store: 'shop.myshopify.com'}) + + expect(renderInfo).toHaveBeenCalledWith( + expect.objectContaining({headline: expect.stringContaining("didn't open automatically")}), + ) + }) +}) diff --git a/packages/store/src/cli/services/store/open.ts b/packages/store/src/cli/services/store/open.ts new file mode 100644 index 00000000000..10a3f3c128f --- /dev/null +++ b/packages/store/src/cli/services/store/open.ts @@ -0,0 +1,48 @@ +import {getStoreInfo} from './info/index.js' +import {openURL as defaultOpenURL} from '@shopify/cli-kit/node/system' +import {renderInfo} from '@shopify/cli-kit/node/ui' +import {outputContent, outputToken} from '@shopify/cli-kit/node/output' +import type {StoreInfoResult} from './info/types.js' + +interface OpenStoreOptions { + store: string +} + +interface OpenStoreDependencies { + getStoreInfo: typeof getStoreInfo + openURL: typeof defaultOpenURL +} + +const defaultDependencies: OpenStoreDependencies = { + getStoreInfo, + openURL: defaultOpenURL, +} + +/** + * Opens a store's storefront in the default browser. + */ +export async function openStore( + options: OpenStoreOptions, + dependencies: Partial = {}, +): Promise { + const {getStoreInfo: getInfo, openURL} = {...defaultDependencies, ...dependencies} + + const info = await getInfo({store: options.store}) + const url = storefrontUrl(info) + + const opened = await openURL(url) + if (opened) { + renderInfo({headline: `Opening the storefront for ${options.store} in your browser.`}) + return + } + + renderInfo({ + headline: `Browser didn't open automatically. Open the storefront manually:`, + body: [outputContent`${outputToken.link(url, url)}`.value], + }) +} + +function storefrontUrl(info: StoreInfoResult): string { + // Preview stores surface a tokenized access URL; everyone else resolves to the canonical domain. + return info.accessUrl ?? `https://${info.subdomain}` +} diff --git a/packages/store/src/index.ts b/packages/store/src/index.ts index 1b989133e3a..3d91e134b84 100644 --- a/packages/store/src/index.ts +++ b/packages/store/src/index.ts @@ -8,6 +8,7 @@ import StoreCreatePreview from './cli/commands/store/create/preview.js' import StoreExecute from './cli/commands/store/execute.js' import StoreInfo from './cli/commands/store/info.js' import StoreList from './cli/commands/store/list.js' +import StoreOpen from './cli/commands/store/open.js' export {loadAdminSessionFromStoreAuth} from './cli/services/store/auth/admin-session.js' @@ -22,6 +23,7 @@ const COMMANDS = { 'store:execute': StoreExecute, 'store:info': StoreInfo, 'store:list': StoreList, + 'store:open': StoreOpen, } export default COMMANDS From e945c9072dfe529703d095299608c70dd3502cde Mon Sep 17 00:00:00 2001 From: Ariel Caplan Date: Mon, 29 Jun 2026 19:25:59 +0300 Subject: [PATCH 2/3] Refresh manifests and README for store open command Assisted-By: devx/14c34c82-e316-418f-9255-474db36c1b87 --- packages/cli/README.md | 23 +++++++++++++++ packages/cli/oclif.manifest.json | 49 ++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/packages/cli/README.md b/packages/cli/README.md index 81a27a55be0..7ff9a437fa9 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -84,6 +84,7 @@ * [`shopify store bulk status`](#shopify-store-bulk-status) * [`shopify store execute`](#shopify-store-execute) * [`shopify store info`](#shopify-store-info) +* [`shopify store open`](#shopify-store-open) * [`shopify theme check`](#shopify-theme-check) * [`shopify theme console`](#shopify-theme-console) * [`shopify theme delete`](#shopify-theme-delete) @@ -2388,6 +2389,28 @@ EXAMPLES $ shopify store info --store shop.myshopify.com --json ``` +## `shopify store open` + +Open your Shopify store in the default web browser. + +``` +USAGE + $ shopify store open -s [--no-color] [--verbose] + +FLAGS + -s, --store= (required) [env: SHOPIFY_FLAG_STORE] The myshopify.com domain of the store. + --no-color [env: SHOPIFY_FLAG_NO_COLOR] Disable color output. + --verbose [env: SHOPIFY_FLAG_VERBOSE] Increase the verbosity of the output. + +DESCRIPTION + Open your Shopify store in the default web browser. + + Opens the storefront for a store you have access to in your default web browser. + +EXAMPLES + $ shopify store open --store shop.myshopify.com +``` + ## `shopify theme check` Validate the theme. diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index ad60fffaed5..97ae4c6bb43 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -6577,6 +6577,55 @@ "strict": true, "summary": "List stores in a Shopify organization." }, + "store:open": { + "aliases": [ + ], + "args": { + }, + "customPluginName": "@shopify/store", + "description": "Opens the storefront for a store you have access to in your default web browser.", + "descriptionWithMarkdown": "Opens the storefront for a store you have access to in your default web browser.", + "examples": [ + "<%= config.bin %> <%= command.id %> --store shop.myshopify.com" + ], + "flags": { + "no-color": { + "allowNo": false, + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", + "type": "boolean" + }, + "store": { + "char": "s", + "description": "The myshopify.com domain of the store.", + "env": "SHOPIFY_FLAG_STORE", + "hasDynamicHelp": false, + "multiple": false, + "name": "store", + "required": true, + "type": "option" + }, + "verbose": { + "allowNo": false, + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", + "type": "boolean" + } + }, + "hasDynamicHelp": false, + "hiddenAliases": [ + ], + "id": "store:open", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true, + "summary": "Open your Shopify store in the default web browser." + }, "theme:check": { "aliases": [ ], From 5ea31e3ef85fc66d999b388ce7d11cda2e47b238 Mon Sep 17 00:00:00 2001 From: Ariel Caplan Date: Mon, 29 Jun 2026 23:13:13 +0300 Subject: [PATCH 3/3] Hide command --- packages/cli/README.md | 23 ------------------- packages/cli/oclif.manifest.json | 1 + packages/store/src/cli/commands/store/open.ts | 2 ++ 3 files changed, 3 insertions(+), 23 deletions(-) diff --git a/packages/cli/README.md b/packages/cli/README.md index 7ff9a437fa9..81a27a55be0 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -84,7 +84,6 @@ * [`shopify store bulk status`](#shopify-store-bulk-status) * [`shopify store execute`](#shopify-store-execute) * [`shopify store info`](#shopify-store-info) -* [`shopify store open`](#shopify-store-open) * [`shopify theme check`](#shopify-theme-check) * [`shopify theme console`](#shopify-theme-console) * [`shopify theme delete`](#shopify-theme-delete) @@ -2389,28 +2388,6 @@ EXAMPLES $ shopify store info --store shop.myshopify.com --json ``` -## `shopify store open` - -Open your Shopify store in the default web browser. - -``` -USAGE - $ shopify store open -s [--no-color] [--verbose] - -FLAGS - -s, --store= (required) [env: SHOPIFY_FLAG_STORE] The myshopify.com domain of the store. - --no-color [env: SHOPIFY_FLAG_NO_COLOR] Disable color output. - --verbose [env: SHOPIFY_FLAG_VERBOSE] Increase the verbosity of the output. - -DESCRIPTION - Open your Shopify store in the default web browser. - - Opens the storefront for a store you have access to in your default web browser. - -EXAMPLES - $ shopify store open --store shop.myshopify.com -``` - ## `shopify theme check` Validate the theme. diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index 97ae4c6bb43..803785a5e11 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -6617,6 +6617,7 @@ } }, "hasDynamicHelp": false, + "hidden": true, "hiddenAliases": [ ], "id": "store:open", diff --git a/packages/store/src/cli/commands/store/open.ts b/packages/store/src/cli/commands/store/open.ts index 57cdff7345a..f4c25417bcc 100644 --- a/packages/store/src/cli/commands/store/open.ts +++ b/packages/store/src/cli/commands/store/open.ts @@ -4,6 +4,8 @@ import {storeFlags} from '../../flags.js' import {globalFlags} from '@shopify/cli-kit/node/cli' export default class StoreOpen extends StoreCommand { + static hidden = true + static summary = 'Open your Shopify store in the default web browser.' static descriptionWithMarkdown = `Opens the storefront for a store you have access to in your default web browser.`