From ab54f9c1870b40528941355d566cc42cc6ff56a4 Mon Sep 17 00:00:00 2001 From: Alfonso Noriega Date: Thu, 11 Jun 2026 11:20:25 +0200 Subject: [PATCH] Use store auth sessions for theme commands --- .changeset/theme-preview-store-auth.md | 6 ++ .../services/store/auth/admin-session.test.ts | 47 +++++++++ .../cli/services/store/auth/admin-session.ts | 23 +++++ .../store/execute/admin-context.test.ts | 48 ++++----- .../services/store/execute/admin-context.ts | 12 +-- packages/store/src/index.ts | 2 + packages/theme/package.json | 1 + .../src/cli/utilities/theme-command.test.ts | 98 +++++++++++++++++++ .../theme/src/cli/utilities/theme-command.ts | 61 ++++++++++-- packages/theme/tsconfig.json | 3 +- pnpm-lock.yaml | 3 + tsconfig.json | 1 + 12 files changed, 253 insertions(+), 52 deletions(-) create mode 100644 .changeset/theme-preview-store-auth.md create mode 100644 packages/store/src/cli/services/store/auth/admin-session.test.ts create mode 100644 packages/store/src/cli/services/store/auth/admin-session.ts diff --git a/.changeset/theme-preview-store-auth.md b/.changeset/theme-preview-store-auth.md new file mode 100644 index 00000000000..62f6dea3319 --- /dev/null +++ b/.changeset/theme-preview-store-auth.md @@ -0,0 +1,6 @@ +--- +'@shopify/theme': patch +'@shopify/store': patch +--- + +Use matching store-auth cache sessions for theme pull and push when no theme password is provided. diff --git a/packages/store/src/cli/services/store/auth/admin-session.test.ts b/packages/store/src/cli/services/store/auth/admin-session.test.ts new file mode 100644 index 00000000000..35773362d74 --- /dev/null +++ b/packages/store/src/cli/services/store/auth/admin-session.test.ts @@ -0,0 +1,47 @@ +import {loadAdminSessionFromStoreAuth} from './admin-session.js' +import {loadStoredStoreSession} from './session-lifecycle.js' +import {recordStoreFqdnMetadata} from '../attribution.js' +import {setLastSeenUserId} from '@shopify/cli-kit/node/session' +import {describe, expect, test, vi} from 'vitest' + +vi.mock('./session-lifecycle.js') +vi.mock('../attribution.js') +vi.mock('@shopify/cli-kit/node/session') + +describe('loadAdminSessionFromStoreAuth', () => { + test('returns an Admin session from a matching stored store auth session', async () => { + const storedSession = { + store: 'preview.myshopify.com', + clientId: 'client-id', + userId: 'preview:123', + accessToken: 'shpat_token', + scopes: [], + acquiredAt: '2026-06-08T12:00:00.000Z', + kind: 'preview' as const, + preview: { + shopId: '123', + name: 'Preview Store', + createdAt: '2026-06-08T12:00:00.000Z', + }, + } + vi.mocked(loadStoredStoreSession).mockResolvedValue(storedSession) + + const got = await loadAdminSessionFromStoreAuth('https://preview.myshopify.com/admin') + + expect(loadStoredStoreSession).toHaveBeenCalledWith('preview.myshopify.com') + expect(recordStoreFqdnMetadata).toHaveBeenCalledWith('preview.myshopify.com', true) + expect(setLastSeenUserId).toHaveBeenCalledWith('preview:123') + expect(got).toEqual({ + adminSession: {token: 'shpat_token', storeFqdn: 'preview.myshopify.com'}, + session: storedSession, + }) + }) + + test('propagates store auth cache errors', async () => { + vi.mocked(loadStoredStoreSession).mockRejectedValue(new Error('missing session')) + + await expect(loadAdminSessionFromStoreAuth('preview.myshopify.com')).rejects.toThrow('missing session') + expect(recordStoreFqdnMetadata).not.toHaveBeenCalled() + expect(setLastSeenUserId).not.toHaveBeenCalled() + }) +}) diff --git a/packages/store/src/cli/services/store/auth/admin-session.ts b/packages/store/src/cli/services/store/auth/admin-session.ts new file mode 100644 index 00000000000..f97a9c97024 --- /dev/null +++ b/packages/store/src/cli/services/store/auth/admin-session.ts @@ -0,0 +1,23 @@ +import {loadStoredStoreSession} from './session-lifecycle.js' +import {recordStoreFqdnMetadata} from '../attribution.js' +import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn' +import {setLastSeenUserId} from '@shopify/cli-kit/node/session' +import type {AdminSession} from '@shopify/cli-kit/node/session' +import type {StoredStoreAppSession} from './session-store.js' + +export async function loadAdminSessionFromStoreAuth(store: string): Promise<{ + adminSession: AdminSession + session: StoredStoreAppSession +}> { + const session = await loadStoredStoreSession(normalizeStoreFqdn(store)) + await recordStoreFqdnMetadata(session.store, true) + setLastSeenUserId(session.userId) + + return { + adminSession: { + token: session.accessToken, + storeFqdn: session.store, + }, + session, + } +} diff --git a/packages/store/src/cli/services/store/execute/admin-context.test.ts b/packages/store/src/cli/services/store/execute/admin-context.test.ts index cb64d86180f..0d6d3d42597 100644 --- a/packages/store/src/cli/services/store/execute/admin-context.test.ts +++ b/packages/store/src/cli/services/store/execute/admin-context.test.ts @@ -1,15 +1,11 @@ import {prepareAdminStoreGraphQLContext} from './admin-context.js' import {fetchPublicApiVersions} from './admin-transport.js' -import {loadStoredStoreSession} from '../auth/session-lifecycle.js' +import {loadAdminSessionFromStoreAuth} from '../auth/admin-session.js' import {STORE_AUTH_APP_CLIENT_ID} from '../auth/config.js' -import {recordStoreFqdnMetadata} from '../attribution.js' import {AbortError} from '@shopify/cli-kit/node/error' -import {setLastSeenUserId} from '@shopify/cli-kit/node/session' import {beforeEach, describe, expect, test, vi} from 'vitest' -vi.mock('../auth/session-lifecycle.js', () => ({loadStoredStoreSession: vi.fn()})) -vi.mock('../attribution.js') -vi.mock('@shopify/cli-kit/node/session') +vi.mock('../auth/admin-session.js') vi.mock('./admin-transport.js', () => ({ fetchPublicApiVersions: vi.fn(), // runAdminStoreGraphQLOperation isn't exercised here, but we re-export it for type completeness. @@ -29,7 +25,10 @@ describe('prepareAdminStoreGraphQLContext', () => { } beforeEach(() => { - vi.mocked(loadStoredStoreSession).mockResolvedValue(storedSession) + vi.mocked(loadAdminSessionFromStoreAuth).mockResolvedValue({ + adminSession: {token: storedSession.accessToken, storeFqdn: storedSession.store}, + session: storedSession, + }) vi.mocked(fetchPublicApiVersions).mockResolvedValue([ {handle: '2025-10', supported: true}, {handle: '2025-07', supported: true}, @@ -40,10 +39,7 @@ describe('prepareAdminStoreGraphQLContext', () => { test('returns the stored admin session, version, and full auth session', async () => { const result = await prepareAdminStoreGraphQLContext({store}) - expect(loadStoredStoreSession).toHaveBeenCalledWith(store) - expect(recordStoreFqdnMetadata).toHaveBeenCalledOnce() - expect(recordStoreFqdnMetadata).toHaveBeenCalledWith(store, true) - expect(setLastSeenUserId).toHaveBeenCalledWith('42') + expect(loadAdminSessionFromStoreAuth).toHaveBeenCalledWith(store) expect(fetchPublicApiVersions).toHaveBeenCalledWith({ adminSession: {token: 'token', storeFqdn: store}, session: storedSession, @@ -62,7 +58,10 @@ describe('prepareAdminStoreGraphQLContext', () => { refreshToken: 'fresh-refresh-token', expiresAt: '2026-04-03T00:00:00.000Z', } - vi.mocked(loadStoredStoreSession).mockResolvedValue(refreshedSession) + vi.mocked(loadAdminSessionFromStoreAuth).mockResolvedValue({ + adminSession: {token: refreshedSession.accessToken, storeFqdn: refreshedSession.store}, + session: refreshedSession, + }) const result = await prepareAdminStoreGraphQLContext({store}) @@ -83,9 +82,7 @@ describe('prepareAdminStoreGraphQLContext', () => { test('allows unstable without consulting the transport, but still loads the stored session', async () => { const result = await prepareAdminStoreGraphQLContext({store, userSpecifiedVersion: 'unstable'}) - expect(loadStoredStoreSession).toHaveBeenCalledWith(store) - expect(recordStoreFqdnMetadata).toHaveBeenCalledOnce() - expect(recordStoreFqdnMetadata).toHaveBeenCalledWith(store, true) + expect(loadAdminSessionFromStoreAuth).toHaveBeenCalledWith(store) expect(result).toEqual({ adminSession: {token: 'token', storeFqdn: store}, version: 'unstable', @@ -98,32 +95,21 @@ describe('prepareAdminStoreGraphQLContext', () => { await expect(prepareAdminStoreGraphQLContext({store, userSpecifiedVersion: '1999-01'})).rejects.toThrow( 'Invalid API version', ) - expect(recordStoreFqdnMetadata).toHaveBeenCalledOnce() - expect(recordStoreFqdnMetadata).toHaveBeenCalledWith(store, true) + expect(loadAdminSessionFromStoreAuth).toHaveBeenCalledWith(store) }) - test('does not record validated store metadata when loading stored auth fails', async () => { - vi.mocked(loadStoredStoreSession).mockRejectedValue(new AbortError('missing stored auth')) + test('does not resolve API versions when loading stored auth fails', async () => { + vi.mocked(loadAdminSessionFromStoreAuth).mockRejectedValue(new AbortError('missing stored auth')) await expect(prepareAdminStoreGraphQLContext({store})).rejects.toThrow('missing stored auth') - expect(recordStoreFqdnMetadata).not.toHaveBeenCalled() + expect(loadAdminSessionFromStoreAuth).toHaveBeenCalledWith(store) expect(fetchPublicApiVersions).not.toHaveBeenCalled() }) - test('re-records fqdn metadata when the stored session store differs from the requested store', async () => { - vi.mocked(loadStoredStoreSession).mockResolvedValue({...storedSession, store: 'permanent-shop.myshopify.com'}) - - await prepareAdminStoreGraphQLContext({store}) - - expect(recordStoreFqdnMetadata).toHaveBeenCalledOnce() - expect(recordStoreFqdnMetadata).toHaveBeenCalledWith('permanent-shop.myshopify.com', true) - }) - test('rethrows whatever the transport raises (errors are owned by the transport)', async () => { vi.mocked(fetchPublicApiVersions).mockRejectedValue(new AbortError('upstream exploded')) await expect(prepareAdminStoreGraphQLContext({store})).rejects.toThrow('upstream exploded') - expect(recordStoreFqdnMetadata).toHaveBeenCalledOnce() - expect(recordStoreFqdnMetadata).toHaveBeenCalledWith(store, true) + expect(loadAdminSessionFromStoreAuth).toHaveBeenCalledWith(store) }) }) diff --git a/packages/store/src/cli/services/store/execute/admin-context.ts b/packages/store/src/cli/services/store/execute/admin-context.ts index 0eddff703fb..421126513f0 100644 --- a/packages/store/src/cli/services/store/execute/admin-context.ts +++ b/packages/store/src/cli/services/store/execute/admin-context.ts @@ -1,8 +1,6 @@ import {fetchPublicApiVersions} from './admin-transport.js' -import {loadStoredStoreSession} from '../auth/session-lifecycle.js' -import {recordStoreFqdnMetadata} from '../attribution.js' +import {loadAdminSessionFromStoreAuth} from '../auth/admin-session.js' import {AbortError} from '@shopify/cli-kit/node/error' -import {setLastSeenUserId} from '@shopify/cli-kit/node/session' import type {AdminSession} from '@shopify/cli-kit/node/session' import type {StoredStoreAppSession} from '../auth/session-store.js' @@ -38,13 +36,7 @@ export async function prepareAdminStoreGraphQLContext(input: { store: string userSpecifiedVersion?: string }): Promise { - const session = await loadStoredStoreSession(input.store) - await recordStoreFqdnMetadata(session.store, true) - setLastSeenUserId(session.userId) - const adminSession = { - token: session.accessToken, - storeFqdn: session.store, - } + const {adminSession, session} = await loadAdminSessionFromStoreAuth(input.store) const version = await resolveApiVersion({session, adminSession, userSpecifiedVersion: input.userSpecifiedVersion}) return {adminSession, version, session} diff --git a/packages/store/src/index.ts b/packages/store/src/index.ts index 7bc28796e37..71c3bc39a41 100644 --- a/packages/store/src/index.ts +++ b/packages/store/src/index.ts @@ -3,6 +3,8 @@ 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' +export {loadAdminSessionFromStoreAuth} from './cli/services/store/auth/admin-session.js' + const COMMANDS = { 'store:auth': StoreAuth, 'store:create:preview': StoreCreatePreview, diff --git a/packages/theme/package.json b/packages/theme/package.json index c7ac1502cd9..b35be97b86d 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -42,6 +42,7 @@ "dependencies": { "@oclif/core": "4.11.4", "@shopify/cli-kit": "4.1.0", + "@shopify/store": "4.1.0", "@shopify/theme-check-node": "3.26.1", "@shopify/theme-language-server-node": "2.21.3", "chokidar": "3.6.0", diff --git a/packages/theme/src/cli/utilities/theme-command.test.ts b/packages/theme/src/cli/utilities/theme-command.test.ts index 22f6a3698a1..aae4dd1ac88 100644 --- a/packages/theme/src/cli/utilities/theme-command.test.ts +++ b/packages/theme/src/cli/utilities/theme-command.test.ts @@ -3,6 +3,7 @@ import {ensureThemeStore} from './theme-store.js' import {describe, vi, expect, test, beforeEach} from 'vitest' import {Config, Flags} from '@oclif/core' import {AdminSession, ensureAuthenticatedThemes} from '@shopify/cli-kit/node/session' +import {loadAdminSessionFromStoreAuth} from '@shopify/store' import {loadEnvironment} from '@shopify/cli-kit/node/environments' import {fileExistsSync} from '@shopify/cli-kit/node/fs' import {AbortError} from '@shopify/cli-kit/node/error' @@ -14,6 +15,7 @@ import {hashString} from '@shopify/cli-kit/node/crypto' import type {Writable} from 'stream' vi.mock('@shopify/cli-kit/node/session') +vi.mock('@shopify/store', () => ({loadAdminSessionFromStoreAuth: vi.fn()})) vi.mock('@shopify/cli-kit/node/environments') vi.mock('@shopify/cli-kit/node/ui') vi.mock('@shopify/cli-kit/node/metadata', () => ({ @@ -180,6 +182,9 @@ describe('ThemeCommand', () => { } vi.mocked(ensureThemeStore).mockReturnValue('test-store.myshopify.com') vi.mocked(ensureAuthenticatedThemes).mockResolvedValue(mockSession) + vi.mocked(loadAdminSessionFromStoreAuth).mockRejectedValue( + new AbortError('No stored app authentication found for test-store.myshopify.com.'), + ) vi.mocked(fileExistsSync).mockReturnValue(true) }) @@ -244,6 +249,45 @@ describe('ThemeCommand', () => { expect(sensitiveMetadata).toContainEqual({store_fqdn: mockSession.storeFqdn}) }) + test('uses a matching store auth cache session when no password is provided', async () => { + const storeAuthSession = {token: 'shpat_preview_token', storeFqdn: 'test-store.myshopify.com'} + vi.mocked(loadAdminSessionFromStoreAuth).mockResolvedValue({adminSession: storeAuthSession, session: {} as any}) + + await CommandConfig.load() + const command = new TestThemeCommand([], CommandConfig) + + await command.run() + + expect(loadAdminSessionFromStoreAuth).toHaveBeenCalledWith('test-store.myshopify.com') + expect(ensureAuthenticatedThemes).not.toHaveBeenCalled() + expect(command.commandCalls[0]).toMatchObject({session: storeAuthSession}) + }) + + test('uses the password flag instead of a matching store auth cache session', async () => { + const storeAuthSession = {token: 'shpat_preview_token', storeFqdn: 'test-store.myshopify.com'} + vi.mocked(loadAdminSessionFromStoreAuth).mockResolvedValue({adminSession: storeAuthSession, session: {} as any}) + + await CommandConfig.load() + const command = new TestThemeCommand(['--password', 'shptka_password'], CommandConfig) + + await command.run() + + expect(loadAdminSessionFromStoreAuth).not.toHaveBeenCalled() + expect(ensureAuthenticatedThemes).toHaveBeenCalledWith('test-store.myshopify.com', 'shptka_password') + expect(command.commandCalls[0]).toMatchObject({session: mockSession}) + }) + + test('falls back to theme authentication when no matching store auth cache session exists', async () => { + await CommandConfig.load() + const command = new TestThemeCommand([], CommandConfig) + + await command.run() + + expect(loadAdminSessionFromStoreAuth).toHaveBeenCalledWith('test-store.myshopify.com') + expect(ensureAuthenticatedThemes).toHaveBeenCalledWith('test-store.myshopify.com', undefined) + expect(command.commandCalls[0]).toMatchObject({session: mockSession}) + }) + test('single environment provided but not found in TOML - throws AbortError', async () => { // Given vi.mocked(loadEnvironment).mockResolvedValue(undefined) @@ -839,6 +883,60 @@ describe('ThemeCommand', () => { expect(liveEnvFlags?.['no-color']).toEqual(true) }) + test('multiple environment commands accept missing password when a store auth cache session exists', async () => { + const storeAuthSession = {token: 'shpat_preview_token', storeFqdn: 'store1.myshopify.com'} + vi.mocked(loadEnvironment) + .mockResolvedValueOnce({store: 'store1.myshopify.com', path: '/home/path/to/theme1'}) + .mockResolvedValueOnce({store: 'store2.myshopify.com', password: 'password2', path: '/home/path/to/theme2'}) + vi.mocked(loadAdminSessionFromStoreAuth).mockResolvedValue({adminSession: storeAuthSession, session: {} as any}) + vi.mocked(renderConfirmationPrompt).mockResolvedValue(true) + vi.mocked(renderConcurrent).mockImplementation(async ({processes}) => { + for (const process of processes) { + // eslint-disable-next-line no-await-in-loop + await process.action({} as Writable, {} as Writable, {} as any) + } + }) + vi.mocked(ensureThemeStore).mockImplementation((options: any) => options.store) + + await CommandConfig.load() + const command = new TestThemeCommandWithPathFlag( + ['--environment', 'preview', '--environment', 'another-preview'], + CommandConfig, + ) + + await command.run() + + expect(renderWarning).not.toHaveBeenCalled() + expect(loadAdminSessionFromStoreAuth).toHaveBeenCalledWith('store1.myshopify.com') + expect(ensureAuthenticatedThemes).toHaveBeenCalledWith('store2.myshopify.com', 'password2') + expect(command.commandCalls).toEqual( + expect.arrayContaining([expect.objectContaining({session: storeAuthSession})]), + ) + }) + + test('multiple environment commands still require password when no store auth cache session exists', async () => { + vi.mocked(loadEnvironment) + .mockResolvedValueOnce({store: 'store1.myshopify.com', path: '/home/path/to/theme1'}) + .mockResolvedValueOnce({store: 'store2.myshopify.com', password: 'password2', path: '/home/path/to/theme2'}) + vi.mocked(renderConcurrent).mockResolvedValue(undefined) + + await CommandConfig.load() + const command = new TestThemeCommandWithPathFlag( + ['--environment', 'preview', '--environment', 'another-preview'], + CommandConfig, + ) + + await command.run() + + expect(renderWarning).toHaveBeenCalledWith( + expect.objectContaining({ + body: ['Missing required flags in environment configuration for preview:', {list: {items: ['password']}}], + }), + ) + expect(renderConcurrent).not.toHaveBeenCalled() + expect(ensureAuthenticatedThemes).not.toHaveBeenCalled() + }) + test('commands will only create a session object if the password flag is supported', async () => { // Given vi.mocked(loadEnvironment) diff --git a/packages/theme/src/cli/utilities/theme-command.ts b/packages/theme/src/cli/utilities/theme-command.ts index e80f625972e..992ab8ef688 100644 --- a/packages/theme/src/cli/utilities/theme-command.ts +++ b/packages/theme/src/cli/utilities/theme-command.ts @@ -6,6 +6,7 @@ import {hashString} from '@shopify/cli-kit/node/crypto' import {Input} from '@oclif/core/interfaces' import Command, {ArgOutput, FlagOutput, noDefaultsOptions} from '@shopify/cli-kit/node/base-command' import {AdminSession, ensureAuthenticatedThemes} from '@shopify/cli-kit/node/session' +import {loadAdminSessionFromStoreAuth} from '@shopify/store' import {loadEnvironment} from '@shopify/cli-kit/node/environments' import { renderWarning, @@ -29,6 +30,7 @@ interface ValidEnvironment { environment: EnvironmentName flags: FlagValues requiresAuth: boolean + storeAuthSession?: AdminSession } type EnvironmentName = string /** @@ -184,14 +186,23 @@ export default abstract class ThemeCommand extends Command { const valid: ValidEnvironment[] = [] const invalid: {environment: EnvironmentName; reason: string}[] = [] - for (const [environmentName, {flags, validationFlags}] of environmentMap) { - const validationResult = this.validConfig(validationFlags, requiredFlags, environmentName) + const entriesWithStoreAuthSessions = await Promise.all( + Array.from(environmentMap.entries()).map(async ([environmentName, {flags, validationFlags}]) => ({ + environmentName, + flags, + validationFlags, + storeAuthSession: await this.storeAuthSessionForTheme(validationFlags), + })), + ) + + for (const {environmentName, flags, validationFlags, storeAuthSession} of entriesWithStoreAuthSessions) { + const validationResult = this.validConfig(validationFlags, requiredFlags, environmentName, storeAuthSession) if (validationResult !== true) { const missingFlagsText = validationResult.join(', ') invalid.push({environment: environmentName, reason: `Missing flags: ${missingFlagsText}`}) continue } - valid.push({environment: environmentName, flags, requiresAuth}) + valid.push({environment: environmentName, flags, requiresAuth, storeAuthSession}) } return {valid, invalid} @@ -267,13 +278,13 @@ export default abstract class ThemeCommand extends Command { for (const runGroup of runGroups) { // eslint-disable-next-line no-await-in-loop await renderConcurrent({ - processes: runGroup.map(({environment, flags, requiresAuth}) => ({ + processes: runGroup.map(({environment, flags, requiresAuth, storeAuthSession}) => ({ prefix: environment, action: async (stdout: Writable, stderr: Writable, _signal) => { try { const store = flags.store as string await useThemeStoreContext(store, async () => { - const session = requiresAuth ? await this.createSession(flags) : undefined + const session = requiresAuth ? await this.createSession(flags, storeAuthSession) : undefined const commandName = this.constructor.name.toLowerCase() recordEvent(`theme-command:${commandName}:multi-env:authenticated`) @@ -323,14 +334,34 @@ export default abstract class ThemeCommand extends Command { * @param flags - The environment flags containing store and password * @returns The unauthenticated session object */ - private async createSession(flags: FlagValues) { - const store = flags.store as string - const password = flags.password as string - const session = await ensureAuthenticatedThemes(ensureThemeStore({store}), password) + private async createSession(flags: FlagValues, storeAuthSession?: AdminSession) { + const store = ensureThemeStore({store: flags.store as string | undefined}) + const password = flags.password as string | undefined + const session = password + ? await ensureAuthenticatedThemes(store, password) + : (storeAuthSession ?? + (await this.storeAuthSessionForTheme({store})) ?? + (await ensureAuthenticatedThemes(store, password))) return session } + private async storeAuthSessionForTheme(flags: FlagValues): Promise { + const store = typeof flags.store === 'string' ? flags.store : undefined + const password = flags.password + if (!store || password) return undefined + + try { + const {adminSession} = await loadAdminSessionFromStoreAuth(store) + return adminSession + } catch (error) { + if (error instanceof AbortError && error.message.startsWith('No stored app authentication found for ')) { + return undefined + } + throw error + } + } + /** * Ensure that all required flags are present * @param environmentFlags - The environment flags @@ -342,9 +373,14 @@ export default abstract class ThemeCommand extends Command { environmentFlags: FlagValues, requiredFlags: Exclude, environmentName: string, + storeAuthSession?: AdminSession, ): string[] | true { const missingFlags = requiredFlags - .filter((flag) => (Array.isArray(flag) ? !flag.some((flag) => environmentFlags[flag]) : !environmentFlags[flag])) + .filter((flag) => + Array.isArray(flag) + ? !flag.some((flag) => this.hasRequiredFlag(environmentFlags, flag, storeAuthSession)) + : !this.hasRequiredFlag(environmentFlags, flag, storeAuthSession), + ) .map((flag) => (Array.isArray(flag) ? flag.join(' or ') : flag)) if (missingFlags.length > 0) { @@ -360,6 +396,11 @@ export default abstract class ThemeCommand extends Command { return true } + private hasRequiredFlag(environmentFlags: FlagValues, flag: string, storeAuthSession?: AdminSession): boolean { + if (flag === 'password' && storeAuthSession) return true + return Boolean(environmentFlags[flag]) + } + /** * Error if the --path flag is provided via CLI when running a multi environment command * Commands that act on local files require each environment to specify its own path in the shopify.theme.toml diff --git a/packages/theme/tsconfig.json b/packages/theme/tsconfig.json index ea7490fa22f..912e09f599a 100644 --- a/packages/theme/tsconfig.json +++ b/packages/theme/tsconfig.json @@ -8,6 +8,7 @@ "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" }, "references": [ - {"path": "../cli-kit"} + {"path": "../cli-kit"}, + {"path": "../store"} ] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 21f96ef949f..efaf7e321f0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -667,6 +667,9 @@ importers: '@shopify/cli-kit': specifier: 4.1.0 version: link:../cli-kit + '@shopify/store': + specifier: 4.1.0 + version: link:../store '@shopify/theme-check-node': specifier: 3.26.1 version: 3.26.1 diff --git a/tsconfig.json b/tsconfig.json index d9d10a325e7..3a4094dc89e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,6 +5,7 @@ {"path": "./packages/cli"}, {"path": "./packages/app"}, {"path": "./packages/theme"}, + {"path": "./packages/store"}, {"path": "./packages/cli-kit"}, {"path": "./packages/create-app"}, {"path": "./packages/ui-extensions-server-kit"},