diff --git a/packages/cli-kit/src/private/node/session.test.ts b/packages/cli-kit/src/private/node/session.test.ts index 8d3593fd1be..c089ee8e14e 100644 --- a/packages/cli-kit/src/private/node/session.test.ts +++ b/packages/cli-kit/src/private/node/session.test.ts @@ -313,6 +313,46 @@ describe('when existing session is valid', () => { expect(fetchSessions).toHaveBeenCalledOnce() }) + test('returns identity token when identity auth is requested', async () => { + // Given + vi.mocked(validateSession).mockResolvedValueOnce('ok') + vi.mocked(fetchSessions).mockResolvedValue(validSessions) + const identityScope = 'https://api.shopify.com/auth/example.scope' + + // When + const got = await ensureAuthenticated({ + identityApi: {scopes: [identityScope]}, + }) + + // Then + expect(got).toEqual({identity: 'access_token', userId}) + expect(validateSession).toHaveBeenCalledWith( + [identityScope], + {identityApi: {scopes: [identityScope]}}, + validSessions[fqdn]![userId], + ) + }) + + test('does not exchange application tokens for identity-only auth', async () => { + // Given + vi.mocked(validateSession).mockResolvedValueOnce('needs_full_auth') + vi.mocked(fetchSessions).mockResolvedValue(undefined) + const identityScope = 'https://api.shopify.com/auth/example.scope' + + // When + const got = await ensureAuthenticated({ + identityApi: {scopes: [identityScope]}, + }) + + // Then + expect(exchangeAccessForApplicationTokens).not.toHaveBeenCalled() + expect(businessPlatformRequest).not.toHaveBeenCalled() + expect(got).toEqual({identity: 'access_token', userId}) + + const storedSession = vi.mocked(storeSessions).mock.calls[0]![0] + expect(storedSession[fqdn]![userId]!.applications).toEqual({}) + }) + test('overwrites partners token if provided with a custom CLI token', async () => { // Given vi.mocked(validateSession).mockResolvedValueOnce('ok') @@ -371,6 +411,23 @@ describe('when existing session is expired', () => { expect(fetchSessions).toHaveBeenCalledOnce() }) + test('does not exchange application tokens when refreshing identity-only auth', async () => { + // Given + vi.mocked(validateSession).mockResolvedValueOnce('needs_refresh') + vi.mocked(fetchSessions).mockResolvedValue(validSessions) + const identityScope = 'https://api.shopify.com/auth/example.scope' + + // When + const got = await ensureAuthenticated({ + identityApi: {scopes: [identityScope]}, + }) + + // Then + expect(refreshAccessToken).toHaveBeenCalled() + expect(exchangeAccessForApplicationTokens).not.toHaveBeenCalled() + expect(got).toEqual({identity: 'access_token', userId}) + }) + test('attempts to refresh the token and executes a complete flow if identity returns an invalid grant error', async () => { // Given const tokenResponseError = new InvalidGrantError() diff --git a/packages/cli-kit/src/private/node/session.ts b/packages/cli-kit/src/private/node/session.ts index 3b750d1cdcb..e58192af4c0 100644 --- a/packages/cli-kit/src/private/node/session.ts +++ b/packages/cli-kit/src/private/node/session.ts @@ -9,7 +9,7 @@ import { InvalidGrantError, InvalidRequestError, } from './session/exchange.js' -import {IdentityToken, Session, Sessions} from './session/schema.js' +import {ApplicationToken, IdentityToken, Session, Sessions} from './session/schema.js' import * as sessionStore from './session/store.js' import {pollForDeviceAuthorization, requestDeviceAuthorization} from './session/device-authorization.js' import {isThemeAccessSession} from './api/rest.js' @@ -92,6 +92,12 @@ interface BusinessPlatformAPIOAuthOptions { scopes: BusinessPlatformScope[] } +export type IdentityScope = string +interface IdentityOAuthOptions { + /** List of Identity scopes to request permissions for. */ + scopes: IdentityScope[] +} + /** * It represents the authentication requirements and * is the input necessary to trigger the authentication @@ -103,6 +109,7 @@ export interface OAuthApplications { partnersApi?: PartnersAPIOAuthOptions businessPlatformApi?: BusinessPlatformAPIOAuthOptions appManagementApi?: AppManagementAPIOauthOptions + identityApi?: IdentityOAuthOptions } export interface OAuthSession { @@ -111,6 +118,7 @@ export interface OAuthSession { storefront?: string businessPlatform?: string appManagement?: string + identity?: string userId: string } @@ -294,7 +302,6 @@ The CLI is currently unable to prompt for reauthentication.`, */ async function executeCompleteFlow(applications: OAuthApplications, existingAlias?: string): Promise { const scopes = getFlattenScopes(applications) - const exchangeScopes = getExchangeScopes(applications) const store = applications.adminApi?.storeFqdn if (firstPartyDev()) { outputDebug(outputContent`Authenticating as Shopify Employee...`) @@ -315,9 +322,12 @@ async function executeCompleteFlow(applications: OAuthApplications, existingAlia identityToken = await pollForDeviceAuthorization(deviceAuth.deviceCode, deviceAuth.interval) } - // Exchange identity token for application tokens - outputDebug(outputContent`CLI token received. Exchanging it for application tokens...`) - const result = await exchangeAccessForApplicationTokens(identityToken, exchangeScopes, store) + let result: Record = {} + if (requiresApplicationTokenExchange(applications)) { + // Exchange identity token for application tokens + outputDebug(outputContent`CLI token received. Exchanging it for application tokens...`) + result = await exchangeAccessForApplicationTokens(identityToken, getExchangeScopes(applications), store) + } // Preserve existing alias if available, otherwise try fetching email const businessPlatformToken = result[applicationId('business-platform')]?.accessToken @@ -344,13 +354,13 @@ async function executeCompleteFlow(applications: OAuthApplications, existingAlia async function refreshTokens(session: Session, applications: OAuthApplications): Promise { // Refresh Identity Token const identityToken = await refreshAccessToken(session.identity) - // Exchange new identity token for application tokens - const exchangeScopes = getExchangeScopes(applications) - const applicationTokens = await exchangeAccessForApplicationTokens( - identityToken, - exchangeScopes, - applications.adminApi?.storeFqdn, - ) + const applicationTokens = requiresApplicationTokenExchange(applications) + ? await exchangeAccessForApplicationTokens( + identityToken, + getExchangeScopes(applications), + applications.adminApi?.storeFqdn, + ) + : {} return { identity: {...identityToken, alias: session.identity.alias}, @@ -358,6 +368,16 @@ async function refreshTokens(session: Session, applications: OAuthApplications): } } +function requiresApplicationTokenExchange(apps: OAuthApplications): boolean { + return [ + apps.adminApi, + apps.storefrontRendererApi, + apps.partnersApi, + apps.businessPlatformApi, + apps.appManagementApi, + ].some((app) => app !== undefined) +} + /** * Get the application tokens for a given session. * @@ -399,6 +419,10 @@ async function tokensFor(applications: OAuthApplications, session: Session): Pro tokens.appManagement = session.applications[appId]?.accessToken } + if (applications.identityApi) { + tokens.identity = session.identity.accessToken + } + return tokens } @@ -415,7 +439,8 @@ function getFlattenScopes(apps: OAuthApplications): string[] { const storefront = apps.storefrontRendererApi?.scopes ?? [] const businessPlatform = apps.businessPlatformApi?.scopes ?? [] const appManagement = apps.appManagementApi?.scopes ?? [] - const requestedScopes = [...admin, ...partner, ...storefront, ...businessPlatform, ...appManagement] + const identity = apps.identityApi?.scopes ?? [] + const requestedScopes = [...admin, ...partner, ...storefront, ...businessPlatform, ...appManagement, ...identity] return allDefaultScopes(requestedScopes) } diff --git a/packages/cli-kit/src/public/node/session.test.ts b/packages/cli-kit/src/public/node/session.test.ts index b54da575406..a118956d7f2 100644 --- a/packages/cli-kit/src/public/node/session.test.ts +++ b/packages/cli-kit/src/public/node/session.test.ts @@ -3,6 +3,7 @@ import { ensureAuthenticatedAdminAsApp, ensureAuthenticatedAppManagementAndBusinessPlatform, ensureAuthenticatedBusinessPlatform, + ensureAuthenticatedIdentity, ensureAuthenticatedPartners, ensureAuthenticatedStorefront, ensureAuthenticatedThemes, @@ -43,6 +44,29 @@ describe('store command analytics session helpers', () => { }) }) +describe('ensureAuthenticatedIdentity', () => { + test('returns the identity token when success', async () => { + vi.mocked(ensureAuthenticated).mockResolvedValueOnce({identity: 'identity_token', userId: '1234-5678'}) + + const got = await ensureAuthenticatedIdentity(['https://api.shopify.com/auth/example.scope']) + + expect(got).toEqual({token: 'identity_token', userId: '1234-5678'}) + expect(ensureAuthenticated).toHaveBeenCalledWith( + {identityApi: {scopes: ['https://api.shopify.com/auth/example.scope']}}, + process.env, + {}, + ) + }) + + test('throws error if there is no identity token', async () => { + vi.mocked(ensureAuthenticated).mockResolvedValueOnce({userId: '1234-5678'}) + + const got = ensureAuthenticatedIdentity(['https://api.shopify.com/auth/example.scope']) + + await expect(got).rejects.toThrow(`No identity token`) + }) +}) + describe('ensureAuthenticatedStorefront', () => { test('returns only storefront token if success', async () => { // Given diff --git a/packages/cli-kit/src/public/node/session.ts b/packages/cli-kit/src/public/node/session.ts index 309804abf78..ed077783e2f 100644 --- a/packages/cli-kit/src/public/node/session.ts +++ b/packages/cli-kit/src/public/node/session.ts @@ -14,6 +14,7 @@ import { AppManagementAPIScope, BusinessPlatformScope, EnsureAuthenticatedAdditionalOptions, + IdentityScope, PartnersAPIScope, StorefrontRendererScope, ensureAuthenticated, @@ -101,6 +102,32 @@ export async function ensureAuthenticatedUser( return {userId: tokens.userId} } +/** + * Ensure that we have a valid Identity access token with the requested scopes. + * + * Use this when a first-party Shopify service validates the CLI caller directly + * with Identity rather than with an exchanged application token. + * + * @param scopes - Identity scopes to authenticate with. + * @param env - Optional environment variables to use. + * @param options - Optional extra options to use. + * @returns The Identity access token and user ID. + */ +export async function ensureAuthenticatedIdentity( + scopes: IdentityScope[] = [], + env = process.env, + options: EnsureAuthenticatedAdditionalOptions = {}, +): Promise<{token: string; userId: string}> { + outputDebug(outputContent`Ensuring that the user is authenticated with Identity scopes: +${outputToken.json(scopes)} +`) + const tokens = await ensureAuthenticated({identityApi: {scopes}}, env, options) + if (!tokens.identity) { + throw new BugError('No identity token found after ensuring authenticated') + } + return {token: tokens.identity, userId: tokens.userId} +} + /** * Ensure that we have a valid session to access the Partners API. * If SHOPIFY_CLI_PARTNERS_TOKEN exists, that token will be used to obtain a valid Partners Token diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index b905320bf20..9bb565ce536 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -1,55 +1,58 @@ { "commands": { "app:build": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/app", + "aliases": [], + "args": {}, "description": "This command executes the build script specified in the element's TOML file. You can specify a custom script in the file. To learn about configuration files in Shopify apps, refer to \"App configuration\" (https://shopify.dev/docs/apps/tools/cli/configuration).\n\n If you're building a \"theme app extension\" (https://shopify.dev/docs/apps/online-store/theme-app-extensions), then running the `build` command runs \"Theme Check\" (https://shopify.dev/docs/themes/tools/theme-check) against your extension to ensure that it's valid.", - "descriptionWithMarkdown": "This command executes the build script specified in the element's TOML file. You can specify a custom script in the file. To learn about configuration files in Shopify apps, refer to [App configuration](https://shopify.dev/docs/apps/tools/cli/configuration).\n\n If you're building a [theme app extension](https://shopify.dev/docs/apps/online-store/theme-app-extensions), then running the `build` command runs [Theme Check](https://shopify.dev/docs/themes/tools/theme-check) against your extension to ensure that it's valid.", "flags": { - "client-id": { - "description": "The Client ID of your app.", - "env": "SHOPIFY_FLAG_CLIENT_ID", - "exclusive": [ - "config" - ], - "hasDynamicHelp": false, + "no-color": { + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", + "allowNo": false, + "type": "boolean" + }, + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", "hidden": false, + "name": "verbose", + "allowNo": false, + "type": "boolean" + }, + "path": { + "description": "The path to your app directory.", + "env": "SHOPIFY_FLAG_PATH", + "name": "path", + "noCacheDefault": true, + "hasDynamicHelp": false, "multiple": false, - "name": "client-id", "type": "option" }, "config": { "char": "c", "description": "The name of the app configuration.", "env": "SHOPIFY_FLAG_APP_CONFIG", - "hasDynamicHelp": false, "hidden": false, - "multiple": false, "name": "config", + "hasDynamicHelp": false, + "multiple": false, "type": "option" }, - "no-color": { - "allowNo": false, - "description": "Disable color output.", - "env": "SHOPIFY_FLAG_NO_COLOR", + "client-id": { + "description": "The Client ID of your app.", + "env": "SHOPIFY_FLAG_CLIENT_ID", + "exclusive": [ + "config" + ], "hidden": false, - "name": "no-color", - "type": "boolean" - }, - "path": { - "description": "The path to your app directory.", - "env": "SHOPIFY_FLAG_PATH", + "name": "client-id", "hasDynamicHelp": false, "multiple": false, - "name": "path", - "noCacheDefault": true, "type": "option" }, "reset": { - "allowNo": false, "description": "Reset all your settings.", "env": "SHOPIFY_FLAG_RESET", "exclusive": [ @@ -57,93 +60,82 @@ ], "hidden": false, "name": "reset", + "allowNo": false, "type": "boolean" }, "skip-dependencies-installation": { - "allowNo": false, "description": "Skips the installation of dependencies. Deprecated, use workspaces instead.", "env": "SHOPIFY_FLAG_SKIP_DEPENDENCIES_INSTALLATION", "hidden": false, "name": "skip-dependencies-installation", - "type": "boolean" - }, - "verbose": { "allowNo": false, - "description": "Increase the verbosity of the output.", - "env": "SHOPIFY_FLAG_VERBOSE", - "hidden": false, - "name": "verbose", "type": "boolean" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], + "hiddenAliases": [], "id": "app:build", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", "strict": true, - "summary": "Build the app, including extensions." + "summary": "Build the app, including extensions.", + "descriptionWithMarkdown": "This command executes the build script specified in the element's TOML file. You can specify a custom script in the file. To learn about configuration files in Shopify apps, refer to [App configuration](https://shopify.dev/docs/apps/tools/cli/configuration).\n\n If you're building a [theme app extension](https://shopify.dev/docs/apps/online-store/theme-app-extensions), then running the `build` command runs [Theme Check](https://shopify.dev/docs/themes/tools/theme-check) against your extension to ensure that it's valid.", + "customPluginName": "@shopify/app" }, "app:bulk:cancel": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/app", + "aliases": [], + "args": {}, "description": "Cancels a running bulk operation by ID.", "flags": { - "client-id": { - "description": "The Client ID of your app.", - "env": "SHOPIFY_FLAG_CLIENT_ID", - "exclusive": [ - "config" - ], - "hasDynamicHelp": false, + "no-color": { + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", + "allowNo": false, + "type": "boolean" + }, + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", "hidden": false, + "name": "verbose", + "allowNo": false, + "type": "boolean" + }, + "path": { + "description": "The path to your app directory.", + "env": "SHOPIFY_FLAG_PATH", + "name": "path", + "noCacheDefault": true, + "hasDynamicHelp": false, "multiple": false, - "name": "client-id", "type": "option" }, "config": { "char": "c", "description": "The name of the app configuration.", "env": "SHOPIFY_FLAG_APP_CONFIG", - "hasDynamicHelp": false, "hidden": false, - "multiple": false, "name": "config", - "type": "option" - }, - "id": { - "description": "The bulk operation ID to cancel (numeric ID or full GID).", - "env": "SHOPIFY_FLAG_ID", "hasDynamicHelp": false, "multiple": false, - "name": "id", - "required": true, "type": "option" }, - "no-color": { - "allowNo": false, - "description": "Disable color output.", - "env": "SHOPIFY_FLAG_NO_COLOR", + "client-id": { + "description": "The Client ID of your app.", + "env": "SHOPIFY_FLAG_CLIENT_ID", + "exclusive": [ + "config" + ], "hidden": false, - "name": "no-color", - "type": "boolean" - }, - "path": { - "description": "The path to your app directory.", - "env": "SHOPIFY_FLAG_PATH", + "name": "client-id", "hasDynamicHelp": false, "multiple": false, - "name": "path", - "noCacheDefault": true, "type": "option" }, "reset": { - "allowNo": false, "description": "Reset all your settings.", "env": "SHOPIFY_FLAG_RESET", "exclusive": [ @@ -151,115 +143,91 @@ ], "hidden": false, "name": "reset", + "allowNo": false, "type": "boolean" }, + "id": { + "description": "The bulk operation ID to cancel (numeric ID or full GID).", + "env": "SHOPIFY_FLAG_ID", + "name": "id", + "required": true, + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, "store": { "char": "s", "description": "The store domain. Must be an existing dev store.", "env": "SHOPIFY_FLAG_STORE", + "name": "store", "hasDynamicHelp": false, "multiple": false, - "name": "store", "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": [ - ], + "hiddenAliases": [], "id": "app:bulk:cancel", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", "strict": true, - "summary": "Cancel a bulk operation." + "summary": "Cancel a bulk operation.", + "customPluginName": "@shopify/app" }, - "app:bulk:execute": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/app", - "description": "Executes an Admin API GraphQL query or mutation on the specified store, as a bulk operation. Mutations are only allowed on dev stores.\n\n Bulk operations allow you to process large amounts of data asynchronously. Learn more about \"bulk query operations\" (https://shopify.dev/docs/api/usage/bulk-operations/queries) and \"bulk mutation operations\" (https://shopify.dev/docs/api/usage/bulk-operations/imports).\n\n Use \"`bulk status`\" (https://shopify.dev/docs/api/shopify-cli/app/app-bulk-status) to check the status of your bulk operations.", - "descriptionWithMarkdown": "Executes an Admin API GraphQL query or mutation on the specified store, as a bulk operation. Mutations are only allowed on dev stores.\n\n Bulk operations allow you to process large amounts of data asynchronously. Learn more about [bulk query operations](https://shopify.dev/docs/api/usage/bulk-operations/queries) and [bulk mutation operations](https://shopify.dev/docs/api/usage/bulk-operations/imports).\n\n Use [`bulk status`](https://shopify.dev/docs/api/shopify-cli/app/app-bulk-status) to check the status of your bulk operations.", + "app:bulk:status": { + "aliases": [], + "args": {}, + "description": "Check the status of a specific bulk operation by ID, or list all bulk operations belonging to this app on this store in the last 7 days.\n\n Bulk operations allow you to process large amounts of data asynchronously. Learn more about \"bulk query operations\" (https://shopify.dev/docs/api/usage/bulk-operations/queries) and \"bulk mutation operations\" (https://shopify.dev/docs/api/usage/bulk-operations/imports).\n\n Use \"`bulk execute`\" (https://shopify.dev/docs/api/shopify-cli/app/app-bulk-execute) to start a new bulk operation.", "flags": { - "client-id": { - "description": "The Client ID of your app.", - "env": "SHOPIFY_FLAG_CLIENT_ID", - "exclusive": [ - "config" - ], - "hasDynamicHelp": false, - "hidden": false, - "multiple": false, - "name": "client-id", - "type": "option" - }, - "config": { - "char": "c", - "description": "The name of the app configuration.", - "env": "SHOPIFY_FLAG_APP_CONFIG", - "hasDynamicHelp": false, - "hidden": false, - "multiple": false, - "name": "config", - "type": "option" - }, "no-color": { - "allowNo": false, "description": "Disable color output.", "env": "SHOPIFY_FLAG_NO_COLOR", "hidden": false, "name": "no-color", + "allowNo": false, "type": "boolean" }, - "output-file": { - "dependsOn": [ - "watch" - ], - "description": "The file path where results should be written if --watch is specified. If not specified, results will be written to STDOUT.", - "env": "SHOPIFY_FLAG_OUTPUT_FILE", - "hasDynamicHelp": false, - "multiple": false, - "name": "output-file", - "type": "option" + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", + "allowNo": false, + "type": "boolean" }, "path": { "description": "The path to your app directory.", "env": "SHOPIFY_FLAG_PATH", - "hasDynamicHelp": false, - "multiple": false, "name": "path", "noCacheDefault": true, + "hasDynamicHelp": false, + "multiple": false, "type": "option" }, - "query": { - "char": "q", - "description": "The GraphQL query or mutation to run as a bulk operation.", - "env": "SHOPIFY_FLAG_QUERY", + "config": { + "char": "c", + "description": "The name of the app configuration.", + "env": "SHOPIFY_FLAG_APP_CONFIG", + "hidden": false, + "name": "config", "hasDynamicHelp": false, "multiple": false, - "name": "query", - "required": false, "type": "option" }, - "query-file": { - "description": "Path to a file containing the GraphQL query or mutation. Can't be used with --query.", - "env": "SHOPIFY_FLAG_QUERY_FILE", + "client-id": { + "description": "The Client ID of your app.", + "env": "SHOPIFY_FLAG_CLIENT_ID", + "exclusive": [ + "config" + ], + "hidden": false, + "name": "client-id", "hasDynamicHelp": false, "multiple": false, - "name": "query-file", "type": "option" }, "reset": { - "allowNo": false, "description": "Reset all your settings.", "env": "SHOPIFY_FLAG_RESET", "exclusive": [ @@ -267,132 +235,91 @@ ], "hidden": false, "name": "reset", + "allowNo": false, "type": "boolean" }, + "id": { + "description": "The bulk operation ID (numeric ID or full GID). If not provided, lists all bulk operations belonging to this app on this store in the last 7 days.", + "env": "SHOPIFY_FLAG_ID", + "name": "id", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, "store": { "char": "s", "description": "The store domain. Must be an existing dev store.", "env": "SHOPIFY_FLAG_STORE", - "hasDynamicHelp": false, - "multiple": false, "name": "store", - "type": "option" - }, - "variable-file": { - "description": "Path to a file containing GraphQL variables in JSONL format (one JSON object per line). Can't be used with --variables.", - "env": "SHOPIFY_FLAG_VARIABLE_FILE", - "exclusive": [ - "variables" - ], "hasDynamicHelp": false, "multiple": false, - "name": "variable-file", "type": "option" - }, - "variables": { - "char": "v", - "description": "The values for any GraphQL variables in your mutation, in JSON format. Can be specified multiple times.", - "env": "SHOPIFY_FLAG_VARIABLES", - "exclusive": [ - "variable-file" - ], - "hasDynamicHelp": false, - "multiple": true, - "name": "variables", - "type": "option" - }, - "verbose": { - "allowNo": false, - "description": "Increase the verbosity of the output.", - "env": "SHOPIFY_FLAG_VERBOSE", - "hidden": false, - "name": "verbose", - "type": "boolean" - }, - "version": { - "description": "The API version to use for the bulk operation. If not specified, uses the latest stable version.", - "env": "SHOPIFY_FLAG_VERSION", - "hasDynamicHelp": false, - "multiple": false, - "name": "version", - "type": "option" - }, - "watch": { - "allowNo": false, - "description": "Wait for bulk operation results before exiting. Defaults to false.", - "env": "SHOPIFY_FLAG_WATCH", - "name": "watch", - "type": "boolean" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "app:bulk:execute", + "hiddenAliases": [], + "id": "app:bulk:status", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", "strict": true, - "summary": "Execute bulk operations." - }, - "app:bulk:status": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/app", - "description": "Check the status of a specific bulk operation by ID, or list all bulk operations belonging to this app on this store in the last 7 days.\n\n Bulk operations allow you to process large amounts of data asynchronously. Learn more about \"bulk query operations\" (https://shopify.dev/docs/api/usage/bulk-operations/queries) and \"bulk mutation operations\" (https://shopify.dev/docs/api/usage/bulk-operations/imports).\n\n Use \"`bulk execute`\" (https://shopify.dev/docs/api/shopify-cli/app/app-bulk-execute) to start a new bulk operation.", + "summary": "Check the status of bulk operations.", "descriptionWithMarkdown": "Check the status of a specific bulk operation by ID, or list all bulk operations belonging to this app on this store in the last 7 days.\n\n Bulk operations allow you to process large amounts of data asynchronously. Learn more about [bulk query operations](https://shopify.dev/docs/api/usage/bulk-operations/queries) and [bulk mutation operations](https://shopify.dev/docs/api/usage/bulk-operations/imports).\n\n Use [`bulk execute`](https://shopify.dev/docs/api/shopify-cli/app/app-bulk-execute) to start a new bulk operation.", + "customPluginName": "@shopify/app" + }, + "app:deploy": { + "aliases": [], + "args": {}, + "description": "\"Builds the app\" (https://shopify.dev/docs/api/shopify-cli/app/app-build), then deploys your app configuration and extensions.\n\n This command creates an app version, which is a snapshot of your app configuration and all extensions. This version is then released to users.\n\n This command doesn't deploy your \"web app\" (https://shopify.dev/docs/apps/tools/cli/structure#web-components). You need to \"deploy your web app\" (https://shopify.dev/docs/apps/deployment/web) to your own hosting solution.\n ", "flags": { - "client-id": { - "description": "The Client ID of your app.", - "env": "SHOPIFY_FLAG_CLIENT_ID", - "exclusive": [ - "config" - ], - "hasDynamicHelp": false, + "no-color": { + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", + "allowNo": false, + "type": "boolean" + }, + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", "hidden": false, + "name": "verbose", + "allowNo": false, + "type": "boolean" + }, + "path": { + "description": "The path to your app directory.", + "env": "SHOPIFY_FLAG_PATH", + "name": "path", + "noCacheDefault": true, + "hasDynamicHelp": false, "multiple": false, - "name": "client-id", "type": "option" }, "config": { "char": "c", "description": "The name of the app configuration.", "env": "SHOPIFY_FLAG_APP_CONFIG", - "hasDynamicHelp": false, "hidden": false, - "multiple": false, "name": "config", - "type": "option" - }, - "id": { - "description": "The bulk operation ID (numeric ID or full GID). If not provided, lists all bulk operations belonging to this app on this store in the last 7 days.", - "env": "SHOPIFY_FLAG_ID", "hasDynamicHelp": false, "multiple": false, - "name": "id", "type": "option" }, - "no-color": { - "allowNo": false, - "description": "Disable color output.", - "env": "SHOPIFY_FLAG_NO_COLOR", + "client-id": { + "description": "The Client ID of your app.", + "env": "SHOPIFY_FLAG_CLIENT_ID", + "exclusive": [ + "config" + ], "hidden": false, - "name": "no-color", - "type": "boolean" - }, - "path": { - "description": "The path to your app directory.", - "env": "SHOPIFY_FLAG_PATH", + "name": "client-id", "hasDynamicHelp": false, "multiple": false, - "name": "path", - "noCacheDefault": true, "type": "option" }, "reset": { - "allowNo": false, "description": "Reset all your settings.", "env": "SHOPIFY_FLAG_RESET", "exclusive": [ @@ -400,97 +327,145 @@ ], "hidden": false, "name": "reset", + "allowNo": false, "type": "boolean" }, - "store": { - "char": "s", - "description": "The store domain. Must be an existing dev store.", - "env": "SHOPIFY_FLAG_STORE", + "force": { + "char": "f", + "description": "[Deprecated] Deploy without asking for confirmation. Equivalent to --allow-updates --allow-deletes. Use --allow-updates for CI/CD environments instead.", + "env": "SHOPIFY_FLAG_FORCE", + "hidden": false, + "name": "force", + "allowNo": false, + "type": "boolean" + }, + "allow-updates": { + "description": "Allows adding and updating extensions and configuration without requiring user confirmation. Recommended option for CI/CD environments.", + "env": "SHOPIFY_FLAG_ALLOW_UPDATES", + "hidden": false, + "name": "allow-updates", + "allowNo": false, + "type": "boolean" + }, + "allow-deletes": { + "description": "Allows removing extensions and configuration without requiring user confirmation. For CI/CD environments, the recommended flag is --allow-updates.", + "env": "SHOPIFY_FLAG_ALLOW_DELETES", + "hidden": false, + "name": "allow-deletes", + "allowNo": false, + "type": "boolean" + }, + "no-release": { + "description": "Creates a version but doesn't release it - it's not made available to merchants. With this flag, a user confirmation is not required.", + "env": "SHOPIFY_FLAG_NO_RELEASE", + "exclusive": [ + "allow-updates", + "allow-deletes" + ], + "hidden": false, + "name": "no-release", + "allowNo": false, + "type": "boolean" + }, + "no-build": { + "description": "Use with caution: Skips building any elements of the app that require building. You should ensure your app has been prepared in advance, such as by running `shopify app build` or by caching build artifacts.", + "env": "SHOPIFY_FLAG_NO_BUILD", + "name": "no-build", + "allowNo": false, + "type": "boolean" + }, + "message": { + "description": "Optional message that will be associated with this version. This is for internal use only and won't be available externally.", + "env": "SHOPIFY_FLAG_MESSAGE", + "hidden": false, + "name": "message", "hasDynamicHelp": false, "multiple": false, - "name": "store", "type": "option" }, - "verbose": { - "allowNo": false, - "description": "Increase the verbosity of the output.", - "env": "SHOPIFY_FLAG_VERBOSE", + "version": { + "description": "Optional version tag that will be associated with this app version. If not provided, an auto-generated identifier will be generated for this app version.", + "env": "SHOPIFY_FLAG_VERSION", "hidden": false, - "name": "verbose", - "type": "boolean" + "name": "version", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "source-control-url": { + "description": "URL associated with the new app version.", + "env": "SHOPIFY_FLAG_SOURCE_CONTROL_URL", + "hidden": false, + "name": "source-control-url", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "app:bulk:status", + "hiddenAliases": [], + "id": "app:deploy", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", "strict": true, - "summary": "Check the status of bulk operations." + "summary": "Deploy your Shopify app.", + "descriptionWithMarkdown": "[Builds the app](https://shopify.dev/docs/api/shopify-cli/app/app-build), then deploys your app configuration and extensions.\n\n This command creates an app version, which is a snapshot of your app configuration and all extensions. This version is then released to users.\n\n This command doesn't deploy your [web app](https://shopify.dev/docs/apps/tools/cli/structure#web-components). You need to [deploy your web app](https://shopify.dev/docs/apps/deployment/web) to your own hosting solution.\n ", + "customPluginName": "@shopify/app" }, - "app:config:link": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/app", - "description": "Pulls app configuration from the Developer Dashboard and creates or overwrites a configuration file. You can create a new app with this command to start with a default configuration file.\n\n For more information on the format of the created TOML configuration file, refer to the \"App configuration\" (https://shopify.dev/docs/apps/tools/cli/configuration) page.\n ", - "descriptionWithMarkdown": "Pulls app configuration from the Developer Dashboard and creates or overwrites a configuration file. You can create a new app with this command to start with a default configuration file.\n\n For more information on the format of the created TOML configuration file, refer to the [App configuration](https://shopify.dev/docs/apps/tools/cli/configuration) page.\n ", + "app:dev": { + "aliases": [], + "args": {}, + "description": "Builds and previews your app on a dev store, and watches for changes. \"Read more about testing apps locally\" (https://shopify.dev/docs/apps/build/cli-for-apps/test-apps-locally).", "flags": { - "client-id": { - "description": "The Client ID of your app.", - "env": "SHOPIFY_FLAG_CLIENT_ID", - "exclusive": [ - "config" - ], - "hasDynamicHelp": false, + "no-color": { + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", + "allowNo": false, + "type": "boolean" + }, + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", "hidden": false, + "name": "verbose", + "allowNo": false, + "type": "boolean" + }, + "path": { + "description": "The path to your app directory.", + "env": "SHOPIFY_FLAG_PATH", + "name": "path", + "noCacheDefault": true, + "hasDynamicHelp": false, "multiple": false, - "name": "client-id", "type": "option" }, "config": { "char": "c", "description": "The name of the app configuration.", "env": "SHOPIFY_FLAG_APP_CONFIG", - "hasDynamicHelp": false, "hidden": false, - "multiple": false, "name": "config", - "type": "option" - }, - "no-color": { - "allowNo": false, - "description": "Disable color output.", - "env": "SHOPIFY_FLAG_NO_COLOR", - "hidden": false, - "name": "no-color", - "type": "boolean" - }, - "organization-id": { - "env": "SHOPIFY_FLAG_ORGANIZATION_ID", - "exclusive": [ - "client-id" - ], "hasDynamicHelp": false, - "hidden": true, "multiple": false, - "name": "organization-id", "type": "option" }, - "path": { - "description": "The path to your app directory.", - "env": "SHOPIFY_FLAG_PATH", + "client-id": { + "description": "The Client ID of your app.", + "env": "SHOPIFY_FLAG_CLIENT_ID", + "exclusive": [ + "config" + ], + "hidden": false, + "name": "client-id", "hasDynamicHelp": false, "multiple": false, - "name": "path", - "noCacheDefault": true, "type": "option" }, "reset": { - "allowNo": false, "description": "Reset all your settings.", "env": "SHOPIFY_FLAG_RESET", "exclusive": [ @@ -498,77 +473,185 @@ ], "hidden": false, "name": "reset", + "allowNo": false, "type": "boolean" }, - "verbose": { + "store": { + "char": "s", + "description": "Store URL. Must be an existing development or Shopify Plus sandbox store.", + "env": "SHOPIFY_FLAG_STORE", + "name": "store", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "skip-dependencies-installation": { + "description": "Skips the installation of dependencies. Deprecated, use workspaces instead.", + "env": "SHOPIFY_FLAG_SKIP_DEPENDENCIES_INSTALLATION", + "name": "skip-dependencies-installation", + "allowNo": false, + "type": "boolean" + }, + "no-update": { + "description": "Uses the app URL from the toml file instead an autogenerated URL for dev.", + "env": "SHOPIFY_FLAG_NO_UPDATE", + "name": "no-update", + "allowNo": false, + "type": "boolean" + }, + "subscription-product-url": { + "description": "Resource URL for subscription UI extension. Format: \"/products/{productId}\"", + "env": "SHOPIFY_FLAG_SUBSCRIPTION_PRODUCT_URL", + "name": "subscription-product-url", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "checkout-cart-url": { + "description": "Resource URL for checkout UI extension. Format: \"/cart/{productVariantID}:{productQuantity}\"", + "env": "SHOPIFY_FLAG_CHECKOUT_CART_URL", + "name": "checkout-cart-url", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "tunnel-url": { + "description": "Use a custom tunnel, it must be running before executing dev. Format: \"https://my-tunnel-url:port\".", + "env": "SHOPIFY_FLAG_TUNNEL_URL", + "exclusive": [ + "tunnel" + ], + "name": "tunnel-url", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "use-localhost": { + "description": "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)", + "env": "SHOPIFY_FLAG_USE_LOCALHOST", + "exclusive": [ + "tunnel-url" + ], + "name": "use-localhost", "allowNo": false, - "description": "Increase the verbosity of the output.", - "env": "SHOPIFY_FLAG_VERBOSE", - "hidden": false, - "name": "verbose", "type": "boolean" + }, + "localhost-port": { + "description": "Port to use for localhost.", + "env": "SHOPIFY_FLAG_LOCALHOST_PORT", + "name": "localhost-port", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "theme": { + "char": "t", + "description": "Theme ID or name of the theme app extension host theme.", + "env": "SHOPIFY_FLAG_THEME", + "name": "theme", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "theme-app-extension-port": { + "description": "Local port of the theme app extension development server.", + "env": "SHOPIFY_FLAG_THEME_APP_EXTENSION_PORT", + "name": "theme-app-extension-port", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "notify": { + "description": "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", + "name": "notify", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "graphiql-port": { + "description": "Local port of the GraphiQL development server.", + "env": "SHOPIFY_FLAG_GRAPHIQL_PORT", + "hidden": true, + "name": "graphiql-port", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "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", + "hidden": true, + "name": "graphiql-key", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "app:config:link", + "hiddenAliases": [], + "id": "app:dev", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", "strict": true, - "summary": "Fetch your app configuration from the Developer Dashboard." + "summary": "Run the app.", + "descriptionWithMarkdown": "Builds and previews your app on a dev store, and watches for changes. [Read more about testing apps locally](https://shopify.dev/docs/apps/build/cli-for-apps/test-apps-locally).", + "customPluginName": "@shopify/app" }, - "app:config:pull": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/app", - "description": "Pulls the latest configuration from the already-linked Shopify app and updates the selected configuration file.\n\nThis command reuses the existing linked app and organization and skips all interactive prompts. Use `--config` to target a specific configuration file, or omit it to use the default one.", - "descriptionWithMarkdown": "Pulls the latest configuration from the already-linked Shopify app and updates the selected configuration file.\n\nThis command reuses the existing linked app and organization and skips all interactive prompts. Use `--config` to target a specific configuration file, or omit it to use the default one.", + "app:dev:clean": { + "aliases": [], + "args": {}, + "description": "Stop the dev preview that was started with `shopify app dev`.\n\n It restores the app's active version to the selected development store.\n ", "flags": { - "client-id": { - "description": "The Client ID of your app.", - "env": "SHOPIFY_FLAG_CLIENT_ID", - "exclusive": [ - "config" - ], - "hasDynamicHelp": false, + "no-color": { + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", + "allowNo": false, + "type": "boolean" + }, + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", "hidden": false, + "name": "verbose", + "allowNo": false, + "type": "boolean" + }, + "path": { + "description": "The path to your app directory.", + "env": "SHOPIFY_FLAG_PATH", + "name": "path", + "noCacheDefault": true, + "hasDynamicHelp": false, "multiple": false, - "name": "client-id", "type": "option" }, "config": { "char": "c", "description": "The name of the app configuration.", "env": "SHOPIFY_FLAG_APP_CONFIG", - "hasDynamicHelp": false, "hidden": false, - "multiple": false, "name": "config", + "hasDynamicHelp": false, + "multiple": false, "type": "option" }, - "no-color": { - "allowNo": false, - "description": "Disable color output.", - "env": "SHOPIFY_FLAG_NO_COLOR", + "client-id": { + "description": "The Client ID of your app.", + "env": "SHOPIFY_FLAG_CLIENT_ID", + "exclusive": [ + "config" + ], "hidden": false, - "name": "no-color", - "type": "boolean" - }, - "path": { - "description": "The path to your app directory.", - "env": "SHOPIFY_FLAG_PATH", + "name": "client-id", "hasDynamicHelp": false, "multiple": false, - "name": "path", - "noCacheDefault": true, "type": "option" }, "reset": { - "allowNo": false, "description": "Reset all your settings.", "env": "SHOPIFY_FLAG_RESET", "exclusive": [ @@ -576,159 +659,197 @@ ], "hidden": false, "name": "reset", + "allowNo": false, "type": "boolean" }, - "verbose": { - "allowNo": false, - "description": "Increase the verbosity of the output.", - "env": "SHOPIFY_FLAG_VERBOSE", + "store": { + "char": "s", + "description": "Store URL. Must be an existing development store.", + "env": "SHOPIFY_FLAG_STORE", "hidden": false, - "name": "verbose", - "type": "boolean" + "name": "store", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "app:config:pull", + "hiddenAliases": [], + "id": "app:dev:clean", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", "strict": true, - "summary": "Refresh an already-linked app configuration without prompts." + "summary": "Cleans up the dev preview from the selected store.", + "descriptionWithMarkdown": "Stop the dev preview that was started with `shopify app dev`.\n\n It restores the app's active version to the selected development store.\n ", + "customPluginName": "@shopify/app" }, - "app:config:use": { - "aliases": [ - ], - "args": { - "config": { - "description": "The name of the app configuration. Can be 'shopify.app.staging.toml' or simply 'staging'.", - "name": "config" - } - }, - "customPluginName": "@shopify/app", - "description": "Sets default configuration when you run app-related CLI commands. If you omit the `config-name` parameter, then you'll be prompted to choose from the configuration files in your project.", - "descriptionWithMarkdown": "Sets default configuration when you run app-related CLI commands. If you omit the `config-name` parameter, then you'll be prompted to choose from the configuration files in your project.", + "app:logs": { + "aliases": [], + "args": {}, + "description": "\n Opens a real-time stream of detailed app logs from the selected app and store.\n Use the `--source` argument to limit output to a particular log source, such as a specific Shopify Function handle. Use the `shopify app logs sources` command to view a list of sources.\n Use the `--status` argument to filter on status, either `success` or `failure`.\n ```\n shopify app logs --status=success --source=extension.discount-function\n ```\n ", "flags": { - "client-id": { - "description": "The Client ID of your app.", - "env": "SHOPIFY_FLAG_CLIENT_ID", - "exclusive": [ - "config" - ], - "hasDynamicHelp": false, - "hidden": false, - "multiple": false, - "name": "client-id", - "type": "option" - }, "no-color": { - "allowNo": false, "description": "Disable color output.", "env": "SHOPIFY_FLAG_NO_COLOR", "hidden": false, "name": "no-color", + "allowNo": false, + "type": "boolean" + }, + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", + "allowNo": false, "type": "boolean" }, "path": { "description": "The path to your app directory.", "env": "SHOPIFY_FLAG_PATH", - "hasDynamicHelp": false, - "multiple": false, "name": "path", "noCacheDefault": true, + "hasDynamicHelp": false, + "multiple": false, "type": "option" }, - "reset": { - "allowNo": false, - "description": "Reset all your settings.", - "env": "SHOPIFY_FLAG_RESET", + "config": { + "char": "c", + "description": "The name of the app configuration.", + "env": "SHOPIFY_FLAG_APP_CONFIG", + "hidden": false, + "name": "config", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "client-id": { + "description": "The Client ID of your app.", + "env": "SHOPIFY_FLAG_CLIENT_ID", "exclusive": [ "config" ], "hidden": false, - "name": "reset", - "type": "boolean" - }, - "verbose": { + "name": "client-id", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "reset": { + "description": "Reset all your settings.", + "env": "SHOPIFY_FLAG_RESET", + "exclusive": [ + "config" + ], + "hidden": false, + "name": "reset", "allowNo": false, - "description": "Increase the verbosity of the output.", - "env": "SHOPIFY_FLAG_VERBOSE", + "type": "boolean" + }, + "json": { + "char": "j", + "description": "Output the result as JSON. Automatically disables color output.", + "env": "SHOPIFY_FLAG_JSON", "hidden": false, - "name": "verbose", + "name": "json", + "allowNo": false, "type": "boolean" + }, + "store": { + "char": "s", + "description": "Store URL. Must be an existing development or Shopify Plus sandbox store.", + "env": "SHOPIFY_FLAG_STORE", + "name": "store", + "hasDynamicHelp": false, + "multiple": true, + "type": "option" + }, + "source": { + "description": "Filters output to the specified log source.", + "env": "SHOPIFY_FLAG_SOURCE", + "name": "source", + "hasDynamicHelp": false, + "multiple": true, + "type": "option" + }, + "status": { + "description": "Filters output to the specified status (success or failure).", + "env": "SHOPIFY_FLAG_STATUS", + "name": "status", + "hasDynamicHelp": false, + "multiple": false, + "options": [ + "success", + "failure" + ], + "type": "option" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "app:config:use", + "hiddenAliases": [], + "id": "app:logs", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", "strict": true, - "summary": "Activate an app configuration.", - "usage": "app config use [config] [flags]" + "summary": "Stream detailed logs for your Shopify app.", + "descriptionWithMarkdown": "\n Opens a real-time stream of detailed app logs from the selected app and store.\n Use the `--source` argument to limit output to a particular log source, such as a specific Shopify Function handle. Use the `shopify app logs sources` command to view a list of sources.\n Use the `--status` argument to filter on status, either `success` or `failure`.\n ```\n shopify app logs --status=success --source=extension.discount-function\n ```\n ", + "customPluginName": "@shopify/app" }, - "app:config:validate": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/app", - "description": "Validates the selected app configuration file and all extension configurations against their schemas and reports any errors found.", - "descriptionWithMarkdown": "Validates the selected app configuration file and all extension configurations against their schemas and reports any errors found.", + "app:logs:sources": { + "aliases": [], + "args": {}, + "description": "The output source names can be used with the `--source` argument of `shopify app logs` to filter log output. Currently only function extensions are supported as sources.", "flags": { - "client-id": { - "description": "The Client ID of your app.", - "env": "SHOPIFY_FLAG_CLIENT_ID", - "exclusive": [ - "config" - ], - "hasDynamicHelp": false, + "no-color": { + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", + "allowNo": false, + "type": "boolean" + }, + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", "hidden": false, + "name": "verbose", + "allowNo": false, + "type": "boolean" + }, + "path": { + "description": "The path to your app directory.", + "env": "SHOPIFY_FLAG_PATH", + "name": "path", + "noCacheDefault": true, + "hasDynamicHelp": false, "multiple": false, - "name": "client-id", "type": "option" }, "config": { "char": "c", "description": "The name of the app configuration.", "env": "SHOPIFY_FLAG_APP_CONFIG", - "hasDynamicHelp": false, "hidden": false, - "multiple": false, "name": "config", + "hasDynamicHelp": false, + "multiple": false, "type": "option" }, - "json": { - "allowNo": false, - "char": "j", - "description": "Output the result as JSON. Automatically disables color output.", - "env": "SHOPIFY_FLAG_JSON", - "hidden": false, - "name": "json", - "type": "boolean" - }, - "no-color": { - "allowNo": false, - "description": "Disable color output.", - "env": "SHOPIFY_FLAG_NO_COLOR", + "client-id": { + "description": "The Client ID of your app.", + "env": "SHOPIFY_FLAG_CLIENT_ID", + "exclusive": [ + "config" + ], "hidden": false, - "name": "no-color", - "type": "boolean" - }, - "path": { - "description": "The path to your app directory.", - "env": "SHOPIFY_FLAG_PATH", + "name": "client-id", "hasDynamicHelp": false, "multiple": false, - "name": "path", - "noCacheDefault": true, "type": "option" }, "reset": { - "allowNo": false, "description": "Reset all your settings.", "env": "SHOPIFY_FLAG_RESET", "exclusive": [ @@ -736,121 +857,74 @@ ], "hidden": false, "name": "reset", - "type": "boolean" - }, - "verbose": { "allowNo": false, - "description": "Increase the verbosity of the output.", - "env": "SHOPIFY_FLAG_VERBOSE", - "hidden": false, - "name": "verbose", "type": "boolean" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "app:config:validate", + "hiddenAliases": [], + "id": "app:logs:sources", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", "strict": true, - "summary": "Validate your app configuration and extensions." + "summary": "Print out a list of sources that may be used with the logs command.", + "descriptionWithMarkdown": "The output source names can be used with the `--source` argument of `shopify app logs` to filter log output. Currently only function extensions are supported as sources.", + "customPluginName": "@shopify/app" }, - "app:deploy": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/app", - "description": "\"Builds the app\" (https://shopify.dev/docs/api/shopify-cli/app/app-build), then deploys your app configuration and extensions.\n\n This command creates an app version, which is a snapshot of your app configuration and all extensions. This version is then released to users.\n\n This command doesn't deploy your \"web app\" (https://shopify.dev/docs/apps/tools/cli/structure#web-components). You need to \"deploy your web app\" (https://shopify.dev/docs/apps/deployment/web) to your own hosting solution.\n ", - "descriptionWithMarkdown": "[Builds the app](https://shopify.dev/docs/api/shopify-cli/app/app-build), then deploys your app configuration and extensions.\n\n This command creates an app version, which is a snapshot of your app configuration and all extensions. This version is then released to users.\n\n This command doesn't deploy your [web app](https://shopify.dev/docs/apps/tools/cli/structure#web-components). You need to [deploy your web app](https://shopify.dev/docs/apps/deployment/web) to your own hosting solution.\n ", + "app:import-custom-data-definitions": { + "aliases": [], + "args": {}, + "description": "Import metafield and metaobject definitions from your development store. \"Read more about declarative custom data definitions\" (https://shopify.dev/docs/apps/build/custom-data/declarative-custom-data-definitions).", "flags": { - "allow-deletes": { - "allowNo": false, - "description": "Allows removing extensions and configuration without requiring user confirmation. For CI/CD environments, the recommended flag is --allow-updates.", - "env": "SHOPIFY_FLAG_ALLOW_DELETES", + "no-color": { + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", "hidden": false, - "name": "allow-deletes", + "name": "no-color", + "allowNo": false, "type": "boolean" }, - "allow-updates": { - "allowNo": false, - "description": "Allows adding and updating extensions and configuration without requiring user confirmation. Recommended option for CI/CD environments.", - "env": "SHOPIFY_FLAG_ALLOW_UPDATES", + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", "hidden": false, - "name": "allow-updates", + "name": "verbose", + "allowNo": false, "type": "boolean" }, - "client-id": { - "description": "The Client ID of your app.", - "env": "SHOPIFY_FLAG_CLIENT_ID", - "exclusive": [ - "config" - ], + "path": { + "description": "The path to your app directory.", + "env": "SHOPIFY_FLAG_PATH", + "name": "path", + "noCacheDefault": true, "hasDynamicHelp": false, - "hidden": false, "multiple": false, - "name": "client-id", "type": "option" }, "config": { "char": "c", "description": "The name of the app configuration.", "env": "SHOPIFY_FLAG_APP_CONFIG", - "hasDynamicHelp": false, "hidden": false, - "multiple": false, "name": "config", - "type": "option" - }, - "message": { - "description": "Optional message that will be associated with this version. This is for internal use only and won't be available externally.", - "env": "SHOPIFY_FLAG_MESSAGE", "hasDynamicHelp": false, - "hidden": false, "multiple": false, - "name": "message", "type": "option" }, - "no-build": { - "allowNo": false, - "description": "Use with caution: Skips building any elements of the app that require building. You should ensure your app has been prepared in advance, such as by running `shopify app build` or by caching build artifacts.", - "env": "SHOPIFY_FLAG_NO_BUILD", - "name": "no-build", - "type": "boolean" - }, - "no-color": { - "allowNo": false, - "description": "Disable color output.", - "env": "SHOPIFY_FLAG_NO_COLOR", - "hidden": false, - "name": "no-color", - "type": "boolean" - }, - "no-release": { - "allowNo": false, - "description": "Creates a version but doesn't release it - it's not made available to merchants. With this flag, a user confirmation is not required.", - "env": "SHOPIFY_FLAG_NO_RELEASE", + "client-id": { + "description": "The Client ID of your app.", + "env": "SHOPIFY_FLAG_CLIENT_ID", "exclusive": [ - "allow-updates", - "allow-deletes" + "config" ], "hidden": false, - "name": "no-release", - "type": "boolean" - }, - "path": { - "description": "The path to your app directory.", - "env": "SHOPIFY_FLAG_PATH", + "name": "client-id", "hasDynamicHelp": false, "multiple": false, - "name": "path", - "noCacheDefault": true, "type": "option" }, "reset": { - "allowNo": false, "description": "Reset all your settings.", "env": "SHOPIFY_FLAG_RESET", "exclusive": [ @@ -858,284 +932,163 @@ ], "hidden": false, "name": "reset", + "allowNo": false, "type": "boolean" }, - "source-control-url": { - "description": "URL associated with the new app version.", - "env": "SHOPIFY_FLAG_SOURCE_CONTROL_URL", + "store": { + "char": "s", + "description": "Store URL. Must be an existing development or Shopify Plus sandbox store.", + "env": "SHOPIFY_FLAG_STORE", + "name": "store", "hasDynamicHelp": false, - "hidden": false, "multiple": false, - "name": "source-control-url", "type": "option" }, - "verbose": { + "include-existing": { + "description": "Include existing declared definitions in the output.", + "env": "SHOPIFY_FLAG_INCLUDE_EXISTING", + "name": "include-existing", "allowNo": false, - "description": "Increase the verbosity of the output.", - "env": "SHOPIFY_FLAG_VERBOSE", - "hidden": false, - "name": "verbose", "type": "boolean" - }, - "version": { - "description": "Optional version tag that will be associated with this app version. If not provided, an auto-generated identifier will be generated for this app version.", - "env": "SHOPIFY_FLAG_VERSION", - "hasDynamicHelp": false, - "hidden": false, - "multiple": false, - "name": "version", - "type": "option" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "app:deploy", + "hiddenAliases": [], + "id": "app:import-custom-data-definitions", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", "strict": true, - "summary": "Deploy your Shopify app." + "summary": "Import metafield and metaobject definitions.", + "descriptionWithMarkdown": "Import metafield and metaobject definitions from your development store. [Read more about declarative custom data definitions](https://shopify.dev/docs/apps/build/custom-data/declarative-custom-data-definitions).", + "customPluginName": "@shopify/app" }, - "app:dev": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/app", - "description": "Builds and previews your app on a dev store, and watches for changes. \"Read more about testing apps locally\" (https://shopify.dev/docs/apps/build/cli-for-apps/test-apps-locally).", - "descriptionWithMarkdown": "Builds and previews your app on a dev store, and watches for changes. [Read more about testing apps locally](https://shopify.dev/docs/apps/build/cli-for-apps/test-apps-locally).", + "app:import-extensions": { + "aliases": [], + "args": {}, + "description": "Import dashboard-managed extensions into your app.", "flags": { - "checkout-cart-url": { - "description": "Resource URL for checkout UI extension. Format: \"/cart/{productVariantID}:{productQuantity}\"", - "env": "SHOPIFY_FLAG_CHECKOUT_CART_URL", - "hasDynamicHelp": false, - "multiple": false, - "name": "checkout-cart-url", - "type": "option" - }, - "client-id": { - "description": "The Client ID of your app.", - "env": "SHOPIFY_FLAG_CLIENT_ID", - "exclusive": [ - "config" - ], - "hasDynamicHelp": false, - "hidden": false, - "multiple": false, - "name": "client-id", - "type": "option" - }, - "config": { - "char": "c", - "description": "The name of the app configuration.", - "env": "SHOPIFY_FLAG_APP_CONFIG", - "hasDynamicHelp": false, - "hidden": false, - "multiple": false, - "name": "config", - "type": "option" - }, - "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", - "hasDynamicHelp": false, - "hidden": true, - "multiple": false, - "name": "graphiql-key", - "type": "option" - }, - "graphiql-port": { - "description": "Local port of the GraphiQL development server. Must be between 1 and 65535.", - "env": "SHOPIFY_FLAG_GRAPHIQL_PORT", - "hasDynamicHelp": false, - "hidden": true, - "multiple": false, - "name": "graphiql-port", - "type": "option" - }, - "localhost-port": { - "description": "Port to use for localhost. Must be between 1 and 65535.", - "env": "SHOPIFY_FLAG_LOCALHOST_PORT", - "hasDynamicHelp": false, - "multiple": false, - "name": "localhost-port", - "type": "option" - }, "no-color": { - "allowNo": false, "description": "Disable color output.", "env": "SHOPIFY_FLAG_NO_COLOR", "hidden": false, "name": "no-color", + "allowNo": false, "type": "boolean" }, - "no-update": { + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", "allowNo": false, - "description": "Uses the app URL from the toml file instead an autogenerated URL for dev.", - "env": "SHOPIFY_FLAG_NO_UPDATE", - "name": "no-update", "type": "boolean" }, - "notify": { - "description": "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", - "hasDynamicHelp": false, - "multiple": false, - "name": "notify", - "type": "option" - }, "path": { "description": "The path to your app directory.", "env": "SHOPIFY_FLAG_PATH", - "hasDynamicHelp": false, - "multiple": false, "name": "path", "noCacheDefault": true, - "type": "option" - }, - "reset": { - "allowNo": false, - "description": "Reset all your settings.", - "env": "SHOPIFY_FLAG_RESET", - "exclusive": [ - "config" - ], - "hidden": false, - "name": "reset", - "type": "boolean" - }, - "skip-dependencies-installation": { - "allowNo": false, - "description": "Skips the installation of dependencies. Deprecated, use workspaces instead.", - "env": "SHOPIFY_FLAG_SKIP_DEPENDENCIES_INSTALLATION", - "name": "skip-dependencies-installation", - "type": "boolean" - }, - "store": { - "char": "s", - "description": "Store URL. Must be an existing development or Shopify Plus sandbox store.", - "env": "SHOPIFY_FLAG_STORE", - "hasDynamicHelp": false, - "multiple": false, - "name": "store", - "type": "option" - }, - "subscription-product-url": { - "description": "Resource URL for subscription UI extension. Format: \"/products/{productId}\"", - "env": "SHOPIFY_FLAG_SUBSCRIPTION_PRODUCT_URL", - "hasDynamicHelp": false, - "multiple": false, - "name": "subscription-product-url", - "type": "option" - }, - "theme": { - "char": "t", - "description": "Theme ID or name of the theme app extension host theme.", - "env": "SHOPIFY_FLAG_THEME", "hasDynamicHelp": false, "multiple": false, - "name": "theme", "type": "option" }, - "theme-app-extension-port": { - "description": "Local port of the theme app extension development server. Must be between 1 and 65535.", - "env": "SHOPIFY_FLAG_THEME_APP_EXTENSION_PORT", + "config": { + "char": "c", + "description": "The name of the app configuration.", + "env": "SHOPIFY_FLAG_APP_CONFIG", + "hidden": false, + "name": "config", "hasDynamicHelp": false, "multiple": false, - "name": "theme-app-extension-port", "type": "option" }, - "tunnel-url": { - "description": "Use a custom tunnel, it must be running before executing dev. Format: \"https://my-tunnel-url:port\".", - "env": "SHOPIFY_FLAG_TUNNEL_URL", + "client-id": { + "description": "The Client ID of your app.", + "env": "SHOPIFY_FLAG_CLIENT_ID", "exclusive": [ - "tunnel" + "config" ], + "hidden": false, + "name": "client-id", "hasDynamicHelp": false, "multiple": false, - "name": "tunnel-url", "type": "option" }, - "use-localhost": { - "allowNo": false, - "description": "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)", - "env": "SHOPIFY_FLAG_USE_LOCALHOST", + "reset": { + "description": "Reset all your settings.", + "env": "SHOPIFY_FLAG_RESET", "exclusive": [ - "tunnel-url" + "config" ], - "name": "use-localhost", - "type": "boolean" - }, - "verbose": { - "allowNo": false, - "description": "Increase the verbosity of the output.", - "env": "SHOPIFY_FLAG_VERBOSE", "hidden": false, - "name": "verbose", + "name": "reset", + "allowNo": false, "type": "boolean" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "app:dev", + "hiddenAliases": [], + "id": "app:import-extensions", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", "strict": true, - "summary": "Run the app." + "customPluginName": "@shopify/app" }, - "app:dev:clean": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/app", - "description": "Stop the dev preview that was started with `shopify app dev`.\n\n It restores the app's active version to the selected development store.\n ", - "descriptionWithMarkdown": "Stop the dev preview that was started with `shopify app dev`.\n\n It restores the app's active version to the selected development store.\n ", + "app:info": { + "aliases": [], + "args": {}, + "description": "The information returned includes the following:\n\n - The app and dev store that's used when you run the \"dev\" (https://shopify.dev/docs/api/shopify-cli/app/app-dev) command. You can reset these configurations using \"`dev --reset`\" (https://shopify.dev/docs/api/shopify-cli/app/app-dev#flags-propertydetail-reset).\n - The \"structure\" (https://shopify.dev/docs/apps/tools/cli/structure) of your app project.\n - The \"access scopes\" (https://shopify.dev/docs/api/usage) your app has requested.\n - System information, including the package manager and version of Shopify CLI used in the project.", "flags": { - "client-id": { - "description": "The Client ID of your app.", - "env": "SHOPIFY_FLAG_CLIENT_ID", - "exclusive": [ - "config" - ], - "hasDynamicHelp": false, + "no-color": { + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", + "allowNo": false, + "type": "boolean" + }, + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", "hidden": false, + "name": "verbose", + "allowNo": false, + "type": "boolean" + }, + "path": { + "description": "The path to your app directory.", + "env": "SHOPIFY_FLAG_PATH", + "name": "path", + "noCacheDefault": true, + "hasDynamicHelp": false, "multiple": false, - "name": "client-id", "type": "option" }, "config": { "char": "c", "description": "The name of the app configuration.", "env": "SHOPIFY_FLAG_APP_CONFIG", - "hasDynamicHelp": false, "hidden": false, - "multiple": false, "name": "config", + "hasDynamicHelp": false, + "multiple": false, "type": "option" }, - "no-color": { - "allowNo": false, - "description": "Disable color output.", - "env": "SHOPIFY_FLAG_NO_COLOR", + "client-id": { + "description": "The Client ID of your app.", + "env": "SHOPIFY_FLAG_CLIENT_ID", + "exclusive": [ + "config" + ], "hidden": false, - "name": "no-color", - "type": "boolean" - }, - "path": { - "description": "The path to your app directory.", - "env": "SHOPIFY_FLAG_PATH", + "name": "client-id", "hasDynamicHelp": false, "multiple": false, - "name": "path", - "noCacheDefault": true, "type": "option" }, "reset": { - "allowNo": false, "description": "Reset all your settings.", "env": "SHOPIFY_FLAG_RESET", "exclusive": [ @@ -1143,174 +1096,205 @@ ], "hidden": false, "name": "reset", + "allowNo": false, "type": "boolean" }, - "store": { - "char": "s", - "description": "Store URL. Must be an existing development store.", - "env": "SHOPIFY_FLAG_STORE", - "hasDynamicHelp": false, + "json": { + "char": "j", + "description": "Output the result as JSON. Automatically disables color output.", + "env": "SHOPIFY_FLAG_JSON", "hidden": false, - "multiple": false, - "name": "store", - "type": "option" - }, - "verbose": { + "name": "json", "allowNo": false, - "description": "Increase the verbosity of the output.", - "env": "SHOPIFY_FLAG_VERBOSE", + "type": "boolean" + }, + "web-env": { + "description": "Outputs environment variables necessary for running and deploying web/.", + "env": "SHOPIFY_FLAG_OUTPUT_WEB_ENV", "hidden": false, - "name": "verbose", + "name": "web-env", + "allowNo": false, "type": "boolean" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "app:dev:clean", + "hiddenAliases": [], + "id": "app:info", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", "strict": true, - "summary": "Cleans up the dev preview from the selected store." + "summary": "Print basic information about your app and extensions.", + "descriptionWithMarkdown": "The information returned includes the following:\n\n - The app and dev store that's used when you run the [dev](https://shopify.dev/docs/api/shopify-cli/app/app-dev) command. You can reset these configurations using [`dev --reset`](https://shopify.dev/docs/api/shopify-cli/app/app-dev#flags-propertydetail-reset).\n - The [structure](https://shopify.dev/docs/apps/tools/cli/structure) of your app project.\n - The [access scopes](https://shopify.dev/docs/api/usage) your app has requested.\n - System information, including the package manager and version of Shopify CLI used in the project.", + "customPluginName": "@shopify/app" }, - "app:env:pull": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/app", - "description": "Creates or updates an `.env` files that contains app and app extension environment variables.\n\n When an existing `.env` file is updated, changes to the variables are displayed in the terminal output. Existing variables and commented variables are preserved.", - "descriptionWithMarkdown": "Creates or updates an `.env` files that contains app and app extension environment variables.\n\n When an existing `.env` file is updated, changes to the variables are displayed in the terminal output. Existing variables and commented variables are preserved.", + "app:init": { + "aliases": [], + "args": {}, "flags": { - "client-id": { - "description": "The Client ID of your app.", - "env": "SHOPIFY_FLAG_CLIENT_ID", - "exclusive": [ - "config" - ], + "no-color": { + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", + "allowNo": false, + "type": "boolean" + }, + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", + "allowNo": false, + "type": "boolean" + }, + "name": { + "char": "n", + "description": "The name for the new app. When provided, skips the app selection prompt and creates a new app with this name.", + "env": "SHOPIFY_FLAG_NAME", + "hidden": false, + "name": "name", "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "path": { + "char": "p", + "env": "SHOPIFY_FLAG_PATH", "hidden": false, + "name": "path", + "default": "/Users/gasse/src/github.com/Shopify/cli/packages/cli", + "hasDynamicHelp": false, "multiple": false, - "name": "client-id", "type": "option" }, - "config": { - "char": "c", - "description": "The name of the app configuration.", - "env": "SHOPIFY_FLAG_APP_CONFIG", + "template": { + "description": "The app template. Accepts one of the following:\n - \n - Any GitHub repo with optional branch and subpath, e.g., https://github.com/Shopify//[subpath]#[branch]", + "env": "SHOPIFY_FLAG_TEMPLATE", + "name": "template", "hasDynamicHelp": false, - "hidden": false, "multiple": false, - "name": "config", "type": "option" }, - "env-file": { - "description": "Specify an environment file to update if the update flag is set", - "env": "SHOPIFY_FLAG_ENV_FILE", + "flavor": { + "description": "Which flavor of the given template to use.", + "env": "SHOPIFY_FLAG_TEMPLATE_FLAVOR", + "name": "flavor", "hasDynamicHelp": false, - "hidden": false, "multiple": false, - "name": "env-file", "type": "option" }, - "no-color": { - "allowNo": false, - "description": "Disable color output.", - "env": "SHOPIFY_FLAG_NO_COLOR", + "package-manager": { + "char": "d", + "env": "SHOPIFY_FLAG_PACKAGE_MANAGER", "hidden": false, - "name": "no-color", - "type": "boolean" - }, - "path": { - "description": "The path to your app directory.", - "env": "SHOPIFY_FLAG_PATH", + "name": "package-manager", "hasDynamicHelp": false, "multiple": false, - "name": "path", - "noCacheDefault": true, + "options": [ + "npm", + "yarn", + "pnpm", + "bun" + ], "type": "option" }, - "reset": { + "local": { + "char": "l", + "env": "SHOPIFY_FLAG_LOCAL", + "hidden": true, + "name": "local", "allowNo": false, - "description": "Reset all your settings.", - "env": "SHOPIFY_FLAG_RESET", + "type": "boolean" + }, + "client-id": { + "description": "The Client ID of your app. Use this to automatically link your new project to an existing app. Using this flag avoids the app selection prompt.", + "env": "SHOPIFY_FLAG_CLIENT_ID", "exclusive": [ "config" ], "hidden": false, - "name": "reset", - "type": "boolean" + "name": "client-id", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" }, - "verbose": { - "allowNo": false, - "description": "Increase the verbosity of the output.", - "env": "SHOPIFY_FLAG_VERBOSE", + "organization-id": { + "description": "The organization ID. Your organization ID can be found in your Dev Dashboard URL: https://dev.shopify.com/dashboard/", + "env": "SHOPIFY_FLAG_ORGANIZATION_ID", + "exclusive": [ + "client-id" + ], "hidden": false, - "name": "verbose", - "type": "boolean" + "name": "organization-id", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "app:env:pull", + "hiddenAliases": [], + "id": "app:init", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", "strict": true, - "summary": "Pull app and extensions environment variables." + "summary": "Create a new app project", + "customPluginName": "@shopify/app" }, - "app:env:show": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/app", - "description": "Displays environment variables that can be used to deploy apps and app extensions.", - "descriptionWithMarkdown": "Displays environment variables that can be used to deploy apps and app extensions.", + "app:config:validate": { + "aliases": [], + "args": {}, + "description": "Validates the selected app configuration file and all extension configurations against their schemas and reports any errors found.", "flags": { - "client-id": { - "description": "The Client ID of your app.", - "env": "SHOPIFY_FLAG_CLIENT_ID", - "exclusive": [ - "config" - ], - "hasDynamicHelp": false, + "no-color": { + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", + "allowNo": false, + "type": "boolean" + }, + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", "hidden": false, + "name": "verbose", + "allowNo": false, + "type": "boolean" + }, + "path": { + "description": "The path to your app directory.", + "env": "SHOPIFY_FLAG_PATH", + "name": "path", + "noCacheDefault": true, + "hasDynamicHelp": false, "multiple": false, - "name": "client-id", "type": "option" }, "config": { "char": "c", "description": "The name of the app configuration.", "env": "SHOPIFY_FLAG_APP_CONFIG", - "hasDynamicHelp": false, "hidden": false, - "multiple": false, "name": "config", + "hasDynamicHelp": false, + "multiple": false, "type": "option" }, - "no-color": { - "allowNo": false, - "description": "Disable color output.", - "env": "SHOPIFY_FLAG_NO_COLOR", + "client-id": { + "description": "The Client ID of your app.", + "env": "SHOPIFY_FLAG_CLIENT_ID", + "exclusive": [ + "config" + ], "hidden": false, - "name": "no-color", - "type": "boolean" - }, - "path": { - "description": "The path to your app directory.", - "env": "SHOPIFY_FLAG_PATH", + "name": "client-id", "hasDynamicHelp": false, "multiple": false, - "name": "path", - "noCacheDefault": true, "type": "option" }, "reset": { - "allowNo": false, "description": "Reset all your settings.", "env": "SHOPIFY_FLAG_RESET", "exclusive": [ @@ -1318,103 +1302,83 @@ ], "hidden": false, "name": "reset", + "allowNo": false, "type": "boolean" }, - "verbose": { - "allowNo": false, - "description": "Increase the verbosity of the output.", - "env": "SHOPIFY_FLAG_VERBOSE", + "json": { + "char": "j", + "description": "Output the result as JSON. Automatically disables color output.", + "env": "SHOPIFY_FLAG_JSON", "hidden": false, - "name": "verbose", + "name": "json", + "allowNo": false, "type": "boolean" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "app:env:show", + "hiddenAliases": [], + "id": "app:config:validate", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", "strict": true, - "summary": "Display app and extensions environment variables." + "summary": "Validate your app configuration and extensions.", + "descriptionWithMarkdown": "Validates the selected app configuration file and all extension configurations against their schemas and reports any errors found.", + "customPluginName": "@shopify/app" }, - "app:execute": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/app", - "description": "Executes an Admin API GraphQL query or mutation on the specified store. Mutations are only allowed on dev stores.\n\n For operations that process large amounts of data, use \"`bulk execute`\" (https://shopify.dev/docs/api/shopify-cli/app/app-bulk-execute) instead.", - "descriptionWithMarkdown": "Executes an Admin API GraphQL query or mutation on the specified store. Mutations are only allowed on dev stores.\n\n For operations that process large amounts of data, use [`bulk execute`](https://shopify.dev/docs/api/shopify-cli/app/app-bulk-execute) instead.", + "app:release": { + "aliases": [], + "args": {}, + "description": "Releases an existing app version. Pass the name of the version that you want to release using the `--version` flag.", "flags": { - "client-id": { - "description": "The Client ID of your app.", - "env": "SHOPIFY_FLAG_CLIENT_ID", - "exclusive": [ - "config" - ], - "hasDynamicHelp": false, + "no-color": { + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", + "allowNo": false, + "type": "boolean" + }, + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", "hidden": false, + "name": "verbose", + "allowNo": false, + "type": "boolean" + }, + "path": { + "description": "The path to your app directory.", + "env": "SHOPIFY_FLAG_PATH", + "name": "path", + "noCacheDefault": true, + "hasDynamicHelp": false, "multiple": false, - "name": "client-id", "type": "option" }, "config": { "char": "c", "description": "The name of the app configuration.", "env": "SHOPIFY_FLAG_APP_CONFIG", - "hasDynamicHelp": false, "hidden": false, - "multiple": false, "name": "config", - "type": "option" - }, - "no-color": { - "allowNo": false, - "description": "Disable color output.", - "env": "SHOPIFY_FLAG_NO_COLOR", - "hidden": false, - "name": "no-color", - "type": "boolean" - }, - "output-file": { - "description": "The file name where results should be written, instead of STDOUT.", - "env": "SHOPIFY_FLAG_OUTPUT_FILE", - "hasDynamicHelp": false, - "multiple": false, - "name": "output-file", - "type": "option" - }, - "path": { - "description": "The path to your app directory.", - "env": "SHOPIFY_FLAG_PATH", - "hasDynamicHelp": false, - "multiple": false, - "name": "path", - "noCacheDefault": true, - "type": "option" - }, - "query": { - "char": "q", - "description": "The GraphQL query or mutation, as a string.", - "env": "SHOPIFY_FLAG_QUERY", "hasDynamicHelp": false, "multiple": false, - "name": "query", - "required": false, "type": "option" }, - "query-file": { - "description": "Path to a file containing the GraphQL query or mutation. Can't be used with --query.", - "env": "SHOPIFY_FLAG_QUERY_FILE", + "client-id": { + "description": "The Client ID of your app.", + "env": "SHOPIFY_FLAG_CLIENT_ID", + "exclusive": [ + "config" + ], + "hidden": false, + "name": "client-id", "hasDynamicHelp": false, "multiple": false, - "name": "query-file", "type": "option" }, "reset": { - "allowNo": false, "description": "Reset all your settings.", "env": "SHOPIFY_FLAG_RESET", "exclusive": [ @@ -1422,118 +1386,110 @@ ], "hidden": false, "name": "reset", + "allowNo": false, "type": "boolean" }, - "store": { - "char": "s", - "description": "The myshopify.com domain of the store to execute against. The app must be installed on the store. If not specified, you will be prompted to select a store.", - "env": "SHOPIFY_FLAG_STORE", - "hasDynamicHelp": false, - "multiple": false, - "name": "store", - "type": "option" - }, - "variable-file": { - "description": "Path to a file containing GraphQL variables in JSON format. Can't be used with --variables.", - "env": "SHOPIFY_FLAG_VARIABLE_FILE", - "exclusive": [ - "variables" - ], - "hasDynamicHelp": false, - "multiple": false, - "name": "variable-file", - "type": "option" - }, - "variables": { - "char": "v", - "description": "The values for any GraphQL variables in your query or mutation, in JSON format.", - "env": "SHOPIFY_FLAG_VARIABLES", - "exclusive": [ - "variable-file" - ], - "hasDynamicHelp": false, - "multiple": false, - "name": "variables", - "type": "option" + "force": { + "char": "f", + "description": "[Deprecated] Release without asking for confirmation. Equivalent to --allow-updates --allow-deletes. Use --allow-updates for CI/CD environments instead.", + "env": "SHOPIFY_FLAG_FORCE", + "hidden": false, + "name": "force", + "allowNo": false, + "type": "boolean" }, - "verbose": { + "allow-updates": { + "description": "Allows adding and updating extensions and configuration without requiring user confirmation. Recommended option for CI/CD environments.", + "env": "SHOPIFY_FLAG_ALLOW_UPDATES", + "hidden": false, + "name": "allow-updates", "allowNo": false, - "description": "Increase the verbosity of the output.", - "env": "SHOPIFY_FLAG_VERBOSE", + "type": "boolean" + }, + "allow-deletes": { + "description": "Allows removing extensions and configuration without requiring user confirmation. For CI/CD environments, the recommended flag is --allow-updates.", + "env": "SHOPIFY_FLAG_ALLOW_DELETES", "hidden": false, - "name": "verbose", + "name": "allow-deletes", + "allowNo": false, "type": "boolean" }, "version": { - "description": "The API version to use for the query or mutation. Defaults to the latest stable version.", + "description": "The name of the app version to release.", "env": "SHOPIFY_FLAG_VERSION", + "hidden": false, + "name": "version", + "required": true, "hasDynamicHelp": false, "multiple": false, - "name": "version", "type": "option" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "app:execute", + "hiddenAliases": [], + "id": "app:release", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", "strict": true, - "summary": "Execute GraphQL queries and mutations." + "summary": "Release an app version.", + "usage": "app release --version ", + "descriptionWithMarkdown": "Releases an existing app version. Pass the name of the version that you want to release using the `--version` flag.", + "customPluginName": "@shopify/app" }, - "app:function:build": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/app", - "description": "Compiles the function in your current directory to WebAssembly (Wasm) for testing purposes.", - "descriptionWithMarkdown": "Compiles the function in your current directory to WebAssembly (Wasm) for testing purposes.", + "app:config:link": { + "aliases": [], + "args": {}, + "description": "Pulls app configuration from the Developer Dashboard and creates or overwrites a configuration file. You can create a new app with this command to start with a default configuration file.\n\n For more information on the format of the created TOML configuration file, refer to the \"App configuration\" (https://shopify.dev/docs/apps/tools/cli/configuration) page.\n ", "flags": { - "client-id": { - "description": "The Client ID of your app.", - "env": "SHOPIFY_FLAG_CLIENT_ID", - "exclusive": [ - "config" - ], - "hasDynamicHelp": false, + "no-color": { + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", + "allowNo": false, + "type": "boolean" + }, + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", "hidden": false, + "name": "verbose", + "allowNo": false, + "type": "boolean" + }, + "path": { + "description": "The path to your app directory.", + "env": "SHOPIFY_FLAG_PATH", + "name": "path", + "noCacheDefault": true, + "hasDynamicHelp": false, "multiple": false, - "name": "client-id", "type": "option" }, "config": { "char": "c", "description": "The name of the app configuration.", "env": "SHOPIFY_FLAG_APP_CONFIG", - "hasDynamicHelp": false, "hidden": false, - "multiple": false, "name": "config", + "hasDynamicHelp": false, + "multiple": false, "type": "option" }, - "no-color": { - "allowNo": false, - "description": "Disable color output.", - "env": "SHOPIFY_FLAG_NO_COLOR", + "client-id": { + "description": "The Client ID of your app.", + "env": "SHOPIFY_FLAG_CLIENT_ID", + "exclusive": [ + "config" + ], "hidden": false, - "name": "no-color", - "type": "boolean" - }, - "path": { - "description": "The path to your function directory.", - "env": "SHOPIFY_FLAG_PATH", + "name": "client-id", "hasDynamicHelp": false, - "hidden": false, "multiple": false, - "name": "path", - "noCacheDefault": true, "type": "option" }, "reset": { - "allowNo": false, "description": "Reset all your settings.", "env": "SHOPIFY_FLAG_RESET", "exclusive": [ @@ -1541,184 +1497,145 @@ ], "hidden": false, "name": "reset", - "type": "boolean" - }, - "verbose": { "allowNo": false, - "description": "Increase the verbosity of the output.", - "env": "SHOPIFY_FLAG_VERBOSE", - "hidden": false, - "name": "verbose", "type": "boolean" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "app:function:build", + "hiddenAliases": [], + "id": "app:config:link", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", "strict": true, - "summary": "Compile a function to wasm." + "summary": "Fetch your app configuration from the Developer Dashboard.", + "descriptionWithMarkdown": "Pulls app configuration from the Developer Dashboard and creates or overwrites a configuration file. You can create a new app with this command to start with a default configuration file.\n\n For more information on the format of the created TOML configuration file, refer to the [App configuration](https://shopify.dev/docs/apps/tools/cli/configuration) page.\n ", + "customPluginName": "@shopify/app" }, - "app:function:info": { - "aliases": [ - ], + "app:config:use": { + "aliases": [], "args": { + "config": { + "description": "The name of the app configuration. Can be 'shopify.app.staging.toml' or simply 'staging'.", + "name": "config" + } }, - "customPluginName": "@shopify/app", - "description": "The information returned includes the following:\n\n - The function handle\n - The function name\n - The function API version\n - The targeting configuration\n - The schema path\n - The WASM path\n - The function runner path", - "descriptionWithMarkdown": "The information returned includes the following:\n\n - The function handle\n - The function name\n - The function API version\n - The targeting configuration\n - The schema path\n - The WASM path\n - The function runner path", + "description": "Sets default configuration when you run app-related CLI commands. If you omit the `config-name` parameter, then you'll be prompted to choose from the configuration files in your project.", "flags": { - "client-id": { - "description": "The Client ID of your app.", - "env": "SHOPIFY_FLAG_CLIENT_ID", - "exclusive": [ - "config" - ], - "hasDynamicHelp": false, - "hidden": false, - "multiple": false, - "name": "client-id", - "type": "option" - }, - "config": { - "char": "c", - "description": "The name of the app configuration.", - "env": "SHOPIFY_FLAG_APP_CONFIG", - "hasDynamicHelp": false, - "hidden": false, - "multiple": false, - "name": "config", - "type": "option" - }, - "json": { - "allowNo": false, - "char": "j", - "description": "Output the result as JSON. Automatically disables color output.", - "env": "SHOPIFY_FLAG_JSON", - "hidden": false, - "name": "json", - "type": "boolean" - }, "no-color": { - "allowNo": false, "description": "Disable color output.", "env": "SHOPIFY_FLAG_NO_COLOR", "hidden": false, "name": "no-color", + "allowNo": false, + "type": "boolean" + }, + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", + "allowNo": false, "type": "boolean" }, "path": { - "description": "The path to your function directory.", + "description": "The path to your app directory.", "env": "SHOPIFY_FLAG_PATH", - "hasDynamicHelp": false, - "hidden": false, - "multiple": false, "name": "path", "noCacheDefault": true, + "hasDynamicHelp": false, + "multiple": false, "type": "option" }, - "reset": { - "allowNo": false, - "description": "Reset all your settings.", - "env": "SHOPIFY_FLAG_RESET", + "client-id": { + "description": "The Client ID of your app.", + "env": "SHOPIFY_FLAG_CLIENT_ID", "exclusive": [ "config" ], "hidden": false, - "name": "reset", - "type": "boolean" + "name": "client-id", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" }, - "verbose": { - "allowNo": false, - "description": "Increase the verbosity of the output.", - "env": "SHOPIFY_FLAG_VERBOSE", + "reset": { + "description": "Reset all your settings.", + "env": "SHOPIFY_FLAG_RESET", + "exclusive": [ + "config" + ], "hidden": false, - "name": "verbose", + "name": "reset", + "allowNo": false, "type": "boolean" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "app:function:info", + "hiddenAliases": [], + "id": "app:config:use", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", "strict": true, - "summary": "Print basic information about your function." + "summary": "Activate an app configuration.", + "usage": "app config use [config] [flags]", + "descriptionWithMarkdown": "Sets default configuration when you run app-related CLI commands. If you omit the `config-name` parameter, then you'll be prompted to choose from the configuration files in your project.", + "customPluginName": "@shopify/app" }, - "app:function:replay": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/app", - "description": "Runs the function from your current directory for \"testing purposes\" (https://shopify.dev/docs/apps/functions/testing-and-debugging). To learn how you can monitor and debug functions when errors occur, refer to \"Shopify Functions error handling\" (https://shopify.dev/docs/api/functions/errors).", - "descriptionWithMarkdown": "Runs the function from your current directory for [testing purposes](https://shopify.dev/docs/apps/functions/testing-and-debugging). To learn how you can monitor and debug functions when errors occur, refer to [Shopify Functions error handling](https://shopify.dev/docs/api/functions/errors).", + "app:config:pull": { + "aliases": [], + "args": {}, + "description": "Pulls the latest configuration from the already-linked Shopify app and updates the selected configuration file.\n\nThis command reuses the existing linked app and organization and skips all interactive prompts. Use `--config` to target a specific configuration file, or omit it to use the default one.", "flags": { - "client-id": { - "description": "The Client ID of your app.", - "env": "SHOPIFY_FLAG_CLIENT_ID", - "exclusive": [ - "config" - ], - "hasDynamicHelp": false, + "no-color": { + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", + "allowNo": false, + "type": "boolean" + }, + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", "hidden": false, + "name": "verbose", + "allowNo": false, + "type": "boolean" + }, + "path": { + "description": "The path to your app directory.", + "env": "SHOPIFY_FLAG_PATH", + "name": "path", + "noCacheDefault": true, + "hasDynamicHelp": false, "multiple": false, - "name": "client-id", "type": "option" }, "config": { "char": "c", "description": "The name of the app configuration.", "env": "SHOPIFY_FLAG_APP_CONFIG", - "hasDynamicHelp": false, "hidden": false, - "multiple": false, "name": "config", - "type": "option" - }, - "json": { - "allowNo": false, - "char": "j", - "description": "Output the result as JSON. Automatically disables color output.", - "env": "SHOPIFY_FLAG_JSON", - "hidden": false, - "name": "json", - "type": "boolean" - }, - "log": { - "char": "l", - "description": "Specifies a log identifier to replay instead of selecting from a list. The identifier is provided in the output of `shopify app dev` and is the suffix of the log file name.", - "env": "SHOPIFY_FLAG_LOG", "hasDynamicHelp": false, "multiple": false, - "name": "log", "type": "option" }, - "no-color": { - "allowNo": false, - "description": "Disable color output.", - "env": "SHOPIFY_FLAG_NO_COLOR", + "client-id": { + "description": "The Client ID of your app.", + "env": "SHOPIFY_FLAG_CLIENT_ID", + "exclusive": [ + "config" + ], "hidden": false, - "name": "no-color", - "type": "boolean" - }, - "path": { - "description": "The path to your function directory.", - "env": "SHOPIFY_FLAG_PATH", + "name": "client-id", "hasDynamicHelp": false, - "hidden": false, "multiple": false, - "name": "path", - "noCacheDefault": true, "type": "option" }, "reset": { - "allowNo": false, "description": "Reset all your settings.", "env": "SHOPIFY_FLAG_RESET", "exclusive": [ @@ -1726,115 +1643,158 @@ ], "hidden": false, "name": "reset", - "type": "boolean" - }, - "verbose": { "allowNo": false, - "description": "Increase the verbosity of the output.", - "env": "SHOPIFY_FLAG_VERBOSE", - "hidden": false, - "name": "verbose", - "type": "boolean" - }, - "watch": { - "allowNo": true, - "char": "w", - "description": "Re-run the function when the source code changes.", - "env": "SHOPIFY_FLAG_WATCH", - "hidden": false, - "name": "watch", "type": "boolean" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "app:function:replay", + "hiddenAliases": [], + "id": "app:config:pull", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", "strict": true, - "summary": "Replays a function run from an app log." + "summary": "Refresh an already-linked app configuration without prompts.", + "descriptionWithMarkdown": "Pulls the latest configuration from the already-linked Shopify app and updates the selected configuration file.\n\nThis command reuses the existing linked app and organization and skips all interactive prompts. Use `--config` to target a specific configuration file, or omit it to use the default one.", + "customPluginName": "@shopify/app" }, - "app:function:run": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/app", - "description": "Runs the function from your current directory for \"testing purposes\" (https://shopify.dev/docs/apps/functions/testing-and-debugging). To learn how you can monitor and debug functions when errors occur, refer to \"Shopify Functions error handling\" (https://shopify.dev/docs/api/functions/errors).", - "descriptionWithMarkdown": "Runs the function from your current directory for [testing purposes](https://shopify.dev/docs/apps/functions/testing-and-debugging). To learn how you can monitor and debug functions when errors occur, refer to [Shopify Functions error handling](https://shopify.dev/docs/api/functions/errors).", + "app:env:pull": { + "aliases": [], + "args": {}, + "description": "Creates or updates an `.env` files that contains app and app extension environment variables.\n\n When an existing `.env` file is updated, changes to the variables are displayed in the terminal output. Existing variables and commented variables are preserved.", "flags": { - "client-id": { - "description": "The Client ID of your app.", - "env": "SHOPIFY_FLAG_CLIENT_ID", - "exclusive": [ - "config" - ], - "hasDynamicHelp": false, + "no-color": { + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", + "allowNo": false, + "type": "boolean" + }, + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", "hidden": false, + "name": "verbose", + "allowNo": false, + "type": "boolean" + }, + "path": { + "description": "The path to your app directory.", + "env": "SHOPIFY_FLAG_PATH", + "name": "path", + "noCacheDefault": true, + "hasDynamicHelp": false, "multiple": false, - "name": "client-id", "type": "option" }, "config": { "char": "c", "description": "The name of the app configuration.", "env": "SHOPIFY_FLAG_APP_CONFIG", - "hasDynamicHelp": false, "hidden": false, - "multiple": false, "name": "config", - "type": "option" - }, - "export": { - "char": "e", - "description": "Name of the WebAssembly export to invoke.", - "env": "SHOPIFY_FLAG_EXPORT", "hasDynamicHelp": false, - "hidden": false, "multiple": false, - "name": "export", "type": "option" }, - "input": { - "char": "i", - "description": "The input JSON to pass to the function. If omitted, standard input is used.", - "env": "SHOPIFY_FLAG_INPUT", + "client-id": { + "description": "The Client ID of your app.", + "env": "SHOPIFY_FLAG_CLIENT_ID", + "exclusive": [ + "config" + ], + "hidden": false, + "name": "client-id", "hasDynamicHelp": false, "multiple": false, - "name": "input", "type": "option" }, - "json": { - "allowNo": false, - "char": "j", - "description": "Output the result as JSON. Automatically disables color output.", - "env": "SHOPIFY_FLAG_JSON", + "reset": { + "description": "Reset all your settings.", + "env": "SHOPIFY_FLAG_RESET", + "exclusive": [ + "config" + ], "hidden": false, - "name": "json", + "name": "reset", + "allowNo": false, "type": "boolean" }, + "env-file": { + "description": "Specify an environment file to update if the update flag is set", + "env": "SHOPIFY_FLAG_ENV_FILE", + "hidden": false, + "name": "env-file", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + } + }, + "hasDynamicHelp": false, + "hiddenAliases": [], + "id": "app:env:pull", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true, + "summary": "Pull app and extensions environment variables.", + "descriptionWithMarkdown": "Creates or updates an `.env` files that contains app and app extension environment variables.\n\n When an existing `.env` file is updated, changes to the variables are displayed in the terminal output. Existing variables and commented variables are preserved.", + "customPluginName": "@shopify/app" + }, + "app:env:show": { + "aliases": [], + "args": {}, + "description": "Displays environment variables that can be used to deploy apps and app extensions.", + "flags": { "no-color": { - "allowNo": false, "description": "Disable color output.", "env": "SHOPIFY_FLAG_NO_COLOR", "hidden": false, "name": "no-color", + "allowNo": false, + "type": "boolean" + }, + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", + "allowNo": false, "type": "boolean" }, "path": { - "description": "The path to your function directory.", + "description": "The path to your app directory.", "env": "SHOPIFY_FLAG_PATH", + "name": "path", + "noCacheDefault": true, + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "config": { + "char": "c", + "description": "The name of the app configuration.", + "env": "SHOPIFY_FLAG_APP_CONFIG", + "hidden": false, + "name": "config", "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "client-id": { + "description": "The Client ID of your app.", + "env": "SHOPIFY_FLAG_CLIENT_ID", + "exclusive": [ + "config" + ], "hidden": false, + "name": "client-id", + "hasDynamicHelp": false, "multiple": false, - "name": "path", - "noCacheDefault": true, "type": "option" }, "reset": { - "allowNo": false, "description": "Reset all your settings.", "env": "SHOPIFY_FLAG_RESET", "exclusive": [ @@ -1842,165 +1802,74 @@ ], "hidden": false, "name": "reset", - "type": "boolean" - }, - "verbose": { "allowNo": false, - "description": "Increase the verbosity of the output.", - "env": "SHOPIFY_FLAG_VERBOSE", - "hidden": false, - "name": "verbose", "type": "boolean" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "app:function:run", + "hiddenAliases": [], + "id": "app:env:show", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", "strict": true, - "summary": "Run a function locally for testing." + "summary": "Display app and extensions environment variables.", + "descriptionWithMarkdown": "Displays environment variables that can be used to deploy apps and app extensions.", + "customPluginName": "@shopify/app" }, - "app:function:schema": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/app", - "description": "Generates the latest \"GraphQL schema\" (https://shopify.dev/docs/apps/functions/input-output#graphql-schema) for a function in your app. Run this command from the function directory.\n\n This command uses the API type and version of your function, as defined in your extension TOML file, to generate the latest GraphQL schema. The schema is written to the `schema.graphql` file.", - "descriptionWithMarkdown": "Generates the latest [GraphQL schema](https://shopify.dev/docs/apps/functions/input-output#graphql-schema) for a function in your app. Run this command from the function directory.\n\n This command uses the API type and version of your function, as defined in your extension TOML file, to generate the latest GraphQL schema. The schema is written to the `schema.graphql` file.", + "app:execute": { + "aliases": [], + "args": {}, + "description": "Executes an Admin API GraphQL query or mutation on the specified store. Mutations are only allowed on dev stores.\n\n For operations that process large amounts of data, use \"`bulk execute`\" (https://shopify.dev/docs/api/shopify-cli/app/app-bulk-execute) instead.", "flags": { - "client-id": { - "description": "The Client ID of your app.", - "env": "SHOPIFY_FLAG_CLIENT_ID", - "exclusive": [ - "config" - ], - "hasDynamicHelp": false, - "hidden": false, - "multiple": false, - "name": "client-id", - "type": "option" - }, - "config": { - "char": "c", - "description": "The name of the app configuration.", - "env": "SHOPIFY_FLAG_APP_CONFIG", - "hasDynamicHelp": false, - "hidden": false, - "multiple": false, - "name": "config", - "type": "option" - }, "no-color": { - "allowNo": false, "description": "Disable color output.", "env": "SHOPIFY_FLAG_NO_COLOR", "hidden": false, "name": "no-color", - "type": "boolean" - }, - "path": { - "description": "The path to your function directory.", - "env": "SHOPIFY_FLAG_PATH", - "hasDynamicHelp": false, - "hidden": false, - "multiple": false, - "name": "path", - "noCacheDefault": true, - "type": "option" - }, - "reset": { - "allowNo": false, - "description": "Reset all your settings.", - "env": "SHOPIFY_FLAG_RESET", - "exclusive": [ - "config" - ], - "hidden": false, - "name": "reset", - "type": "boolean" - }, - "stdout": { "allowNo": false, - "description": "Output the schema to stdout instead of writing to a file.", - "env": "SHOPIFY_FLAG_STDOUT", - "name": "stdout", - "required": false, "type": "boolean" }, "verbose": { - "allowNo": false, "description": "Increase the verbosity of the output.", "env": "SHOPIFY_FLAG_VERBOSE", "hidden": false, "name": "verbose", + "allowNo": false, "type": "boolean" - } - }, - "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "app:function:schema", - "pluginAlias": "@shopify/cli", - "pluginName": "@shopify/cli", - "pluginType": "core", - "strict": true, - "summary": "Fetch the latest GraphQL schema for a function." - }, - "app:function:typegen": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/app", - "description": "Creates GraphQL types based on your \"input query\" (https://shopify.dev/docs/apps/functions/input-output#input) for a function. Supports JavaScript functions out of the box, or any language via the `build.typegen_command` configuration.", - "descriptionWithMarkdown": "Creates GraphQL types based on your [input query](https://shopify.dev/docs/apps/functions/input-output#input) for a function. Supports JavaScript functions out of the box, or any language via the `build.typegen_command` configuration.", - "flags": { - "client-id": { - "description": "The Client ID of your app.", - "env": "SHOPIFY_FLAG_CLIENT_ID", - "exclusive": [ - "config" - ], + }, + "path": { + "description": "The path to your app directory.", + "env": "SHOPIFY_FLAG_PATH", + "name": "path", + "noCacheDefault": true, "hasDynamicHelp": false, - "hidden": false, "multiple": false, - "name": "client-id", "type": "option" }, "config": { "char": "c", "description": "The name of the app configuration.", "env": "SHOPIFY_FLAG_APP_CONFIG", - "hasDynamicHelp": false, "hidden": false, - "multiple": false, "name": "config", + "hasDynamicHelp": false, + "multiple": false, "type": "option" }, - "no-color": { - "allowNo": false, - "description": "Disable color output.", - "env": "SHOPIFY_FLAG_NO_COLOR", + "client-id": { + "description": "The Client ID of your app.", + "env": "SHOPIFY_FLAG_CLIENT_ID", + "exclusive": [ + "config" + ], "hidden": false, - "name": "no-color", - "type": "boolean" - }, - "path": { - "description": "The path to your function directory.", - "env": "SHOPIFY_FLAG_PATH", + "name": "client-id", "hasDynamicHelp": false, - "hidden": false, "multiple": false, - "name": "path", - "noCacheDefault": true, "type": "option" }, "reset": { - "allowNo": false, "description": "Reset all your settings.", "env": "SHOPIFY_FLAG_RESET", "exclusive": [ @@ -2008,295 +1877,292 @@ ], "hidden": false, "name": "reset", - "type": "boolean" - }, - "verbose": { "allowNo": false, - "description": "Increase the verbosity of the output.", - "env": "SHOPIFY_FLAG_VERBOSE", - "hidden": false, - "name": "verbose", "type": "boolean" - } - }, - "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "app:function:typegen", - "pluginAlias": "@shopify/cli", - "pluginName": "@shopify/cli", - "pluginType": "core", - "strict": true, - "summary": "Generate GraphQL types for a function." - }, - "app:generate:extension": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/app", - "description": "Generates a new \"app extension\" (https://shopify.dev/docs/apps/build/app-extensions). For a list of app extensions that you can generate using this command, refer to \"Supported extensions\" (https://shopify.dev/docs/apps/build/app-extensions/list-of-app-extensions).\n\n Each new app extension is created in a folder under `extensions/`. To learn more about the extensions file structure, refer to \"App structure\" (https://shopify.dev/docs/apps/build/cli-for-apps/app-structure) and the documentation for your extension.\n ", - "descriptionWithMarkdown": "Generates a new [app extension](https://shopify.dev/docs/apps/build/app-extensions). For a list of app extensions that you can generate using this command, refer to [Supported extensions](https://shopify.dev/docs/apps/build/app-extensions/list-of-app-extensions).\n\n Each new app extension is created in a folder under `extensions/`. To learn more about the extensions file structure, refer to [App structure](https://shopify.dev/docs/apps/build/cli-for-apps/app-structure) and the documentation for your extension.\n ", - "flags": { - "client-id": { - "description": "The Client ID of your app.", - "env": "SHOPIFY_FLAG_CLIENT_ID", + }, + "query": { + "char": "q", + "description": "The GraphQL query or mutation, as a string.", + "env": "SHOPIFY_FLAG_QUERY", + "name": "query", + "required": false, + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "query-file": { + "description": "Path to a file containing the GraphQL query or mutation. Can't be used with --query.", + "env": "SHOPIFY_FLAG_QUERY_FILE", + "name": "query-file", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "variables": { + "char": "v", + "description": "The values for any GraphQL variables in your query or mutation, in JSON format.", + "env": "SHOPIFY_FLAG_VARIABLES", "exclusive": [ - "config" + "variable-file" ], + "name": "variables", "hasDynamicHelp": false, - "hidden": false, "multiple": false, - "name": "client-id", "type": "option" }, - "clone-url": { - "char": "u", - "description": "The Git URL to clone the function extensions templates from. Defaults to: https://github.com/Shopify/function-examples", - "env": "SHOPIFY_FLAG_CLONE_URL", + "variable-file": { + "description": "Path to a file containing GraphQL variables in JSON format. Can't be used with --variables.", + "env": "SHOPIFY_FLAG_VARIABLE_FILE", + "exclusive": [ + "variables" + ], + "name": "variable-file", "hasDynamicHelp": false, - "hidden": true, "multiple": false, - "name": "clone-url", "type": "option" }, - "config": { - "char": "c", - "description": "The name of the app configuration.", - "env": "SHOPIFY_FLAG_APP_CONFIG", + "store": { + "char": "s", + "description": "The myshopify.com domain of the store to execute against. The app must be installed on the store. If not specified, you will be prompted to select a store.", + "env": "SHOPIFY_FLAG_STORE", + "name": "store", "hasDynamicHelp": false, - "hidden": false, "multiple": false, - "name": "config", "type": "option" }, - "flavor": { - "description": "Choose a starting template for your extension, where applicable", - "env": "SHOPIFY_FLAG_FLAVOR", + "version": { + "description": "The API version to use for the query or mutation. Defaults to the latest stable version.", + "env": "SHOPIFY_FLAG_VERSION", + "name": "version", "hasDynamicHelp": false, - "hidden": false, "multiple": false, - "name": "flavor", - "options": [ - "vanilla-js", - "react", - "typescript", - "typescript-react", - "wasm", - "rust" - ], "type": "option" }, - "name": { - "char": "n", - "description": "name of your Extension", - "env": "SHOPIFY_FLAG_NAME", + "output-file": { + "description": "The file name where results should be written, instead of STDOUT.", + "env": "SHOPIFY_FLAG_OUTPUT_FILE", + "name": "output-file", "hasDynamicHelp": false, - "hidden": false, "multiple": false, - "name": "name", "type": "option" - }, + } + }, + "hasDynamicHelp": false, + "hiddenAliases": [], + "id": "app:execute", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true, + "summary": "Execute GraphQL queries and mutations.", + "descriptionWithMarkdown": "Executes an Admin API GraphQL query or mutation on the specified store. Mutations are only allowed on dev stores.\n\n For operations that process large amounts of data, use [`bulk execute`](https://shopify.dev/docs/api/shopify-cli/app/app-bulk-execute) instead.", + "customPluginName": "@shopify/app" + }, + "app:bulk:execute": { + "aliases": [], + "args": {}, + "description": "Executes an Admin API GraphQL query or mutation on the specified store, as a bulk operation. Mutations are only allowed on dev stores.\n\n Bulk operations allow you to process large amounts of data asynchronously. Learn more about \"bulk query operations\" (https://shopify.dev/docs/api/usage/bulk-operations/queries) and \"bulk mutation operations\" (https://shopify.dev/docs/api/usage/bulk-operations/imports).\n\n Use \"`bulk status`\" (https://shopify.dev/docs/api/shopify-cli/app/app-bulk-status) to check the status of your bulk operations.", + "flags": { "no-color": { - "allowNo": false, "description": "Disable color output.", "env": "SHOPIFY_FLAG_NO_COLOR", "hidden": false, "name": "no-color", + "allowNo": false, + "type": "boolean" + }, + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", + "allowNo": false, "type": "boolean" }, "path": { "description": "The path to your app directory.", "env": "SHOPIFY_FLAG_PATH", - "hasDynamicHelp": false, - "multiple": false, "name": "path", "noCacheDefault": true, + "hasDynamicHelp": false, + "multiple": false, "type": "option" }, - "reset": { - "allowNo": false, - "description": "Reset all your settings.", - "env": "SHOPIFY_FLAG_RESET", - "exclusive": [ - "config" - ], + "config": { + "char": "c", + "description": "The name of the app configuration.", + "env": "SHOPIFY_FLAG_APP_CONFIG", "hidden": false, - "name": "reset", - "type": "boolean" - }, - "template": { - "char": "t", - "description": "Extension template", - "env": "SHOPIFY_FLAG_EXTENSION_TEMPLATE", + "name": "config", "hasDynamicHelp": false, - "hidden": false, "multiple": false, - "name": "template", "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": "app:generate:extension", - "pluginAlias": "@shopify/cli", - "pluginName": "@shopify/cli", - "pluginType": "core", - "strict": true, - "summary": "Generate a new app Extension." - }, - "app:import-custom-data-definitions": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/app", - "description": "Import metafield and metaobject definitions from your development store. \"Read more about declarative custom data definitions\" (https://shopify.dev/docs/apps/build/custom-data/declarative-custom-data-definitions).", - "descriptionWithMarkdown": "Import metafield and metaobject definitions from your development store. [Read more about declarative custom data definitions](https://shopify.dev/docs/apps/build/custom-data/declarative-custom-data-definitions).", - "flags": { "client-id": { "description": "The Client ID of your app.", "env": "SHOPIFY_FLAG_CLIENT_ID", "exclusive": [ "config" ], - "hasDynamicHelp": false, "hidden": false, - "multiple": false, "name": "client-id", - "type": "option" - }, - "config": { - "char": "c", - "description": "The name of the app configuration.", - "env": "SHOPIFY_FLAG_APP_CONFIG", "hasDynamicHelp": false, - "hidden": false, "multiple": false, - "name": "config", "type": "option" }, - "include-existing": { + "reset": { + "description": "Reset all your settings.", + "env": "SHOPIFY_FLAG_RESET", + "exclusive": [ + "config" + ], + "hidden": false, + "name": "reset", "allowNo": false, - "description": "Include existing declared definitions in the output.", - "env": "SHOPIFY_FLAG_INCLUDE_EXISTING", - "name": "include-existing", "type": "boolean" }, - "no-color": { - "allowNo": false, - "description": "Disable color output.", - "env": "SHOPIFY_FLAG_NO_COLOR", - "hidden": false, - "name": "no-color", - "type": "boolean" + "query": { + "char": "q", + "description": "The GraphQL query or mutation to run as a bulk operation.", + "env": "SHOPIFY_FLAG_QUERY", + "name": "query", + "required": false, + "hasDynamicHelp": false, + "multiple": false, + "type": "option" }, - "path": { - "description": "The path to your app directory.", - "env": "SHOPIFY_FLAG_PATH", + "query-file": { + "description": "Path to a file containing the GraphQL query or mutation. Can't be used with --query.", + "env": "SHOPIFY_FLAG_QUERY_FILE", + "name": "query-file", "hasDynamicHelp": false, "multiple": false, - "name": "path", - "noCacheDefault": true, "type": "option" }, - "reset": { - "allowNo": false, - "description": "Reset all your settings.", - "env": "SHOPIFY_FLAG_RESET", + "variables": { + "char": "v", + "description": "The values for any GraphQL variables in your mutation, in JSON format. Can be specified multiple times.", + "env": "SHOPIFY_FLAG_VARIABLES", "exclusive": [ - "config" + "variable-file" ], - "hidden": false, - "name": "reset", - "type": "boolean" + "name": "variables", + "hasDynamicHelp": false, + "multiple": true, + "type": "option" + }, + "variable-file": { + "description": "Path to a file containing GraphQL variables in JSONL format (one JSON object per line). Can't be used with --variables.", + "env": "SHOPIFY_FLAG_VARIABLE_FILE", + "exclusive": [ + "variables" + ], + "name": "variable-file", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" }, "store": { "char": "s", - "description": "Store URL. Must be an existing development or Shopify Plus sandbox store.", + "description": "The store domain. Must be an existing dev store.", "env": "SHOPIFY_FLAG_STORE", + "name": "store", "hasDynamicHelp": false, "multiple": false, - "name": "store", "type": "option" }, - "verbose": { + "watch": { + "description": "Wait for bulk operation results before exiting. Defaults to false.", + "env": "SHOPIFY_FLAG_WATCH", + "name": "watch", "allowNo": false, - "description": "Increase the verbosity of the output.", - "env": "SHOPIFY_FLAG_VERBOSE", - "hidden": false, - "name": "verbose", "type": "boolean" + }, + "output-file": { + "dependsOn": [ + "watch" + ], + "description": "The file path where results should be written if --watch is specified. If not specified, results will be written to STDOUT.", + "env": "SHOPIFY_FLAG_OUTPUT_FILE", + "name": "output-file", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "version": { + "description": "The API version to use for the bulk operation. If not specified, uses the latest stable version.", + "env": "SHOPIFY_FLAG_VERSION", + "name": "version", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "app:import-custom-data-definitions", + "hiddenAliases": [], + "id": "app:bulk:execute", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", "strict": true, - "summary": "Import metafield and metaobject definitions." + "summary": "Execute bulk operations.", + "descriptionWithMarkdown": "Executes an Admin API GraphQL query or mutation on the specified store, as a bulk operation. Mutations are only allowed on dev stores.\n\n Bulk operations allow you to process large amounts of data asynchronously. Learn more about [bulk query operations](https://shopify.dev/docs/api/usage/bulk-operations/queries) and [bulk mutation operations](https://shopify.dev/docs/api/usage/bulk-operations/imports).\n\n Use [`bulk status`](https://shopify.dev/docs/api/shopify-cli/app/app-bulk-status) to check the status of your bulk operations.", + "customPluginName": "@shopify/app" }, - "app:import-extensions": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/app", - "description": "Import dashboard-managed extensions into your app.", + "app:generate:schema": { + "aliases": [], + "args": {}, + "description": "\"DEPRECATED, use `app function schema`] Generates the latest [GraphQL schema\" (https://shopify.dev/docs/apps/functions/input-output#graphql-schema) for a function in your app. Run this command from the function directory.\n\n This command uses the API type and version of your function, as defined in your extension TOML file, to generate the latest GraphQL schema. The schema is written to the `schema.graphql` file.", "flags": { - "client-id": { - "description": "The Client ID of your app.", - "env": "SHOPIFY_FLAG_CLIENT_ID", - "exclusive": [ - "config" - ], - "hasDynamicHelp": false, + "no-color": { + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", + "allowNo": false, + "type": "boolean" + }, + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", + "allowNo": false, + "type": "boolean" + }, + "path": { + "description": "The path to your function directory.", + "env": "SHOPIFY_FLAG_PATH", "hidden": false, + "name": "path", + "noCacheDefault": true, + "hasDynamicHelp": false, "multiple": false, - "name": "client-id", "type": "option" }, "config": { "char": "c", "description": "The name of the app configuration.", "env": "SHOPIFY_FLAG_APP_CONFIG", - "hasDynamicHelp": false, "hidden": false, - "multiple": false, "name": "config", + "hasDynamicHelp": false, + "multiple": false, "type": "option" }, - "no-color": { - "allowNo": false, - "description": "Disable color output.", - "env": "SHOPIFY_FLAG_NO_COLOR", + "client-id": { + "description": "The Client ID of your app.", + "env": "SHOPIFY_FLAG_CLIENT_ID", + "exclusive": [ + "config" + ], "hidden": false, - "name": "no-color", - "type": "boolean" - }, - "path": { - "description": "The path to your app directory.", - "env": "SHOPIFY_FLAG_PATH", + "name": "client-id", "hasDynamicHelp": false, "multiple": false, - "name": "path", - "noCacheDefault": true, "type": "option" }, "reset": { - "allowNo": false, "description": "Reset all your settings.", "env": "SHOPIFY_FLAG_RESET", "exclusive": [ @@ -2304,85 +2170,83 @@ ], "hidden": false, "name": "reset", + "allowNo": false, "type": "boolean" }, - "verbose": { + "stdout": { + "description": "Output the schema to stdout instead of writing to a file.", + "env": "SHOPIFY_FLAG_STDOUT", + "name": "stdout", + "required": false, "allowNo": false, - "description": "Increase the verbosity of the output.", - "env": "SHOPIFY_FLAG_VERBOSE", - "hidden": false, - "name": "verbose", "type": "boolean" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "app:import-extensions", + "hidden": true, + "hiddenAliases": [], + "id": "app:generate:schema", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", - "strict": true + "summary": "Fetch the latest GraphQL schema for a function.", + "descriptionWithMarkdown": "[DEPRECATED, use `app function schema`] Generates the latest [GraphQL schema](https://shopify.dev/docs/apps/functions/input-output#graphql-schema) for a function in your app. Run this command from the function directory.\n\n This command uses the API type and version of your function, as defined in your extension TOML file, to generate the latest GraphQL schema. The schema is written to the `schema.graphql` file.", + "customPluginName": "@shopify/app" }, - "app:info": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/app", - "description": "The information returned includes the following:\n\n - The app and dev store that's used when you run the \"dev\" (https://shopify.dev/docs/api/shopify-cli/app/app-dev) command. You can reset these configurations using \"`dev --reset`\" (https://shopify.dev/docs/api/shopify-cli/app/app-dev#flags-propertydetail-reset).\n - The \"structure\" (https://shopify.dev/docs/apps/tools/cli/structure) of your app project.\n - The \"access scopes\" (https://shopify.dev/docs/api/usage) your app has requested.\n - System information, including the package manager and version of Shopify CLI used in the project.", - "descriptionWithMarkdown": "The information returned includes the following:\n\n - The app and dev store that's used when you run the [dev](https://shopify.dev/docs/api/shopify-cli/app/app-dev) command. You can reset these configurations using [`dev --reset`](https://shopify.dev/docs/api/shopify-cli/app/app-dev#flags-propertydetail-reset).\n - The [structure](https://shopify.dev/docs/apps/tools/cli/structure) of your app project.\n - The [access scopes](https://shopify.dev/docs/api/usage) your app has requested.\n - System information, including the package manager and version of Shopify CLI used in the project.", + "app:function:build": { + "aliases": [], + "args": {}, + "description": "Compiles the function in your current directory to WebAssembly (Wasm) for testing purposes.", "flags": { - "client-id": { - "description": "The Client ID of your app.", - "env": "SHOPIFY_FLAG_CLIENT_ID", - "exclusive": [ - "config" - ], - "hasDynamicHelp": false, + "no-color": { + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", + "allowNo": false, + "type": "boolean" + }, + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", + "allowNo": false, + "type": "boolean" + }, + "path": { + "description": "The path to your function directory.", + "env": "SHOPIFY_FLAG_PATH", "hidden": false, + "name": "path", + "noCacheDefault": true, + "hasDynamicHelp": false, "multiple": false, - "name": "client-id", "type": "option" }, "config": { "char": "c", "description": "The name of the app configuration.", "env": "SHOPIFY_FLAG_APP_CONFIG", - "hasDynamicHelp": false, "hidden": false, - "multiple": false, "name": "config", + "hasDynamicHelp": false, + "multiple": false, "type": "option" }, - "json": { - "allowNo": false, - "char": "j", - "description": "Output the result as JSON. Automatically disables color output.", - "env": "SHOPIFY_FLAG_JSON", - "hidden": false, - "name": "json", - "type": "boolean" - }, - "no-color": { - "allowNo": false, - "description": "Disable color output.", - "env": "SHOPIFY_FLAG_NO_COLOR", + "client-id": { + "description": "The Client ID of your app.", + "env": "SHOPIFY_FLAG_CLIENT_ID", + "exclusive": [ + "config" + ], "hidden": false, - "name": "no-color", - "type": "boolean" - }, - "path": { - "description": "The path to your app directory.", - "env": "SHOPIFY_FLAG_PATH", + "name": "client-id", "hasDynamicHelp": false, "multiple": false, - "name": "path", - "noCacheDefault": true, "type": "option" }, "reset": { - "allowNo": false, "description": "Reset all your settings.", "env": "SHOPIFY_FLAG_RESET", "exclusive": [ @@ -2390,318 +2254,178 @@ ], "hidden": false, "name": "reset", - "type": "boolean" - }, - "verbose": { - "allowNo": false, - "description": "Increase the verbosity of the output.", - "env": "SHOPIFY_FLAG_VERBOSE", - "hidden": false, - "name": "verbose", - "type": "boolean" - }, - "web-env": { "allowNo": false, - "description": "Outputs environment variables necessary for running and deploying web/.", - "env": "SHOPIFY_FLAG_OUTPUT_WEB_ENV", - "hidden": false, - "name": "web-env", "type": "boolean" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "app:info", + "hiddenAliases": [], + "id": "app:function:build", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", "strict": true, - "summary": "Print basic information about your app and extensions." + "summary": "Compile a function to wasm.", + "descriptionWithMarkdown": "Compiles the function in your current directory to WebAssembly (Wasm) for testing purposes.", + "customPluginName": "@shopify/app" }, - "app:init": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/app", + "app:function:replay": { + "aliases": [], + "args": {}, + "description": "Runs the function from your current directory for \"testing purposes\" (https://shopify.dev/docs/apps/functions/testing-and-debugging). To learn how you can monitor and debug functions when errors occur, refer to \"Shopify Functions error handling\" (https://shopify.dev/docs/api/functions/errors).", "flags": { - "client-id": { - "description": "The Client ID of your app. Use this to automatically link your new project to an existing app. Using this flag avoids the app selection prompt.", - "env": "SHOPIFY_FLAG_CLIENT_ID", - "exclusive": [ - "config" - ], - "hasDynamicHelp": false, - "hidden": false, - "multiple": false, - "name": "client-id", - "type": "option" - }, - "flavor": { - "description": "Which flavor of the given template to use.", - "env": "SHOPIFY_FLAG_TEMPLATE_FLAVOR", - "hasDynamicHelp": false, - "multiple": false, - "name": "flavor", - "type": "option" - }, - "local": { - "allowNo": false, - "char": "l", - "env": "SHOPIFY_FLAG_LOCAL", - "hidden": true, - "name": "local", - "type": "boolean" - }, - "name": { - "char": "n", - "description": "The name for the new app. When provided, skips the app selection prompt and creates a new app with this name.", - "env": "SHOPIFY_FLAG_NAME", - "hasDynamicHelp": false, - "hidden": false, - "multiple": false, - "name": "name", - "type": "option" - }, "no-color": { - "allowNo": false, "description": "Disable color output.", "env": "SHOPIFY_FLAG_NO_COLOR", "hidden": false, "name": "no-color", + "allowNo": false, "type": "boolean" }, - "organization-id": { - "description": "The organization ID. Your organization ID can be found in your Dev Dashboard URL: https://dev.shopify.com/dashboard/", - "env": "SHOPIFY_FLAG_ORGANIZATION_ID", - "exclusive": [ - "client-id" - ], - "hasDynamicHelp": false, - "hidden": false, - "multiple": false, - "name": "organization-id", - "type": "option" - }, - "package-manager": { - "char": "d", - "env": "SHOPIFY_FLAG_PACKAGE_MANAGER", - "hasDynamicHelp": false, + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", "hidden": false, - "multiple": false, - "name": "package-manager", - "options": [ - "npm", - "yarn", - "pnpm", - "bun" - ], - "type": "option" + "name": "verbose", + "allowNo": false, + "type": "boolean" }, "path": { - "char": "p", - "default": ".", + "description": "The path to your function directory.", "env": "SHOPIFY_FLAG_PATH", - "hasDynamicHelp": false, "hidden": false, - "multiple": false, "name": "path", + "noCacheDefault": true, + "hasDynamicHelp": false, + "multiple": false, "type": "option" }, - "template": { - "description": "The app template. Accepts one of the following:\n - \n - Any GitHub repo with optional branch and subpath, e.g., https://github.com/Shopify//[subpath]#[branch]", - "env": "SHOPIFY_FLAG_TEMPLATE", + "config": { + "char": "c", + "description": "The name of the app configuration.", + "env": "SHOPIFY_FLAG_APP_CONFIG", + "hidden": false, + "name": "config", "hasDynamicHelp": false, "multiple": false, - "name": "template", "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": "app:init", - "pluginAlias": "@shopify/cli", - "pluginName": "@shopify/cli", - "pluginType": "core", - "strict": true, - "summary": "Create a new app project" - }, - "app:logs": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/app", - "description": "\n Opens a real-time stream of detailed app logs from the selected app and store.\n Use the `--source` argument to limit output to a particular log source, such as a specific Shopify Function handle. Use the `shopify app logs sources` command to view a list of sources.\n Use the `--status` argument to filter on status, either `success` or `failure`.\n ```\n shopify app logs --status=success --source=extension.discount-function\n ```\n ", - "descriptionWithMarkdown": "\n Opens a real-time stream of detailed app logs from the selected app and store.\n Use the `--source` argument to limit output to a particular log source, such as a specific Shopify Function handle. Use the `shopify app logs sources` command to view a list of sources.\n Use the `--status` argument to filter on status, either `success` or `failure`.\n ```\n shopify app logs --status=success --source=extension.discount-function\n ```\n ", - "flags": { "client-id": { "description": "The Client ID of your app.", "env": "SHOPIFY_FLAG_CLIENT_ID", "exclusive": [ "config" ], - "hasDynamicHelp": false, "hidden": false, - "multiple": false, "name": "client-id", - "type": "option" - }, - "config": { - "char": "c", - "description": "The name of the app configuration.", - "env": "SHOPIFY_FLAG_APP_CONFIG", "hasDynamicHelp": false, - "hidden": false, "multiple": false, - "name": "config", "type": "option" }, - "json": { + "reset": { + "description": "Reset all your settings.", + "env": "SHOPIFY_FLAG_RESET", + "exclusive": [ + "config" + ], + "hidden": false, + "name": "reset", "allowNo": false, + "type": "boolean" + }, + "json": { "char": "j", "description": "Output the result as JSON. Automatically disables color output.", "env": "SHOPIFY_FLAG_JSON", "hidden": false, "name": "json", - "type": "boolean" - }, - "no-color": { "allowNo": false, - "description": "Disable color output.", - "env": "SHOPIFY_FLAG_NO_COLOR", - "hidden": false, - "name": "no-color", "type": "boolean" }, - "path": { - "description": "The path to your app directory.", - "env": "SHOPIFY_FLAG_PATH", + "log": { + "char": "l", + "description": "Specifies a log identifier to replay instead of selecting from a list. The identifier is provided in the output of `shopify app dev` and is the suffix of the log file name.", + "env": "SHOPIFY_FLAG_LOG", + "name": "log", "hasDynamicHelp": false, "multiple": false, - "name": "path", - "noCacheDefault": true, "type": "option" }, - "reset": { - "allowNo": false, - "description": "Reset all your settings.", - "env": "SHOPIFY_FLAG_RESET", - "exclusive": [ - "config" - ], + "watch": { + "char": "w", + "description": "Re-run the function when the source code changes.", + "env": "SHOPIFY_FLAG_WATCH", "hidden": false, - "name": "reset", - "type": "boolean" - }, - "source": { - "description": "Filters output to the specified log source.", - "env": "SHOPIFY_FLAG_SOURCE", - "hasDynamicHelp": false, - "multiple": true, - "name": "source", - "type": "option" - }, - "status": { - "description": "Filters output to the specified status (success or failure).", - "env": "SHOPIFY_FLAG_STATUS", - "hasDynamicHelp": false, - "multiple": false, - "name": "status", - "options": [ - "success", - "failure" - ], - "type": "option" - }, - "store": { - "char": "s", - "description": "Store URL. Must be an existing development or Shopify Plus sandbox store.", - "env": "SHOPIFY_FLAG_STORE", - "hasDynamicHelp": false, - "multiple": true, - "name": "store", - "type": "option" - }, - "verbose": { - "allowNo": false, - "description": "Increase the verbosity of the output.", - "env": "SHOPIFY_FLAG_VERBOSE", - "hidden": false, - "name": "verbose", + "name": "watch", + "allowNo": true, "type": "boolean" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "app:logs", + "hiddenAliases": [], + "id": "app:function:replay", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", "strict": true, - "summary": "Stream detailed logs for your Shopify app." + "summary": "Replays a function run from an app log.", + "descriptionWithMarkdown": "Runs the function from your current directory for [testing purposes](https://shopify.dev/docs/apps/functions/testing-and-debugging). To learn how you can monitor and debug functions when errors occur, refer to [Shopify Functions error handling](https://shopify.dev/docs/api/functions/errors).", + "customPluginName": "@shopify/app" }, - "app:logs:sources": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/app", - "description": "The output source names can be used with the `--source` argument of `shopify app logs` to filter log output. Currently only function extensions are supported as sources.", - "descriptionWithMarkdown": "The output source names can be used with the `--source` argument of `shopify app logs` to filter log output. Currently only function extensions are supported as sources.", + "app:function:run": { + "aliases": [], + "args": {}, + "description": "Runs the function from your current directory for \"testing purposes\" (https://shopify.dev/docs/apps/functions/testing-and-debugging). To learn how you can monitor and debug functions when errors occur, refer to \"Shopify Functions error handling\" (https://shopify.dev/docs/api/functions/errors).", "flags": { - "client-id": { - "description": "The Client ID of your app.", - "env": "SHOPIFY_FLAG_CLIENT_ID", - "exclusive": [ - "config" - ], - "hasDynamicHelp": false, + "no-color": { + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", + "allowNo": false, + "type": "boolean" + }, + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", + "allowNo": false, + "type": "boolean" + }, + "path": { + "description": "The path to your function directory.", + "env": "SHOPIFY_FLAG_PATH", "hidden": false, + "name": "path", + "noCacheDefault": true, + "hasDynamicHelp": false, "multiple": false, - "name": "client-id", "type": "option" }, "config": { "char": "c", "description": "The name of the app configuration.", "env": "SHOPIFY_FLAG_APP_CONFIG", - "hasDynamicHelp": false, "hidden": false, - "multiple": false, "name": "config", + "hasDynamicHelp": false, + "multiple": false, "type": "option" }, - "no-color": { - "allowNo": false, - "description": "Disable color output.", - "env": "SHOPIFY_FLAG_NO_COLOR", + "client-id": { + "description": "The Client ID of your app.", + "env": "SHOPIFY_FLAG_CLIENT_ID", + "exclusive": [ + "config" + ], "hidden": false, - "name": "no-color", - "type": "boolean" - }, - "path": { - "description": "The path to your app directory.", - "env": "SHOPIFY_FLAG_PATH", + "name": "client-id", "hasDynamicHelp": false, "multiple": false, - "name": "path", - "noCacheDefault": true, "type": "option" }, "reset": { - "allowNo": false, "description": "Reset all your settings.", "env": "SHOPIFY_FLAG_RESET", "exclusive": [ @@ -2709,93 +2433,103 @@ ], "hidden": false, "name": "reset", + "allowNo": false, "type": "boolean" }, - "verbose": { - "allowNo": false, - "description": "Increase the verbosity of the output.", - "env": "SHOPIFY_FLAG_VERBOSE", + "json": { + "char": "j", + "description": "Output the result as JSON. Automatically disables color output.", + "env": "SHOPIFY_FLAG_JSON", "hidden": false, - "name": "verbose", + "name": "json", + "allowNo": false, "type": "boolean" + }, + "input": { + "char": "i", + "description": "The input JSON to pass to the function. If omitted, standard input is used.", + "env": "SHOPIFY_FLAG_INPUT", + "name": "input", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "export": { + "char": "e", + "description": "Name of the WebAssembly export to invoke.", + "env": "SHOPIFY_FLAG_EXPORT", + "hidden": false, + "name": "export", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "app:logs:sources", + "hiddenAliases": [], + "id": "app:function:run", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", "strict": true, - "summary": "Print out a list of sources that may be used with the logs command." + "summary": "Run a function locally for testing.", + "descriptionWithMarkdown": "Runs the function from your current directory for [testing purposes](https://shopify.dev/docs/apps/functions/testing-and-debugging). To learn how you can monitor and debug functions when errors occur, refer to [Shopify Functions error handling](https://shopify.dev/docs/api/functions/errors).", + "customPluginName": "@shopify/app" }, - "app:release": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/app", - "description": "Releases an existing app version. Pass the name of the version that you want to release using the `--version` flag.", - "descriptionWithMarkdown": "Releases an existing app version. Pass the name of the version that you want to release using the `--version` flag.", + "app:function:info": { + "aliases": [], + "args": {}, + "description": "The information returned includes the following:\n\n - The function handle\n - The function name\n - The function API version\n - The targeting configuration\n - The schema path\n - The WASM path\n - The function runner path", "flags": { - "allow-deletes": { - "allowNo": false, - "description": "Allows removing extensions and configuration without requiring user confirmation. For CI/CD environments, the recommended flag is --allow-updates.", - "env": "SHOPIFY_FLAG_ALLOW_DELETES", + "no-color": { + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", "hidden": false, - "name": "allow-deletes", + "name": "no-color", + "allowNo": false, "type": "boolean" }, - "allow-updates": { - "allowNo": false, - "description": "Allows adding and updating extensions and configuration without requiring user confirmation. Recommended option for CI/CD environments.", - "env": "SHOPIFY_FLAG_ALLOW_UPDATES", + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", "hidden": false, - "name": "allow-updates", + "name": "verbose", + "allowNo": false, "type": "boolean" }, - "client-id": { - "description": "The Client ID of your app.", - "env": "SHOPIFY_FLAG_CLIENT_ID", - "exclusive": [ - "config" - ], - "hasDynamicHelp": false, + "path": { + "description": "The path to your function directory.", + "env": "SHOPIFY_FLAG_PATH", "hidden": false, + "name": "path", + "noCacheDefault": true, + "hasDynamicHelp": false, "multiple": false, - "name": "client-id", "type": "option" }, "config": { "char": "c", "description": "The name of the app configuration.", "env": "SHOPIFY_FLAG_APP_CONFIG", - "hasDynamicHelp": false, "hidden": false, - "multiple": false, "name": "config", + "hasDynamicHelp": false, + "multiple": false, "type": "option" }, - "no-color": { - "allowNo": false, - "description": "Disable color output.", - "env": "SHOPIFY_FLAG_NO_COLOR", + "client-id": { + "description": "The Client ID of your app.", + "env": "SHOPIFY_FLAG_CLIENT_ID", + "exclusive": [ + "config" + ], "hidden": false, - "name": "no-color", - "type": "boolean" - }, - "path": { - "description": "The path to your app directory.", - "env": "SHOPIFY_FLAG_PATH", + "name": "client-id", "hasDynamicHelp": false, "multiple": false, - "name": "path", - "noCacheDefault": true, "type": "option" }, "reset": { - "allowNo": false, "description": "Reset all your settings.", "env": "SHOPIFY_FLAG_RESET", "exclusive": [ @@ -2803,97 +2537,84 @@ ], "hidden": false, "name": "reset", - "type": "boolean" - }, - "verbose": { "allowNo": false, - "description": "Increase the verbosity of the output.", - "env": "SHOPIFY_FLAG_VERBOSE", - "hidden": false, - "name": "verbose", "type": "boolean" }, - "version": { - "description": "The name of the app version to release.", - "env": "SHOPIFY_FLAG_VERSION", - "hasDynamicHelp": false, + "json": { + "char": "j", + "description": "Output the result as JSON. Automatically disables color output.", + "env": "SHOPIFY_FLAG_JSON", "hidden": false, - "multiple": false, - "name": "version", - "required": true, - "type": "option" + "name": "json", + "allowNo": false, + "type": "boolean" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "app:release", + "hiddenAliases": [], + "id": "app:function:info", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", "strict": true, - "summary": "Release an app version.", - "usage": "app release --version " + "summary": "Print basic information about your function.", + "descriptionWithMarkdown": "The information returned includes the following:\n\n - The function handle\n - The function name\n - The function API version\n - The targeting configuration\n - The schema path\n - The WASM path\n - The function runner path", + "customPluginName": "@shopify/app" }, - "app:versions:list": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/app", - "description": "Lists the deployed app versions. An app version is a snapshot of your app extensions.", - "descriptionWithMarkdown": "Lists the deployed app versions. An app version is a snapshot of your app extensions.", + "app:function:schema": { + "aliases": [], + "args": {}, + "description": "Generates the latest \"GraphQL schema\" (https://shopify.dev/docs/apps/functions/input-output#graphql-schema) for a function in your app. Run this command from the function directory.\n\n This command uses the API type and version of your function, as defined in your extension TOML file, to generate the latest GraphQL schema. The schema is written to the `schema.graphql` file.", "flags": { - "client-id": { - "description": "The Client ID of your app.", - "env": "SHOPIFY_FLAG_CLIENT_ID", - "exclusive": [ - "config" - ], - "hasDynamicHelp": false, + "no-color": { + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", "hidden": false, + "name": "no-color", + "allowNo": false, + "type": "boolean" + }, + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", + "allowNo": false, + "type": "boolean" + }, + "path": { + "description": "The path to your function directory.", + "env": "SHOPIFY_FLAG_PATH", + "hidden": false, + "name": "path", + "noCacheDefault": true, + "hasDynamicHelp": false, "multiple": false, - "name": "client-id", "type": "option" }, "config": { "char": "c", "description": "The name of the app configuration.", "env": "SHOPIFY_FLAG_APP_CONFIG", - "hasDynamicHelp": false, "hidden": false, - "multiple": false, "name": "config", + "hasDynamicHelp": false, + "multiple": false, "type": "option" }, - "json": { - "allowNo": false, - "char": "j", - "description": "Output the result as JSON. Automatically disables color output.", - "env": "SHOPIFY_FLAG_JSON", - "hidden": false, - "name": "json", - "type": "boolean" - }, - "no-color": { - "allowNo": false, - "description": "Disable color output.", - "env": "SHOPIFY_FLAG_NO_COLOR", + "client-id": { + "description": "The Client ID of your app.", + "env": "SHOPIFY_FLAG_CLIENT_ID", + "exclusive": [ + "config" + ], "hidden": false, - "name": "no-color", - "type": "boolean" - }, - "path": { - "description": "The path to your app directory.", - "env": "SHOPIFY_FLAG_PATH", + "name": "client-id", "hasDynamicHelp": false, "multiple": false, - "name": "path", - "noCacheDefault": true, "type": "option" }, "reset": { - "allowNo": false, "description": "Reset all your settings.", "env": "SHOPIFY_FLAG_RESET", "exclusive": [ @@ -2901,54 +2622,68 @@ ], "hidden": false, "name": "reset", + "allowNo": false, "type": "boolean" }, - "verbose": { + "stdout": { + "description": "Output the schema to stdout instead of writing to a file.", + "env": "SHOPIFY_FLAG_STDOUT", + "name": "stdout", + "required": false, "allowNo": false, - "description": "Increase the verbosity of the output.", - "env": "SHOPIFY_FLAG_VERBOSE", - "hidden": false, - "name": "verbose", "type": "boolean" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "app:versions:list", + "hiddenAliases": [], + "id": "app:function:schema", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", "strict": true, - "summary": "List deployed versions of your app." + "summary": "Fetch the latest GraphQL schema for a function.", + "descriptionWithMarkdown": "Generates the latest [GraphQL schema](https://shopify.dev/docs/apps/functions/input-output#graphql-schema) for a function in your app. Run this command from the function directory.\n\n This command uses the API type and version of your function, as defined in your extension TOML file, to generate the latest GraphQL schema. The schema is written to the `schema.graphql` file.", + "customPluginName": "@shopify/app" }, - "app:webhook:trigger": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/app", - "description": "\n Triggers the delivery of a sample Admin API event topic payload to a designated address.\n\n You should use this command to experiment with webhooks, to initially test your webhook configuration, or for unit testing. However, to test your webhook configuration from end to end, you should always trigger webhooks by performing the related action in Shopify.\n\n Because most webhook deliveries use remote endpoints, you can trigger the command from any directory where you can use Shopify CLI, and send the webhook to any of the supported endpoint types. For example, you can run the command from your app's local directory, but send the webhook to a staging environment endpoint.\n\n To learn more about using webhooks in a Shopify app, refer to \"Webhooks overview\" (https://shopify.dev/docs/apps/webhooks).\n\n ### Limitations\n\n - Webhooks triggered using this method always have the same payload, so they can't be used to test scenarios that differ based on the payload contents.\n - Webhooks triggered using this method aren't retried when they fail.\n - Trigger requests are rate-limited using the \"Partner API rate limit\" (https://shopify.dev/docs/api/partner#rate_limits).\n - You can't use this method to validate your API webhook subscriptions.\n ", - "descriptionWithMarkdown": "\n Triggers the delivery of a sample Admin API event topic payload to a designated address.\n\n You should use this command to experiment with webhooks, to initially test your webhook configuration, or for unit testing. However, to test your webhook configuration from end to end, you should always trigger webhooks by performing the related action in Shopify.\n\n Because most webhook deliveries use remote endpoints, you can trigger the command from any directory where you can use Shopify CLI, and send the webhook to any of the supported endpoint types. For example, you can run the command from your app's local directory, but send the webhook to a staging environment endpoint.\n\n To learn more about using webhooks in a Shopify app, refer to [Webhooks overview](https://shopify.dev/docs/apps/webhooks).\n\n ### Limitations\n\n - Webhooks triggered using this method always have the same payload, so they can't be used to test scenarios that differ based on the payload contents.\n - Webhooks triggered using this method aren't retried when they fail.\n - Trigger requests are rate-limited using the [Partner API rate limit](https://shopify.dev/docs/api/partner#rate_limits).\n - You can't use this method to validate your API webhook subscriptions.\n ", + "app:function:typegen": { + "aliases": [], + "args": {}, + "description": "Creates GraphQL types based on your \"input query\" (https://shopify.dev/docs/apps/functions/input-output#input) for a function. Supports JavaScript functions out of the box, or any language via the `build.typegen_command` configuration.", "flags": { - "address": { - "description": "The URL where the webhook payload should be sent.\n You will need a different address type for each delivery-method:\n · For remote HTTP testing, use a URL that starts with https://\n · For local HTTP testing, use http://localhost:{port}/{url-path}\n · For Google Pub/Sub, use pubsub://{project-id}:{topic-id}\n · For Amazon EventBridge, use an Amazon Resource Name (ARN) starting with arn:aws:events:", - "env": "SHOPIFY_FLAG_ADDRESS", - "hasDynamicHelp": false, + "no-color": { + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", + "allowNo": false, + "type": "boolean" + }, + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", + "allowNo": false, + "type": "boolean" + }, + "path": { + "description": "The path to your function directory.", + "env": "SHOPIFY_FLAG_PATH", "hidden": false, + "name": "path", + "noCacheDefault": true, + "hasDynamicHelp": false, "multiple": false, - "name": "address", - "required": false, "type": "option" }, - "api-version": { - "description": "The API Version of the webhook topic.", - "env": "SHOPIFY_FLAG_API_VERSION", - "hasDynamicHelp": false, + "config": { + "char": "c", + "description": "The name of the app configuration.", + "env": "SHOPIFY_FLAG_APP_CONFIG", "hidden": false, + "name": "config", + "hasDynamicHelp": false, "multiple": false, - "name": "api-version", - "required": false, "type": "option" }, "client-id": { @@ -2957,67 +2692,88 @@ "exclusive": [ "config" ], - "hasDynamicHelp": false, "hidden": false, - "multiple": false, "name": "client-id", + "hasDynamicHelp": false, + "multiple": false, "type": "option" }, - "client-secret": { - "description": "Your app's client secret. This secret allows us to return the X-Shopify-Hmac-SHA256 header that lets you validate the origin of the response that you receive.", - "env": "SHOPIFY_FLAG_CLIENT_SECRET", - "hasDynamicHelp": false, + "reset": { + "description": "Reset all your settings.", + "env": "SHOPIFY_FLAG_RESET", + "exclusive": [ + "config" + ], + "hidden": false, + "name": "reset", + "allowNo": false, + "type": "boolean" + } + }, + "hasDynamicHelp": false, + "hiddenAliases": [], + "id": "app:function:typegen", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true, + "summary": "Generate GraphQL types for a function.", + "descriptionWithMarkdown": "Creates GraphQL types based on your [input query](https://shopify.dev/docs/apps/functions/input-output#input) for a function. Supports JavaScript functions out of the box, or any language via the `build.typegen_command` configuration.", + "customPluginName": "@shopify/app" + }, + "app:generate:extension": { + "aliases": [], + "args": {}, + "description": "Generates a new \"app extension\" (https://shopify.dev/docs/apps/build/app-extensions). For a list of app extensions that you can generate using this command, refer to \"Supported extensions\" (https://shopify.dev/docs/apps/build/app-extensions/list-of-app-extensions).\n\n Each new app extension is created in a folder under `extensions/`. To learn more about the extensions file structure, refer to \"App structure\" (https://shopify.dev/docs/apps/build/cli-for-apps/app-structure) and the documentation for your extension.\n ", + "flags": { + "no-color": { + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", + "allowNo": false, + "type": "boolean" + }, + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", "hidden": false, + "name": "verbose", + "allowNo": false, + "type": "boolean" + }, + "path": { + "description": "The path to your app directory.", + "env": "SHOPIFY_FLAG_PATH", + "name": "path", + "noCacheDefault": true, + "hasDynamicHelp": false, "multiple": false, - "name": "client-secret", - "required": false, "type": "option" }, "config": { "char": "c", "description": "The name of the app configuration.", "env": "SHOPIFY_FLAG_APP_CONFIG", - "hasDynamicHelp": false, "hidden": false, - "multiple": false, "name": "config", - "type": "option" - }, - "delivery-method": { - "description": "Method chosen to deliver the topic payload. If not passed, it's inferred from the address.", - "env": "SHOPIFY_FLAG_DELIVERY_METHOD", "hasDynamicHelp": false, - "hidden": false, "multiple": false, - "name": "delivery-method", - "options": [ - "http", - "google-pub-sub", - "event-bridge" - ], - "required": false, "type": "option" }, - "help": { - "allowNo": false, - "description": "This help. When you run the trigger command the CLI will prompt you for any information that isn't passed using flags.", - "env": "SHOPIFY_FLAG_HELP", + "client-id": { + "description": "The Client ID of your app.", + "env": "SHOPIFY_FLAG_CLIENT_ID", + "exclusive": [ + "config" + ], "hidden": false, - "name": "help", - "required": false, - "type": "boolean" - }, - "path": { - "description": "The path to your app directory.", - "env": "SHOPIFY_FLAG_PATH", + "name": "client-id", "hasDynamicHelp": false, "multiple": false, - "name": "path", - "noCacheDefault": true, "type": "option" }, "reset": { - "allowNo": false, "description": "Reset all your settings.", "env": "SHOPIFY_FLAG_RESET", "exclusive": [ @@ -3025,387 +2781,332 @@ ], "hidden": false, "name": "reset", + "allowNo": false, "type": "boolean" }, - "topic": { - "description": "The requested webhook topic.", - "env": "SHOPIFY_FLAG_TOPIC", + "type": { + "char": "t", + "description": "Deprecated. Please use --template", + "env": "SHOPIFY_FLAG_EXTENSION_TYPE", + "hidden": false, + "name": "type", "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "template": { + "char": "t", + "description": "Extension template", + "env": "SHOPIFY_FLAG_EXTENSION_TEMPLATE", "hidden": false, + "name": "template", + "hasDynamicHelp": false, "multiple": false, - "name": "topic", - "required": false, "type": "option" - } - }, - "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "app:webhook:trigger", - "pluginAlias": "@shopify/cli", - "pluginName": "@shopify/cli", - "pluginType": "core", - "strict": true, - "summary": "Trigger delivery of a sample webhook topic payload to a designated address." - }, - "auth:login": { - "aliases": [ - ], - "args": { - }, - "description": "Logs you in to your Shopify account.", - "enableJsonFlag": false, - "flags": { - "alias": { - "description": "Alias of the session you want to login to.", - "env": "SHOPIFY_FLAG_AUTH_ALIAS", + }, + "name": { + "char": "n", + "description": "name of your Extension", + "env": "SHOPIFY_FLAG_NAME", + "hidden": false, + "name": "name", "hasDynamicHelp": false, "multiple": false, - "name": "alias", "type": "option" - } - }, - "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "auth:login", - "pluginAlias": "@shopify/cli", - "pluginName": "@shopify/cli", - "pluginType": "core", - "strict": true - }, - "auth:logout": { - "aliases": [ - ], - "args": { - }, - "description": "Logs you out of the Shopify account or Partner account and store.", - "enableJsonFlag": false, - "flags": { + }, + "clone-url": { + "char": "u", + "description": "The Git URL to clone the function extensions templates from. Defaults to: https://github.com/Shopify/function-examples", + "env": "SHOPIFY_FLAG_CLONE_URL", + "hidden": true, + "name": "clone-url", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "flavor": { + "description": "Choose a starting template for your extension, where applicable", + "env": "SHOPIFY_FLAG_FLAVOR", + "hidden": false, + "name": "flavor", + "hasDynamicHelp": false, + "multiple": false, + "options": [ + "vanilla-js", + "react", + "typescript", + "typescript-react", + "wasm", + "rust" + ], + "type": "option" + } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "auth:logout", + "hiddenAliases": [], + "id": "app:generate:extension", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", - "strict": true + "strict": true, + "summary": "Generate a new app Extension.", + "descriptionWithMarkdown": "Generates a new [app extension](https://shopify.dev/docs/apps/build/app-extensions). For a list of app extensions that you can generate using this command, refer to [Supported extensions](https://shopify.dev/docs/apps/build/app-extensions/list-of-app-extensions).\n\n Each new app extension is created in a folder under `extensions/`. To learn more about the extensions file structure, refer to [App structure](https://shopify.dev/docs/apps/build/cli-for-apps/app-structure) and the documentation for your extension.\n ", + "customPluginName": "@shopify/app" }, - "cache:clear": { - "aliases": [ - ], - "args": { - }, - "description": "Clear the CLI cache, used to store some API responses and handle notifications status", - "enableJsonFlag": false, + "app:versions:list": { + "aliases": [], + "args": {}, + "description": "Lists the deployed app versions. An app version is a snapshot of your app extensions.", "flags": { + "no-color": { + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", + "allowNo": false, + "type": "boolean" + }, + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", + "allowNo": false, + "type": "boolean" + }, + "path": { + "description": "The path to your app directory.", + "env": "SHOPIFY_FLAG_PATH", + "name": "path", + "noCacheDefault": true, + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "config": { + "char": "c", + "description": "The name of the app configuration.", + "env": "SHOPIFY_FLAG_APP_CONFIG", + "hidden": false, + "name": "config", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "client-id": { + "description": "The Client ID of your app.", + "env": "SHOPIFY_FLAG_CLIENT_ID", + "exclusive": [ + "config" + ], + "hidden": false, + "name": "client-id", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "reset": { + "description": "Reset all your settings.", + "env": "SHOPIFY_FLAG_RESET", + "exclusive": [ + "config" + ], + "hidden": false, + "name": "reset", + "allowNo": false, + "type": "boolean" + }, + "json": { + "char": "j", + "description": "Output the result as JSON. Automatically disables color output.", + "env": "SHOPIFY_FLAG_JSON", + "hidden": false, + "name": "json", + "allowNo": false, + "type": "boolean" + } }, "hasDynamicHelp": false, - "hidden": true, - "hiddenAliases": [ - ], - "id": "cache:clear", + "hiddenAliases": [], + "id": "app:versions:list", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", - "strict": true + "strict": true, + "summary": "List deployed versions of your app.", + "descriptionWithMarkdown": "Lists the deployed app versions. An app version is a snapshot of your app extensions.", + "customPluginName": "@shopify/app" }, - "commands": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@oclif/plugin-commands", - "description": "List all <%= config.bin %> commands.", - "enableJsonFlag": true, + "app:webhook:trigger": { + "aliases": [], + "args": {}, + "description": "\n Triggers the delivery of a sample Admin API event topic payload to a designated address.\n\n You should use this command to experiment with webhooks, to initially test your webhook configuration, or for unit testing. However, to test your webhook configuration from end to end, you should always trigger webhooks by performing the related action in Shopify.\n\n Because most webhook deliveries use remote endpoints, you can trigger the command from any directory where you can use Shopify CLI, and send the webhook to any of the supported endpoint types. For example, you can run the command from your app's local directory, but send the webhook to a staging environment endpoint.\n\n To learn more about using webhooks in a Shopify app, refer to \"Webhooks overview\" (https://shopify.dev/docs/apps/webhooks).\n\n ### Limitations\n\n - Webhooks triggered using this method always have the same payload, so they can't be used to test scenarios that differ based on the payload contents.\n - Webhooks triggered using this method aren't retried when they fail.\n - Trigger requests are rate-limited using the \"Partner API rate limit\" (https://shopify.dev/docs/api/partner#rate_limits).\n - You can't use this method to validate your API webhook subscriptions.\n ", "flags": { - "columns": { + "path": { + "description": "The path to your app directory.", + "env": "SHOPIFY_FLAG_PATH", + "name": "path", + "noCacheDefault": true, + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "config": { "char": "c", - "delimiter": ",", - "description": "Only show provided columns (comma-separated).", + "description": "The name of the app configuration.", + "env": "SHOPIFY_FLAG_APP_CONFIG", + "hidden": false, + "name": "config", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "client-id": { + "description": "The Client ID of your app.", + "env": "SHOPIFY_FLAG_CLIENT_ID", "exclusive": [ - "tree" + "config" ], + "hidden": false, + "name": "client-id", "hasDynamicHelp": false, - "multiple": true, - "name": "columns", - "options": [ - "id", - "plugin", - "summary", - "type" - ], + "multiple": false, "type": "option" }, - "deprecated": { - "allowNo": false, - "description": "Show deprecated commands.", - "name": "deprecated", - "type": "boolean" - }, - "extended": { - "allowNo": false, - "char": "x", - "description": "Show extra columns.", + "reset": { + "description": "Reset all your settings.", + "env": "SHOPIFY_FLAG_RESET", "exclusive": [ - "tree" + "config" ], - "name": "extended", - "type": "boolean" - }, - "hidden": { + "hidden": false, + "name": "reset", "allowNo": false, - "description": "Show hidden commands.", - "name": "hidden", "type": "boolean" }, - "json": { + "help": { + "description": "This help. When you run the trigger command the CLI will prompt you for any information that isn't passed using flags.", + "env": "SHOPIFY_FLAG_HELP", + "hidden": false, + "name": "help", + "required": false, "allowNo": false, - "description": "Format output as json.", - "helpGroup": "GLOBAL", - "name": "json", "type": "boolean" }, - "no-truncate": { - "allowNo": false, - "description": "Do not truncate output.", - "exclusive": [ - "tree" - ], - "name": "no-truncate", - "type": "boolean" + "topic": { + "description": "The requested webhook topic.", + "env": "SHOPIFY_FLAG_TOPIC", + "hidden": false, + "name": "topic", + "required": false, + "hasDynamicHelp": false, + "multiple": false, + "type": "option" }, - "sort": { - "default": "id", - "description": "Property to sort by.", - "exclusive": [ - "tree" - ], + "api-version": { + "description": "The API Version of the webhook topic.", + "env": "SHOPIFY_FLAG_API_VERSION", + "hidden": false, + "name": "api-version", + "required": false, "hasDynamicHelp": false, "multiple": false, - "name": "sort", - "options": [ - "id", - "plugin", - "summary", - "type" - ], "type": "option" }, - "tree": { - "allowNo": false, - "description": "Show tree of commands.", - "name": "tree", - "type": "boolean" - } - }, - "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "commands", - "pluginAlias": "@shopify/cli", - "pluginName": "@shopify/cli", - "pluginType": "core", - "strict": true - }, - "config:autocorrect:off": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/plugin-did-you-mean", - "description": "Disable autocorrect. Off by default.\n\n When autocorrection is enabled, Shopify CLI automatically runs a corrected version of your command if a correction is available.\n\n When autocorrection is disabled, you need to confirm that you want to run corrections for mistyped commands.\n", - "descriptionWithMarkdown": "Disable autocorrect. Off by default.\n\n When autocorrection is enabled, Shopify CLI automatically runs a corrected version of your command if a correction is available.\n\n When autocorrection is disabled, you need to confirm that you want to run corrections for mistyped commands.\n", - "enableJsonFlag": false, - "flags": { - }, - "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "config:autocorrect:off", - "pluginAlias": "@shopify/cli", - "pluginName": "@shopify/cli", - "pluginType": "core", - "strict": true, - "summary": "Disable autocorrect. Off by default." - }, - "config:autocorrect:on": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/plugin-did-you-mean", - "description": "Enable autocorrect. Off by default.\n\n When autocorrection is enabled, Shopify CLI automatically runs a corrected version of your command if a correction is available.\n\n When autocorrection is disabled, you need to confirm that you want to run corrections for mistyped commands.\n", - "descriptionWithMarkdown": "Enable autocorrect. Off by default.\n\n When autocorrection is enabled, Shopify CLI automatically runs a corrected version of your command if a correction is available.\n\n When autocorrection is disabled, you need to confirm that you want to run corrections for mistyped commands.\n", - "enableJsonFlag": false, - "flags": { - }, - "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "config:autocorrect:on", - "pluginAlias": "@shopify/cli", - "pluginName": "@shopify/cli", - "pluginType": "core", - "strict": true, - "summary": "Enable autocorrect. Off by default." - }, - "config:autocorrect:status": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/plugin-did-you-mean", - "description": "Check whether autocorrect is enabled or disabled. On by default.\n\n When autocorrection is enabled, Shopify CLI automatically runs a corrected version of your command if a correction is available.\n\n When autocorrection is disabled, you need to confirm that you want to run corrections for mistyped commands.\n", - "descriptionWithMarkdown": "Check whether autocorrect is enabled or disabled. On by default.\n\n When autocorrection is enabled, Shopify CLI automatically runs a corrected version of your command if a correction is available.\n\n When autocorrection is disabled, you need to confirm that you want to run corrections for mistyped commands.\n", - "enableJsonFlag": false, - "flags": { - }, - "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "config:autocorrect:status", - "pluginAlias": "@shopify/cli", - "pluginName": "@shopify/cli", - "pluginType": "core", - "strict": true, - "summary": "Check whether autocorrect is enabled or disabled. On by default." - }, - "config:autoupgrade:off": { - "aliases": [ - ], - "args": { - }, - "description": "Disable automatic upgrades for Shopify CLI.\n\n When auto-upgrade is disabled, Shopify CLI won't automatically update. Run `shopify upgrade` to update manually.\n\n To enable auto-upgrade, run `shopify config autoupgrade on`.\n", - "descriptionWithMarkdown": "Disable automatic upgrades for Shopify CLI.\n\n When auto-upgrade is disabled, Shopify CLI won't automatically update. Run `shopify upgrade` to update manually.\n\n To enable auto-upgrade, run `shopify config autoupgrade on`.\n", - "enableJsonFlag": false, - "flags": { - }, - "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "config:autoupgrade:off", - "pluginAlias": "@shopify/cli", - "pluginName": "@shopify/cli", - "pluginType": "core", - "strict": true, - "summary": "Disable automatic upgrades for Shopify CLI." - }, - "config:autoupgrade:on": { - "aliases": [ - ], - "args": { - }, - "description": "Enable automatic upgrades for Shopify CLI.\n\n When auto-upgrade is enabled, Shopify CLI automatically updates to the latest version once per day. Major version upgrades are skipped and must be done manually.\n\n To disable auto-upgrade, run `shopify config autoupgrade off`.\n", - "descriptionWithMarkdown": "Enable automatic upgrades for Shopify CLI.\n\n When auto-upgrade is enabled, Shopify CLI automatically updates to the latest version once per day. Major version upgrades are skipped and must be done manually.\n\n To disable auto-upgrade, run `shopify config autoupgrade off`.\n", - "enableJsonFlag": false, - "flags": { - }, - "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "config:autoupgrade:on", - "pluginAlias": "@shopify/cli", - "pluginName": "@shopify/cli", - "pluginType": "core", - "strict": true, - "summary": "Enable automatic upgrades for Shopify CLI." - }, - "config:autoupgrade:status": { - "aliases": [ - ], - "args": { - }, - "description": "Check whether auto-upgrade is enabled, disabled, or not yet configured.\n\n When auto-upgrade is enabled, Shopify CLI automatically updates to the latest version after each command.\n\n Run `shopify config autoupgrade on` or `shopify config autoupgrade off` to configure it.\n", - "descriptionWithMarkdown": "Check whether auto-upgrade is enabled, disabled, or not yet configured.\n\n When auto-upgrade is enabled, Shopify CLI automatically updates to the latest version after each command.\n\n Run `shopify config autoupgrade on` or `shopify config autoupgrade off` to configure it.\n", - "enableJsonFlag": false, - "flags": { - }, - "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "config:autoupgrade:status", - "pluginAlias": "@shopify/cli", - "pluginName": "@shopify/cli", - "pluginType": "core", - "strict": true, - "summary": "Check whether auto-upgrade is enabled, disabled, or not yet configured." - }, - "debug:command-flags": { - "aliases": [ - ], - "args": { - }, - "description": "View all the available command flags", - "enableJsonFlag": false, - "flags": { - "csv": { - "allowNo": false, - "description": "Output as CSV", - "env": "SHOPIFY_FLAG_OUTPUT_CSV", - "name": "csv", - "type": "boolean" + "delivery-method": { + "description": "Method chosen to deliver the topic payload. If not passed, it's inferred from the address.", + "env": "SHOPIFY_FLAG_DELIVERY_METHOD", + "hidden": false, + "name": "delivery-method", + "required": false, + "hasDynamicHelp": false, + "multiple": false, + "options": [ + "http", + "google-pub-sub", + "event-bridge" + ], + "type": "option" + }, + "shared-secret": { + "description": "Deprecated. Please use client-secret.", + "env": "SHOPIFY_FLAG_SHARED_SECRET", + "hidden": false, + "name": "shared-secret", + "required": false, + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "client-secret": { + "description": "Your app's client secret. This secret allows us to return the X-Shopify-Hmac-SHA256 header that lets you validate the origin of the response that you receive.", + "env": "SHOPIFY_FLAG_CLIENT_SECRET", + "hidden": false, + "name": "client-secret", + "required": false, + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "address": { + "description": "The URL where the webhook payload should be sent.\n You will need a different address type for each delivery-method:\n · For remote HTTP testing, use a URL that starts with https://\n · For local HTTP testing, use http://localhost:{port}/{url-path}\n · For Google Pub/Sub, use pubsub://{project-id}:{topic-id}\n · For Amazon EventBridge, use an Amazon Resource Name (ARN) starting with arn:aws:events:", + "env": "SHOPIFY_FLAG_ADDRESS", + "hidden": false, + "name": "address", + "required": false, + "hasDynamicHelp": false, + "multiple": false, + "type": "option" } }, "hasDynamicHelp": false, - "hidden": true, - "hiddenAliases": [ - ], - "id": "debug:command-flags", + "hiddenAliases": [], + "id": "app:webhook:trigger", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", - "strict": true + "strict": true, + "summary": "Trigger delivery of a sample webhook topic payload to a designated address.", + "descriptionWithMarkdown": "\n Triggers the delivery of a sample Admin API event topic payload to a designated address.\n\n You should use this command to experiment with webhooks, to initially test your webhook configuration, or for unit testing. However, to test your webhook configuration from end to end, you should always trigger webhooks by performing the related action in Shopify.\n\n Because most webhook deliveries use remote endpoints, you can trigger the command from any directory where you can use Shopify CLI, and send the webhook to any of the supported endpoint types. For example, you can run the command from your app's local directory, but send the webhook to a staging environment endpoint.\n\n To learn more about using webhooks in a Shopify app, refer to [Webhooks overview](https://shopify.dev/docs/apps/webhooks).\n\n ### Limitations\n\n - Webhooks triggered using this method always have the same payload, so they can't be used to test scenarios that differ based on the payload contents.\n - Webhooks triggered using this method aren't retried when they fail.\n - Trigger requests are rate-limited using the [Partner API rate limit](https://shopify.dev/docs/api/partner#rate_limits).\n - You can't use this method to validate your API webhook subscriptions.\n ", + "customPluginName": "@shopify/app" }, - "demo:watcher": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/app", + "webhook:trigger": { + "aliases": [], + "args": {}, + "description": "\n Triggers the delivery of a sample Admin API event topic payload to a designated address.\n\n You should use this command to experiment with webhooks, to initially test your webhook configuration, or for unit testing. However, to test your webhook configuration from end to end, you should always trigger webhooks by performing the related action in Shopify.\n\n Because most webhook deliveries use remote endpoints, you can trigger the command from any directory where you can use Shopify CLI, and send the webhook to any of the supported endpoint types. For example, you can run the command from your app's local directory, but send the webhook to a staging environment endpoint.\n\n To learn more about using webhooks in a Shopify app, refer to \"Webhooks overview\" (https://shopify.dev/docs/apps/webhooks).\n\n ### Limitations\n\n - Webhooks triggered using this method always have the same payload, so they can't be used to test scenarios that differ based on the payload contents.\n - Webhooks triggered using this method aren't retried when they fail.\n - Trigger requests are rate-limited using the \"Partner API rate limit\" (https://shopify.dev/docs/api/partner#rate_limits).\n - You can't use this method to validate your API webhook subscriptions.\n ", "flags": { - "client-id": { - "description": "The Client ID of your app.", - "env": "SHOPIFY_FLAG_CLIENT_ID", - "exclusive": [ - "config" - ], + "path": { + "description": "The path to your app directory.", + "env": "SHOPIFY_FLAG_PATH", + "name": "path", + "noCacheDefault": true, "hasDynamicHelp": false, - "hidden": false, "multiple": false, - "name": "client-id", "type": "option" }, "config": { "char": "c", "description": "The name of the app configuration.", "env": "SHOPIFY_FLAG_APP_CONFIG", - "hasDynamicHelp": false, "hidden": false, - "multiple": false, "name": "config", + "hasDynamicHelp": false, + "multiple": false, "type": "option" }, - "no-color": { - "allowNo": false, - "description": "Disable color output.", - "env": "SHOPIFY_FLAG_NO_COLOR", + "client-id": { + "description": "The Client ID of your app.", + "env": "SHOPIFY_FLAG_CLIENT_ID", + "exclusive": [ + "config" + ], "hidden": false, - "name": "no-color", - "type": "boolean" - }, - "path": { - "description": "The path to your app directory.", - "env": "SHOPIFY_FLAG_PATH", + "name": "client-id", "hasDynamicHelp": false, "multiple": false, - "name": "path", - "noCacheDefault": true, "type": "option" }, "reset": { - "allowNo": false, "description": "Reset all your settings.", "env": "SHOPIFY_FLAG_RESET", "exclusive": [ @@ -3413,5031 +3114,6406 @@ ], "hidden": false, "name": "reset", + "allowNo": false, "type": "boolean" }, - "verbose": { - "allowNo": false, - "description": "Increase the verbosity of the output.", - "env": "SHOPIFY_FLAG_VERBOSE", + "help": { + "description": "This help. When you run the trigger command the CLI will prompt you for any information that isn't passed using flags.", + "env": "SHOPIFY_FLAG_HELP", "hidden": false, - "name": "verbose", - "type": "boolean" - } - }, - "hasDynamicHelp": false, - "hidden": true, - "hiddenAliases": [ - ], - "id": "demo:watcher", - "pluginAlias": "@shopify/cli", - "pluginName": "@shopify/cli", - "pluginType": "core", - "strict": true, - "summary": "Watch and prints out changes to an app." - }, - "doc:fetch": { - "aliases": [ - ], - "args": { - }, - "description": "Download a complete document from shopify.dev. Every page on shopify.dev has a Markdown version, and that is what this tool returns. Use this to pull an entire document verbatim — for example, a set of instructions an agent follows like a centrally-served skill. For finding the relevant pieces of content across shopify.dev instead, use `doc search`.", - "enableJsonFlag": false, - "examples": [ - "# fetch the Markdown version of a Shopify.dev page\nshopify doc fetch --url https://shopify.dev/docs/api/shopify-cli", - "# save the document to a file instead of printing it\nshopify doc fetch --url https://shopify.dev/docs/api/shopify-cli --output docs/shopify-cli.md" - ], - "flags": { - "no-color": { + "name": "help", + "required": false, "allowNo": false, - "description": "Disable color output.", - "env": "SHOPIFY_FLAG_NO_COLOR", - "hidden": false, - "name": "no-color", "type": "boolean" }, - "output": { - "description": "Write the document to this file path instead of printing it to stdout.", - "env": "SHOPIFY_FLAG_OUTPUT", + "topic": { + "description": "The requested webhook topic.", + "env": "SHOPIFY_FLAG_TOPIC", + "hidden": false, + "name": "topic", + "required": false, "hasDynamicHelp": false, "multiple": false, - "name": "output", "type": "option" }, - "url": { - "description": "The shopify.dev URL to fetch.", - "env": "SHOPIFY_FLAG_URL", + "api-version": { + "description": "The API Version of the webhook topic.", + "env": "SHOPIFY_FLAG_API_VERSION", + "hidden": false, + "name": "api-version", + "required": false, "hasDynamicHelp": false, "multiple": false, - "name": "url", - "required": true, "type": "option" }, - "verbose": { - "allowNo": false, - "description": "Increase the verbosity of the output.", - "env": "SHOPIFY_FLAG_VERBOSE", + "delivery-method": { + "description": "Method chosen to deliver the topic payload. If not passed, it's inferred from the address.", + "env": "SHOPIFY_FLAG_DELIVERY_METHOD", "hidden": false, - "name": "verbose", - "type": "boolean" - } - }, - "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "doc:fetch", - "pluginAlias": "@shopify/cli", - "pluginName": "@shopify/cli", - "pluginType": "core", - "strict": true - }, - "doc:search": { - "aliases": [ - ], - "args": { - }, - "description": "Query the shopify.dev vector store and print the most relevant documentation chunks as JSON. Best for programmatic discovery — surfacing the relevant pieces of documentation for a topic, rather than retrieving a whole document. To download a full document verbatim, use `doc fetch`.", - "enableJsonFlag": false, - "examples": [ - "# search shopify.dev for a topic\n shopify doc search --query \"subscribe to webhooks\"\n\n # narrow the search to a specific API and version\n shopify doc search --query \"create a product\" --api-name admin --api-version latest\n " - ], - "flags": { - "api-name": { - "description": "Limit results to a specific API (for example: admin, storefront, hydrogen, functions). Unrecognized values are ignored.", - "env": "SHOPIFY_FLAG_API_NAME", + "name": "delivery-method", + "required": false, "hasDynamicHelp": false, "multiple": false, - "name": "api-name", + "options": [ + "http", + "google-pub-sub", + "event-bridge" + ], "type": "option" }, - "api-version": { - "description": "Limit results to a specific API version (for example: 2025-10, latest, current).", - "env": "SHOPIFY_FLAG_API_VERSION", + "shared-secret": { + "description": "Deprecated. Please use client-secret.", + "env": "SHOPIFY_FLAG_SHARED_SECRET", + "hidden": false, + "name": "shared-secret", + "required": false, "hasDynamicHelp": false, "multiple": false, - "name": "api-version", "type": "option" }, - "no-color": { - "allowNo": false, - "description": "Disable color output.", - "env": "SHOPIFY_FLAG_NO_COLOR", + "client-secret": { + "description": "Your app's client secret. This secret allows us to return the X-Shopify-Hmac-SHA256 header that lets you validate the origin of the response that you receive.", + "env": "SHOPIFY_FLAG_CLIENT_SECRET", "hidden": false, - "name": "no-color", - "type": "boolean" - }, - "query": { - "description": "The search query.", - "env": "SHOPIFY_FLAG_QUERY", + "name": "client-secret", + "required": false, "hasDynamicHelp": false, "multiple": false, - "name": "query", - "required": true, "type": "option" }, - "verbose": { - "allowNo": false, - "description": "Increase the verbosity of the output.", - "env": "SHOPIFY_FLAG_VERBOSE", + "address": { + "description": "The URL where the webhook payload should be sent.\n You will need a different address type for each delivery-method:\n · For remote HTTP testing, use a URL that starts with https://\n · For local HTTP testing, use http://localhost:{port}/{url-path}\n · For Google Pub/Sub, use pubsub://{project-id}:{topic-id}\n · For Amazon EventBridge, use an Amazon Resource Name (ARN) starting with arn:aws:events:", + "env": "SHOPIFY_FLAG_ADDRESS", "hidden": false, - "name": "verbose", - "type": "boolean" + "name": "address", + "required": false, + "hasDynamicHelp": false, + "multiple": false, + "type": "option" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "doc:search", - "pluginAlias": "@shopify/cli", - "pluginName": "@shopify/cli", - "pluginType": "core", - "strict": true - }, - "docs:generate": { - "aliases": [ - ], - "args": { - }, - "description": "Generate CLI commands documentation", - "enableJsonFlag": false, - "flags": { - }, - "hasDynamicHelp": false, - "hidden": true, - "hiddenAliases": [ - ], - "id": "docs:generate", - "pluginAlias": "@shopify/cli", - "pluginName": "@shopify/cli", - "pluginType": "core", - "strict": true - }, - "doctor-release": { - "aliases": [ - ], - "args": { - }, - "description": "Run CLI doctor-release tests", - "enableJsonFlag": false, - "flags": { - }, - "hasDynamicHelp": false, "hidden": true, - "hiddenAliases": [ - ], - "id": "doctor-release", + "hiddenAliases": [], + "id": "webhook:trigger", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", - "strict": true + "summary": "Trigger delivery of a sample webhook topic payload to a designated address.", + "descriptionWithMarkdown": "\n Triggers the delivery of a sample Admin API event topic payload to a designated address.\n\n You should use this command to experiment with webhooks, to initially test your webhook configuration, or for unit testing. However, to test your webhook configuration from end to end, you should always trigger webhooks by performing the related action in Shopify.\n\n Because most webhook deliveries use remote endpoints, you can trigger the command from any directory where you can use Shopify CLI, and send the webhook to any of the supported endpoint types. For example, you can run the command from your app's local directory, but send the webhook to a staging environment endpoint.\n\n To learn more about using webhooks in a Shopify app, refer to [Webhooks overview](https://shopify.dev/docs/apps/webhooks).\n\n ### Limitations\n\n - Webhooks triggered using this method always have the same payload, so they can't be used to test scenarios that differ based on the payload contents.\n - Webhooks triggered using this method aren't retried when they fail.\n - Trigger requests are rate-limited using the [Partner API rate limit](https://shopify.dev/docs/api/partner#rate_limits).\n - You can't use this method to validate your API webhook subscriptions.\n ", + "customPluginName": "@shopify/app" }, - "doctor-release:theme": { - "aliases": [ - ], - "args": { - }, - "description": "Run all theme command doctor-release tests", - "enableJsonFlag": false, + "demo:watcher": { + "aliases": [], + "args": {}, "flags": { - "environment": { - "char": "e", - "description": "The environment to use from shopify.theme.toml (required for store-connected tests).", - "env": "SHOPIFY_FLAG_ENVIRONMENT", - "hasDynamicHelp": false, - "multiple": false, - "name": "environment", - "required": true, - "type": "option" - }, "no-color": { - "allowNo": false, "description": "Disable color output.", "env": "SHOPIFY_FLAG_NO_COLOR", "hidden": false, "name": "no-color", + "allowNo": false, "type": "boolean" }, - "password": { - "description": "Password from Theme Access app (overrides environment).", - "env": "SHOPIFY_FLAG_PASSWORD", + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", + "allowNo": false, + "type": "boolean" + }, + "path": { + "description": "The path to your app directory.", + "env": "SHOPIFY_FLAG_PATH", + "name": "path", + "noCacheDefault": true, "hasDynamicHelp": false, "multiple": false, - "name": "password", "type": "option" }, - "path": { - "char": "p", - "default": ".", - "description": "The path to run tests in. Defaults to current directory.", - "env": "SHOPIFY_FLAG_PATH", + "config": { + "char": "c", + "description": "The name of the app configuration.", + "env": "SHOPIFY_FLAG_APP_CONFIG", + "hidden": false, + "name": "config", "hasDynamicHelp": false, "multiple": false, - "name": "path", "type": "option" }, - "store": { - "char": "s", - "description": "Store URL (overrides environment).", - "env": "SHOPIFY_FLAG_STORE", + "client-id": { + "description": "The Client ID of your app.", + "env": "SHOPIFY_FLAG_CLIENT_ID", + "exclusive": [ + "config" + ], + "hidden": false, + "name": "client-id", "hasDynamicHelp": false, "multiple": false, - "name": "store", "type": "option" }, - "verbose": { - "allowNo": false, - "description": "Increase the verbosity of the output.", - "env": "SHOPIFY_FLAG_VERBOSE", + "reset": { + "description": "Reset all your settings.", + "env": "SHOPIFY_FLAG_RESET", + "exclusive": [ + "config" + ], "hidden": false, - "name": "verbose", + "name": "reset", + "allowNo": false, "type": "boolean" } }, "hasDynamicHelp": false, "hidden": true, - "hiddenAliases": [ - ], - "id": "doctor-release:theme", + "hiddenAliases": [], + "id": "demo:watcher", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", - "strict": true + "strict": true, + "summary": "Watch and prints out changes to an app.", + "customPluginName": "@shopify/app" }, - "help": { - "aliases": [ - ], - "args": { - "command": { - "description": "Command to show help for.", - "name": "command", - "required": false - } - }, - "description": "Display help for Shopify CLI", - "enableJsonFlag": false, + "organization:list": { + "aliases": [], + "args": {}, + "description": "Lists the Shopify organizations that you have access to, along with their organization IDs.", "flags": { - "nested-commands": { + "no-color": { + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", + "allowNo": false, + "type": "boolean" + }, + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", + "allowNo": false, + "type": "boolean" + }, + "json": { + "char": "j", + "description": "Output the result as JSON. Automatically disables color output.", + "env": "SHOPIFY_FLAG_JSON", + "hidden": false, + "name": "json", "allowNo": false, - "char": "n", - "description": "Include all nested commands in the output.", - "env": "SHOPIFY_FLAG_CLI_NESTED_COMMANDS", - "name": "nested-commands", "type": "boolean" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "help", + "hiddenAliases": [], + "id": "organization:list", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", - "strict": false, - "usage": "help [command] [flags]" - }, - "hydrogen:build": { - "aliases": [ - ], + "strict": true, + "summary": "List Shopify organizations you have access to.", + "enableJsonFlag": false, + "descriptionWithMarkdown": "Lists the Shopify organizations that you have access to, along with their organization IDs.", + "customPluginName": "@shopify/app" + }, + "theme:init": { + "aliases": [], "args": { + "name": { + "description": "Name of the new theme", + "name": "name", + "required": false + } }, - "customPluginName": "@shopify/cli-hydrogen", - "description": "Builds a Hydrogen storefront for production.", - "descriptionWithMarkdown": "Builds a Hydrogen storefront for production. The client and app worker files are compiled to a `/dist` folder in your Hydrogen project directory.", - "enableJsonFlag": false, + "description": "Clones a Git repository to your local machine to use as the starting point for building a theme.\n\n If no Git repository is specified, then this command creates a copy of Shopify's \"Skeleton theme\" (https://github.com/Shopify/skeleton-theme.git), with the specified name in the current folder. If no name is provided, then you're prompted to enter one.\n\n > Caution: If you're building a theme for the Shopify Theme Store, then you can use our example theme as a starting point. However, the theme that you submit needs to be \"substantively different from existing themes\" (https://shopify.dev/docs/themes/store/requirements#uniqueness) so that it provides added value for users.\n ", "flags": { - "bundle-stats": { - "allowNo": true, - "description": "Show a bundle size summary after building. Defaults to true, use `--no-bundle-stats` to disable.", - "name": "bundle-stats", - "type": "boolean" - }, - "codegen": { + "no-color": { + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", "allowNo": false, - "description": "Automatically generates GraphQL types for your project’s Storefront API queries.", - "name": "codegen", - "required": false, "type": "boolean" }, - "codegen-config-path": { - "dependsOn": [ - "codegen" - ], - "description": "Specifies a path to a codegen configuration file. Defaults to `/codegen.ts` if this file exists.", - "hasDynamicHelp": false, - "multiple": false, - "name": "codegen-config-path", - "required": false, - "type": "option" - }, - "disable-route-warning": { + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", "allowNo": false, - "description": "Disables any warnings about missing standard routes.", - "env": "SHOPIFY_HYDROGEN_FLAG_DISABLE_ROUTE_WARNING", - "name": "disable-route-warning", "type": "boolean" }, - "entry": { - "description": "Entry file for the worker. Defaults to `./server`.", - "env": "SHOPIFY_HYDROGEN_FLAG_ENTRY", + "path": { + "description": "The path where you want to run the command. Defaults to the current working directory.", + "env": "SHOPIFY_FLAG_PATH", + "name": "path", + "noCacheDefault": true, "hasDynamicHelp": false, "multiple": false, - "name": "entry", "type": "option" }, - "force-client-sourcemap": { - "allowNo": false, - "description": "Client sourcemapping is avoided by default because it makes backend code visible in the browser. Use this flag to force enabling it.", - "env": "SHOPIFY_HYDROGEN_FLAG_FORCE_CLIENT_SOURCEMAP", - "name": "force-client-sourcemap", - "type": "boolean" - }, - "lockfile-check": { - "allowNo": true, - "description": "Checks that there is exactly one valid lockfile in the project. Defaults to `true`. Deactivate with `--no-lockfile-check`.", - "env": "SHOPIFY_HYDROGEN_FLAG_LOCKFILE_CHECK", - "name": "lockfile-check", - "type": "boolean" - }, - "path": { - "description": "The path to the directory of the Hydrogen storefront. Defaults to the current directory where the command is run.", - "env": "SHOPIFY_HYDROGEN_FLAG_PATH", + "clone-url": { + "char": "u", + "description": "The Git URL to clone from. Defaults to Shopify's Skeleton theme.", + "env": "SHOPIFY_FLAG_CLONE_URL", + "name": "clone-url", + "default": "https://github.com/Shopify/skeleton-theme.git", "hasDynamicHelp": false, "multiple": false, - "name": "path", "type": "option" }, - "sourcemap": { - "allowNo": true, - "description": "Controls whether server sourcemaps are generated. Default to `true`. Deactivate `--no-sourcemaps`.", - "env": "SHOPIFY_HYDROGEN_FLAG_SOURCEMAP", - "name": "sourcemap", - "type": "boolean" - }, - "watch": { + "latest": { + "char": "l", + "description": "Downloads the latest release of the `clone-url`", + "env": "SHOPIFY_FLAG_LATEST", + "name": "latest", "allowNo": false, - "description": "Watches for changes and rebuilds the project writing output to disk.", - "env": "SHOPIFY_HYDROGEN_FLAG_WATCH", - "name": "watch", "type": "boolean" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "hydrogen:build", + "hiddenAliases": [], + "id": "theme:init", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", - "strict": true + "strict": true, + "summary": "Clones a Git repository to use as a starting point for building a new theme.", + "usage": "theme init [name] [flags]", + "descriptionWithMarkdown": "Clones a Git repository to your local machine to use as the starting point for building a theme.\n\n If no Git repository is specified, then this command creates a copy of Shopify's [Skeleton theme](https://github.com/Shopify/skeleton-theme.git), with the specified name in the current folder. If no name is provided, then you're prompted to enter one.\n\n > Caution: If you're building a theme for the Shopify Theme Store, then you can use our example theme as a starting point. However, the theme that you submit needs to be [substantively different from existing themes](https://shopify.dev/docs/themes/store/requirements#uniqueness) so that it provides added value for users.\n ", + "multiEnvironmentsFlags": null, + "customPluginName": "@shopify/theme" }, - "hydrogen:check": { - "aliases": [ - ], - "args": { - "resource": { - "description": "The resource to check. Currently only 'routes' is supported.", - "name": "resource", - "options": [ - "routes" - ], - "required": true - } - }, - "customPluginName": "@shopify/cli-hydrogen", - "description": "Returns diagnostic information about a Hydrogen storefront.", - "descriptionWithMarkdown": "Checks whether your Hydrogen app includes a set of standard Shopify routes.", - "enableJsonFlag": false, + "theme:check": { + "aliases": [], + "args": {}, + "description": "Calls and runs \"Theme Check\" (https://shopify.dev/docs/themes/tools/theme-check) to analyze your theme code for errors and to ensure that it follows theme and Liquid best practices. \"Learn more about the checks that Theme Check runs.\" (https://shopify.dev/docs/themes/tools/theme-check/checks)", "flags": { + "no-color": { + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", + "allowNo": false, + "type": "boolean" + }, + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", + "allowNo": false, + "type": "boolean" + }, "path": { - "description": "The path to the directory of the Hydrogen storefront. Defaults to the current directory where the command is run.", - "env": "SHOPIFY_HYDROGEN_FLAG_PATH", - "hasDynamicHelp": false, - "multiple": false, + "description": "The path where you want to run the command. Defaults to the current working directory.", + "env": "SHOPIFY_FLAG_PATH", "name": "path", - "type": "option" - } - }, - "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "hydrogen:check", - "pluginAlias": "@shopify/cli", - "pluginName": "@shopify/cli", - "pluginType": "core", - "strict": true - }, - "hydrogen:codegen": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/cli-hydrogen", - "description": "Generate types for the Storefront API queries found in your project.", - "descriptionWithMarkdown": "Automatically generates GraphQL types for your project’s Storefront API queries.", - "enableJsonFlag": false, - "flags": { - "codegen-config-path": { - "description": "Specify a path to a codegen configuration file. Defaults to `/codegen.ts` if it exists.", + "noCacheDefault": true, "hasDynamicHelp": false, "multiple": false, - "name": "codegen-config-path", - "required": false, "type": "option" }, - "force-sfapi-version": { - "description": "Force generating Storefront API types for a specific version instead of using the one provided in Hydrogen. A token can also be provided with this format: `:`.", + "auto-correct": { + "char": "a", + "description": "Automatically fix offenses", + "env": "SHOPIFY_FLAG_AUTO_CORRECT", + "name": "auto-correct", + "required": false, + "allowNo": false, + "type": "boolean" + }, + "config": { + "char": "C", + "description": "Use the config provided, overriding .theme-check.yml if present\n Supports all theme-check: config values, e.g., theme-check:theme-app-extension,\n theme-check:recommended, theme-check:all\n For backwards compatibility, :theme_app_extension is also supported ", + "env": "SHOPIFY_FLAG_CONFIG", + "name": "config", + "required": false, "hasDynamicHelp": false, - "hidden": true, "multiple": false, - "name": "force-sfapi-version", "type": "option" }, - "path": { - "description": "The path to the directory of the Hydrogen storefront. Defaults to the current directory where the command is run.", - "env": "SHOPIFY_HYDROGEN_FLAG_PATH", + "fail-level": { + "description": "Minimum severity for exit with error code", + "env": "SHOPIFY_FLAG_FAIL_LEVEL", + "name": "fail-level", + "required": false, + "default": "error", "hasDynamicHelp": false, "multiple": false, - "name": "path", + "options": [ + "crash", + "error", + "suggestion", + "style", + "warning", + "info" + ], "type": "option" }, - "watch": { + "init": { + "description": "Generate a .theme-check.yml file", + "env": "SHOPIFY_FLAG_INIT", + "name": "init", + "required": false, "allowNo": false, - "description": "Watch the project for changes to update types on file save.", - "name": "watch", + "type": "boolean" + }, + "list": { + "description": "List enabled checks", + "env": "SHOPIFY_FLAG_LIST", + "name": "list", "required": false, + "allowNo": false, "type": "boolean" - } - }, - "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "hydrogen:codegen", - "pluginAlias": "@shopify/cli", - "pluginName": "@shopify/cli", - "pluginType": "core", - "strict": true - }, - "hydrogen:customer-account-push": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/cli-hydrogen", - "description": "Push project configuration to admin", - "enableJsonFlag": false, - "flags": { - "dev-origin": { - "description": "The development domain of your application.", + }, + "output": { + "char": "o", + "description": "The output format to use", + "env": "SHOPIFY_FLAG_OUTPUT", + "name": "output", + "required": false, + "default": "text", "hasDynamicHelp": false, "multiple": false, - "name": "dev-origin", - "required": true, + "options": [ + "text", + "json" + ], "type": "option" }, - "path": { - "description": "The path to the directory of the Hydrogen storefront. Defaults to the current directory where the command is run.", - "env": "SHOPIFY_HYDROGEN_FLAG_PATH", + "print": { + "description": "Output active config to STDOUT", + "env": "SHOPIFY_FLAG_PRINT", + "name": "print", + "required": false, + "allowNo": false, + "type": "boolean" + }, + "version": { + "char": "v", + "description": "Print Theme Check version", + "env": "SHOPIFY_FLAG_VERSION", + "name": "version", + "required": false, + "allowNo": false, + "type": "boolean" + }, + "environment": { + "char": "e", + "description": "The environment to apply to the current command.", + "env": "SHOPIFY_FLAG_ENVIRONMENT", + "name": "environment", "hasDynamicHelp": false, - "multiple": false, - "name": "path", + "multiple": true, "type": "option" + } + }, + "hasDynamicHelp": false, + "hiddenAliases": [], + "id": "theme:check", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true, + "summary": "Validate the theme.", + "descriptionWithMarkdown": "Calls and runs [Theme Check](https://shopify.dev/docs/themes/tools/theme-check) to analyze your theme code for errors and to ensure that it follows theme and Liquid best practices. [Learn more about the checks that Theme Check runs.](https://shopify.dev/docs/themes/tools/theme-check/checks)", + "multiEnvironmentsFlags": [ + "path" + ], + "customPluginName": "@shopify/theme" + }, + "theme:console": { + "aliases": [], + "args": {}, + "description": "Starts the Shopify Liquid REPL (read-eval-print loop) tool. This tool provides an interactive terminal interface for evaluating Liquid code and exploring Liquid objects, filters, and tags using real store data.\n\n You can also provide context to the console using a URL, as some Liquid objects are context-specific", + "flags": { + "no-color": { + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", + "allowNo": false, + "type": "boolean" }, - "relative-logout-uri": { - "description": "The relative url of allowed url that will be redirected to post-logout for Customer Account API OAuth flow. Default to nothing.", + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", + "allowNo": false, + "type": "boolean" + }, + "path": { + "description": "The path where you want to run the command. Defaults to the current working directory.", + "env": "SHOPIFY_FLAG_PATH", + "name": "path", + "noCacheDefault": true, "hasDynamicHelp": false, "multiple": false, - "name": "relative-logout-uri", "type": "option" }, - "relative-redirect-uri": { - "description": "The relative url of allowed callback url for Customer Account API OAuth flow. Default is '/account/authorize'", + "password": { + "description": "Password generated from the Theme Access app or an Admin API token.", + "env": "SHOPIFY_CLI_THEME_TOKEN", + "name": "password", "hasDynamicHelp": false, "multiple": false, - "name": "relative-redirect-uri", "type": "option" }, - "storefront-id": { - "description": "The id of the storefront the configuration should be pushed to. Must start with 'gid://shopify/HydrogenStorefront/'", + "store": { + "char": "s", + "description": "Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com, https://example.myshopify.com).", + "env": "SHOPIFY_FLAG_STORE", + "name": "store", "hasDynamicHelp": false, "multiple": false, - "name": "storefront-id", "type": "option" - } - }, - "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "hydrogen:customer-account-push", - "pluginAlias": "@shopify/cli", - "pluginName": "@shopify/cli", - "pluginType": "core", - "strict": true - }, - "hydrogen:debug:cpu": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/cli-hydrogen", - "description": "Builds and profiles the server startup time the app.", - "descriptionWithMarkdown": "Builds the app and runs the resulting code to profile the server startup time, watching for changes. This command can be used to [debug slow app startup times](https://shopify.dev/docs/custom-storefronts/hydrogen/debugging/cpu-startup) that cause failed deployments in Oxygen.\n\n The profiling results are written to a `.cpuprofile` file that can be viewed with certain tools such as [Flame Chart Visualizer for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=ms-vscode.vscode-js-profile-flame).", - "enableJsonFlag": false, - "flags": { - "entry": { - "description": "Entry file for the worker. Defaults to `./server`.", - "env": "SHOPIFY_HYDROGEN_FLAG_ENTRY", + }, + "environment": { + "char": "e", + "description": "The environment to apply to the current command.", + "env": "SHOPIFY_FLAG_ENVIRONMENT", + "name": "environment", "hasDynamicHelp": false, - "multiple": false, - "name": "entry", + "multiple": true, "type": "option" }, - "output": { - "default": "startup.cpuprofile", - "description": "Specify a path to generate the profile file. Defaults to \"startup.cpuprofile\".", + "url": { + "description": "The url to be used as context", + "env": "SHOPIFY_FLAG_URL", + "name": "url", + "default": "/", "hasDynamicHelp": false, "multiple": false, - "name": "output", - "required": false, "type": "option" }, - "path": { - "description": "The path to the directory of the Hydrogen storefront. Defaults to the current directory where the command is run.", - "env": "SHOPIFY_HYDROGEN_FLAG_PATH", + "store-password": { + "description": "The password for storefronts with password protection.", + "env": "SHOPIFY_FLAG_STORE_PASSWORD", + "name": "store-password", "hasDynamicHelp": false, "multiple": false, - "name": "path", "type": "option" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "hydrogen:debug:cpu", + "hiddenAliases": [], + "id": "theme:console", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", - "strict": true - }, - "hydrogen:deploy": { - "aliases": [ + "strict": true, + "summary": "Shopify Liquid REPL (read-eval-print loop) tool", + "usage": [ + "theme console", + "theme console --url /products/classic-leather-jacket" ], - "args": { - }, - "customPluginName": "@shopify/cli-hydrogen", - "description": "Builds and deploys a Hydrogen storefront to Oxygen.", - "descriptionWithMarkdown": "Builds and deploys your Hydrogen storefront to Oxygen. Requires an Oxygen deployment token to be set with the `--token` flag or an environment variable (`SHOPIFY_HYDROGEN_DEPLOYMENT_TOKEN`). If the storefront is [linked](https://shopify.dev/docs/api/shopify-cli/hydrogen/hydrogen-link) then the Oxygen deployment token for the linked storefront will be used automatically.", - "enableJsonFlag": false, + "descriptionWithMarkdown": "Starts the Shopify Liquid REPL (read-eval-print loop) tool. This tool provides an interactive terminal interface for evaluating Liquid code and exploring Liquid objects, filters, and tags using real store data.\n\n You can also provide context to the console using a URL, as some Liquid objects are context-specific", + "multiEnvironmentsFlags": null, + "customPluginName": "@shopify/theme" + }, + "theme:delete": { + "aliases": [], + "args": {}, + "description": "Deletes a theme from your store.\n\n You can specify multiple themes by ID. If no theme is specified, then you're prompted to select the theme that you want to delete from the list of themes in your store.\n\n You're asked to confirm that you want to delete the specified themes before they are deleted. You can skip this confirmation using the `--force` flag.", "flags": { - "auth-bypass-token": { + "no-color": { + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", "allowNo": false, - "description": "Generate an authentication bypass token, which can be used to perform end-to-end tests against the deployment.", - "env": "AUTH_BYPASS_TOKEN", - "name": "auth-bypass-token", - "required": false, "type": "boolean" }, - "auth-bypass-token-duration": { - "dependsOn": [ - "auth-bypass-token" - ], - "description": "Specify the duration (in hours) up to 12 hours for the authentication bypass token. Defaults to `2`", - "env": "AUTH_BYPASS_TOKEN_DURATION", - "hasDynamicHelp": false, - "multiple": false, - "name": "auth-bypass-token-duration", - "required": false, - "type": "option" - }, - "build-command": { - "description": "Specify a build command to run before deploying. If not specified, `shopify hydrogen build` will be used.", - "hasDynamicHelp": false, - "multiple": false, - "name": "build-command", - "required": false, - "type": "option" + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", + "allowNo": false, + "type": "boolean" }, - "entry": { - "description": "Entry file for the worker. Defaults to `./server`.", - "env": "SHOPIFY_HYDROGEN_FLAG_ENTRY", + "path": { + "description": "The path where you want to run the command. Defaults to the current working directory.", + "env": "SHOPIFY_FLAG_PATH", + "name": "path", + "noCacheDefault": true, "hasDynamicHelp": false, "multiple": false, - "name": "entry", "type": "option" }, - "env": { - "description": "Specifies the environment to perform the operation using its handle. Fetch the handle using the `env list` command.", - "exclusive": [ - "env-branch" - ], + "password": { + "description": "Password generated from the Theme Access app or an Admin API token.", + "env": "SHOPIFY_CLI_THEME_TOKEN", + "name": "password", "hasDynamicHelp": false, "multiple": false, - "name": "env", "type": "option" }, - "env-branch": { - "deprecated": { - "message": "--env-branch is deprecated. Use --env instead.", - "to": "env" - }, - "description": "Specifies the environment to perform the operation using its Git branch name.", - "env": "SHOPIFY_HYDROGEN_ENVIRONMENT_BRANCH", + "store": { + "char": "s", + "description": "Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com, https://example.myshopify.com).", + "env": "SHOPIFY_FLAG_STORE", + "name": "store", "hasDynamicHelp": false, "multiple": false, - "name": "env-branch", "type": "option" }, - "env-file": { - "description": "Path to an environment file to override existing environment variables for the deployment.", + "environment": { + "char": "e", + "description": "The environment to apply to the current command.", + "env": "SHOPIFY_FLAG_ENVIRONMENT", + "name": "environment", "hasDynamicHelp": false, - "multiple": false, - "name": "env-file", - "required": false, + "multiple": true, "type": "option" }, - "force": { + "development": { + "char": "d", + "description": "Delete your development theme.", + "env": "SHOPIFY_FLAG_DEVELOPMENT", + "name": "development", "allowNo": false, - "char": "f", - "description": "Forces a deployment to proceed if there are uncommited changes in its Git repository.", - "env": "SHOPIFY_HYDROGEN_FLAG_FORCE", - "name": "force", - "required": false, "type": "boolean" }, - "force-client-sourcemap": { + "show-all": { + "char": "a", + "description": "Include others development themes in theme list.", + "env": "SHOPIFY_FLAG_SHOW_ALL", + "name": "show-all", "allowNo": false, - "description": "Client sourcemapping is avoided by default because it makes backend code visible in the browser. Use this flag to force enabling it.", - "env": "SHOPIFY_HYDROGEN_FLAG_FORCE_CLIENT_SOURCEMAP", - "name": "force-client-sourcemap", "type": "boolean" }, - "json-output": { - "allowNo": true, - "description": "Create a JSON file containing the deployment details in CI environments. Defaults to true, use `--no-json-output` to disable.", - "name": "json-output", - "required": false, - "type": "boolean" - }, - "lockfile-check": { - "allowNo": true, - "description": "Checks that there is exactly one valid lockfile in the project. Defaults to `true`. Deactivate with `--no-lockfile-check`.", - "env": "SHOPIFY_HYDROGEN_FLAG_LOCKFILE_CHECK", - "name": "lockfile-check", + "force": { + "char": "f", + "description": "Skip confirmation.", + "env": "SHOPIFY_FLAG_FORCE", + "name": "force", + "allowNo": false, "type": "boolean" }, - "metadata-description": { - "description": "Description of the changes in the deployment. Defaults to the commit message of the latest commit if there are no uncommited changes.", - "env": "SHOPIFY_HYDROGEN_FLAG_METADATA_DESCRIPTION", + "theme": { + "char": "t", + "description": "Theme ID or name of the remote theme.", + "env": "SHOPIFY_FLAG_THEME_ID", + "name": "theme", "hasDynamicHelp": false, - "multiple": false, - "name": "metadata-description", - "required": false, + "multiple": true, "type": "option" + } + }, + "hasDynamicHelp": false, + "hiddenAliases": [], + "id": "theme:delete", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true, + "summary": "Delete remote themes from the connected store. This command can't be undone.", + "descriptionWithMarkdown": "Deletes a theme from your store.\n\n You can specify multiple themes by ID. If no theme is specified, then you're prompted to select the theme that you want to delete from the list of themes in your store.\n\n You're asked to confirm that you want to delete the specified themes before they are deleted. You can skip this confirmation using the `--force` flag.", + "multiEnvironmentsFlags": [ + "store", + "password", + [ + "development", + "theme" + ] + ], + "customPluginName": "@shopify/theme" + }, + "theme:dev": { + "aliases": [], + "args": {}, + "description": "\n Uploads the current theme as the specified theme, or a \"development theme\" (https://shopify.dev/docs/themes/tools/cli#development-themes), to a store so you can preview it.\n\nThis command returns the following information:\n\n- A link to your development theme at http://127.0.0.1:9292. This URL can hot reload local changes to CSS and sections, or refresh the entire page when a file changes, enabling you to preview changes in real time using the store's data.\n\n You can specify a different network interface and port using `--host` and `--port`.\n\n- A link to the \"editor\" (https://shopify.dev/docs/themes/tools/online-editor) for the theme in the Shopify admin.\n\n- A \"preview link\" (https://help.shopify.com/manual/online-store/themes/adding-themes#share-a-theme-preview-with-others) that you can share with other developers.\n\nIf you already have a development theme for your current environment, then this command replaces the development theme with your local theme. You can override this using the `--theme-editor-sync` flag.\n\n> Note: You can't preview checkout customizations using http://127.0.0.1:9292.\n\nDevelopment themes are deleted when you run `shopify auth logout`. If you need a preview link that can be used after you log out, then you should \"share\" (https://shopify.dev/docs/api/shopify-cli/theme/theme-share) your theme or \"push\" (https://shopify.dev/docs/api/shopify-cli/theme/theme-push) to an unpublished theme on your store.\n\nYou can run this command only in a directory that matches the \"default Shopify theme folder structure\" (https://shopify.dev/docs/themes/tools/cli#directory-structure).", + "flags": { + "no-color": { + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", + "allowNo": false, + "type": "boolean" }, - "metadata-url": { - "env": "SHOPIFY_HYDROGEN_FLAG_METADATA_URL", + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", + "allowNo": false, + "type": "boolean" + }, + "path": { + "description": "The path where you want to run the command. Defaults to the current working directory.", + "env": "SHOPIFY_FLAG_PATH", + "name": "path", + "noCacheDefault": true, "hasDynamicHelp": false, - "hidden": true, "multiple": false, - "name": "metadata-url", - "required": false, "type": "option" }, - "metadata-user": { - "description": "User that initiated the deployment. Will be saved and displayed in the Shopify admin", - "env": "SHOPIFY_HYDROGEN_FLAG_METADATA_USER", + "password": { + "description": "Password generated from the Theme Access app or an Admin API token.", + "env": "SHOPIFY_CLI_THEME_TOKEN", + "name": "password", "hasDynamicHelp": false, "multiple": false, - "name": "metadata-user", - "required": false, "type": "option" }, - "metadata-version": { - "env": "SHOPIFY_HYDROGEN_FLAG_METADATA_VERSION", + "store": { + "char": "s", + "description": "Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com, https://example.myshopify.com).", + "env": "SHOPIFY_FLAG_STORE", + "name": "store", "hasDynamicHelp": false, - "hidden": true, "multiple": false, - "name": "metadata-version", - "required": false, "type": "option" }, - "no-verify": { - "allowNo": false, - "description": "Skip the routability verification step after deployment.", - "name": "no-verify", - "required": false, - "type": "boolean" - }, - "path": { - "description": "The path to the directory of the Hydrogen storefront. Defaults to the current directory where the command is run.", - "env": "SHOPIFY_HYDROGEN_FLAG_PATH", + "environment": { + "char": "e", + "description": "The environment to apply to the current command.", + "env": "SHOPIFY_FLAG_ENVIRONMENT", + "name": "environment", "hasDynamicHelp": false, - "multiple": false, - "name": "path", + "multiple": true, "type": "option" }, - "preview": { - "allowNo": false, - "description": "Deploys to the Preview environment.", - "name": "preview", - "required": false, - "type": "boolean" - }, - "shop": { - "char": "s", - "description": "Shop URL. It can be the shop prefix (janes-apparel) or the full myshopify.com URL (janes-apparel.myshopify.com, https://janes-apparel.myshopify.com).", - "env": "SHOPIFY_SHOP", + "host": { + "description": "Set which network interface the web server listens on. The default value is 127.0.0.1.", + "env": "SHOPIFY_FLAG_HOST", + "name": "host", "hasDynamicHelp": false, "multiple": false, - "name": "shop", "type": "option" }, - "token": { - "char": "t", - "description": "Oxygen deployment token. Defaults to the linked storefront's token if available.", - "env": "SHOPIFY_HYDROGEN_DEPLOYMENT_TOKEN", + "live-reload": { + "description": "The live reload mode switches the server behavior when a file is modified:\n- hot-reload Hot reloads local changes to CSS and sections (default)\n- full-page Always refreshes the entire page\n- off Deactivate live reload", + "env": "SHOPIFY_FLAG_LIVE_RELOAD", + "name": "live-reload", + "default": "hot-reload", "hasDynamicHelp": false, "multiple": false, - "name": "token", - "required": false, + "options": [ + "hot-reload", + "full-page", + "off" + ], "type": "option" - } - }, - "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "hydrogen:deploy", - "pluginAlias": "@shopify/cli", - "pluginName": "@shopify/cli", - "pluginType": "core", - "strict": true - }, - "hydrogen:dev": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/cli-hydrogen", - "description": "Runs Hydrogen storefront in an Oxygen worker for development.", - "descriptionWithMarkdown": "Runs a Hydrogen storefront in a local runtime that emulates an Oxygen worker for development.\n\n If your project is [linked](https://shopify.dev/docs/api/shopify-cli/hydrogen/hydrogen-link) to a Hydrogen storefront, then its environment variables will be loaded with the runtime.", - "enableJsonFlag": false, - "flags": { - "codegen": { - "allowNo": false, - "description": "Automatically generates GraphQL types for your project’s Storefront API queries.", - "name": "codegen", - "required": false, - "type": "boolean" }, - "codegen-config-path": { - "dependsOn": [ - "codegen" - ], - "description": "Specifies a path to a codegen configuration file. Defaults to `/codegen.ts` if this file exists.", + "error-overlay": { + "description": "Controls the visibility of the error overlay when an theme asset upload fails:\n- silent Prevents the error overlay from appearing.\n- default Displays the error overlay.\n ", + "env": "SHOPIFY_FLAG_ERROR_OVERLAY", + "name": "error-overlay", + "default": "default", "hasDynamicHelp": false, "multiple": false, - "name": "codegen-config-path", - "required": false, + "options": [ + "silent", + "default" + ], "type": "option" }, - "customer-account-push": { - "allowNo": false, - "description": "Use tunneling for local development and push the tunneling domain to admin. Required to use Customer Account API's OAuth flow", - "env": "SHOPIFY_HYDROGEN_FLAG_CUSTOMER_ACCOUNT_PUSH", - "name": "customer-account-push", - "required": false, - "type": "boolean" - }, - "debug": { - "allowNo": false, - "description": "Enables inspector connections to the server with a debugger such as Visual Studio Code or Chrome DevTools.", - "env": "SHOPIFY_HYDROGEN_FLAG_DEBUG", - "name": "debug", - "type": "boolean" - }, - "disable-deps-optimizer": { - "allowNo": false, - "description": "Disable adding dependencies to Vite's `ssr.optimizeDeps.include` automatically", - "env": "SHOPIFY_HYDROGEN_FLAG_DISABLE_DEPS_OPTIMIZER", - "name": "disable-deps-optimizer", - "type": "boolean" - }, - "disable-version-check": { + "poll": { + "description": "Force polling to detect file changes.", + "env": "SHOPIFY_FLAG_POLL", + "hidden": true, + "name": "poll", "allowNo": false, - "description": "Skip the version check when running `hydrogen dev`", - "name": "disable-version-check", - "required": false, "type": "boolean" }, - "disable-virtual-routes": { + "theme-editor-sync": { + "description": "Synchronize Theme Editor updates in the local theme files.", + "env": "SHOPIFY_FLAG_THEME_EDITOR_SYNC", + "name": "theme-editor-sync", "allowNo": false, - "description": "Disable rendering fallback routes when a route file doesn't exist.", - "env": "SHOPIFY_HYDROGEN_FLAG_DISABLE_VIRTUAL_ROUTES", - "name": "disable-virtual-routes", "type": "boolean" }, - "entry": { - "description": "Entry file for the worker. Defaults to `./server`.", - "env": "SHOPIFY_HYDROGEN_FLAG_ENTRY", + "port": { + "description": "Local port to serve theme preview from.", + "env": "SHOPIFY_FLAG_PORT", + "name": "port", "hasDynamicHelp": false, "multiple": false, - "name": "entry", "type": "option" }, - "env": { - "description": "Specifies the environment to perform the operation using its handle. Fetch the handle using the `env list` command.", - "exclusive": [ - "env-branch" - ], + "theme": { + "char": "t", + "description": "Theme ID or name of the remote theme.", + "env": "SHOPIFY_FLAG_THEME_ID", + "name": "theme", "hasDynamicHelp": false, "multiple": false, - "name": "env", "type": "option" }, - "env-branch": { - "deprecated": { - "message": "--env-branch is deprecated. Use --env instead.", - "to": "env" - }, - "description": "Specifies the environment to perform the operation using its Git branch name.", - "env": "SHOPIFY_HYDROGEN_ENVIRONMENT_BRANCH", + "listing": { + "description": "The listing preset to use for multi-preset themes. Applies preset files from listings/[preset-name] directory.", + "env": "SHOPIFY_FLAG_LISTING", + "name": "listing", "hasDynamicHelp": false, "multiple": false, - "name": "env-branch", "type": "option" }, - "env-file": { - "default": ".env", - "description": "Path to an environment file to override existing environment variables. Defaults to the '.env' located in your project path `--path`.", - "hasDynamicHelp": false, - "multiple": false, - "name": "env-file", - "required": false, - "type": "option" - }, - "host": { + "nodelete": { + "char": "n", + "description": "Prevents files from being deleted in the remote theme when a file has been deleted locally. This applies to files that are deleted while the command is running, and files that have been deleted locally before the command is run.", + "env": "SHOPIFY_FLAG_NODELETE", + "name": "nodelete", "allowNo": false, - "description": "Expose the server to the local network", - "name": "host", - "required": false, "type": "boolean" }, - "inspector-port": { - "description": "The port where the inspector is available. Defaults to 9229.", - "env": "SHOPIFY_HYDROGEN_FLAG_INSPECTOR_PORT", + "only": { + "char": "o", + "description": "Hot reload only files that match the specified pattern.", + "env": "SHOPIFY_FLAG_ONLY", + "name": "only", "hasDynamicHelp": false, - "multiple": false, - "name": "inspector-port", + "multiple": true, "type": "option" }, - "path": { - "description": "The path to the directory of the Hydrogen storefront. Defaults to the current directory where the command is run.", - "env": "SHOPIFY_HYDROGEN_FLAG_PATH", + "ignore": { + "char": "x", + "description": "Skip hot reloading any files that match the specified pattern.", + "env": "SHOPIFY_FLAG_IGNORE", + "name": "ignore", + "hasDynamicHelp": false, + "multiple": true, + "type": "option" + }, + "force": { + "char": "f", + "description": "Proceed without confirmation, if current directory does not seem to be theme directory.", + "env": "SHOPIFY_FLAG_FORCE", + "hidden": true, + "name": "force", + "allowNo": false, + "type": "boolean" + }, + "notify": { + "description": "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", + "name": "notify", "hasDynamicHelp": false, "multiple": false, - "name": "path", "type": "option" }, - "port": { - "description": "The port to run the server on. Defaults to 3000.", - "env": "SHOPIFY_HYDROGEN_FLAG_PORT", + "open": { + "description": "Automatically launch the theme preview in your default web browser.", + "env": "SHOPIFY_FLAG_OPEN", + "name": "open", + "allowNo": false, + "type": "boolean" + }, + "store-password": { + "description": "The password for storefronts with password protection.", + "env": "SHOPIFY_FLAG_STORE_PASSWORD", + "name": "store-password", "hasDynamicHelp": false, "multiple": false, - "name": "port", - "required": false, "type": "option" }, - "verbose": { + "allow-live": { + "char": "a", + "description": "Allow development on a live theme.", + "env": "SHOPIFY_FLAG_ALLOW_LIVE", + "name": "allow-live", "allowNo": false, - "description": "Outputs more information about the command's execution.", - "env": "SHOPIFY_HYDROGEN_FLAG_VERBOSE", - "name": "verbose", - "required": false, "type": "boolean" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "hydrogen:dev", + "hiddenAliases": [], + "id": "theme:dev", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", - "strict": true + "strict": true, + "summary": "Uploads the current theme as a development theme to the connected store, then prints theme editor and preview URLs to your terminal. While running, changes will push to the store in real time.", + "descriptionWithMarkdown": "\n Uploads the current theme as the specified theme, or a [development theme](https://shopify.dev/docs/themes/tools/cli#development-themes), to a store so you can preview it.\n\nThis command returns the following information:\n\n- A link to your development theme at http://127.0.0.1:9292. This URL can hot reload local changes to CSS and sections, or refresh the entire page when a file changes, enabling you to preview changes in real time using the store's data.\n\n You can specify a different network interface and port using `--host` and `--port`.\n\n- A link to the [editor](https://shopify.dev/docs/themes/tools/online-editor) for the theme in the Shopify admin.\n\n- A [preview link](https://help.shopify.com/manual/online-store/themes/adding-themes#share-a-theme-preview-with-others) that you can share with other developers.\n\nIf you already have a development theme for your current environment, then this command replaces the development theme with your local theme. You can override this using the `--theme-editor-sync` flag.\n\n> Note: You can't preview checkout customizations using http://127.0.0.1:9292.\n\nDevelopment themes are deleted when you run `shopify auth logout`. If you need a preview link that can be used after you log out, then you should [share](https://shopify.dev/docs/api/shopify-cli/theme/theme-share) your theme or [push](https://shopify.dev/docs/api/shopify-cli/theme/theme-push) to an unpublished theme on your store.\n\nYou can run this command only in a directory that matches the [default Shopify theme folder structure](https://shopify.dev/docs/themes/tools/cli#directory-structure).", + "multiEnvironmentsFlags": null, + "customPluginName": "@shopify/theme" }, - "hydrogen:env:list": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/cli-hydrogen", - "description": "List the environments on your linked Hydrogen storefront.", - "descriptionWithMarkdown": "Lists all environments available on the linked Hydrogen storefront.", - "enableJsonFlag": false, + "theme:duplicate": { + "aliases": [], + "args": {}, + "description": "If you want to duplicate your local theme, you need to run `shopify theme push` first.\n\nIf no theme ID is specified, you're prompted to select the theme that you want to duplicate from the list of themes in your store. You're asked to confirm that you want to duplicate the specified theme.\n\nPrompts and confirmations are not shown when duplicate is run in a CI environment or the `--force` flag is used, therefore you must specify a theme ID using the `--theme` flag.\n\nYou can optionally name the duplicated theme using the `--name` flag.\n\nIf you use the `--json` flag, then theme information is returned in JSON format, which can be used as a machine-readable input for scripts or continuous integration.\n\nSample JSON output:\n\n```json\n{\n \"theme\": {\n \"id\": 108267175958,\n \"name\": \"A Duplicated Theme\",\n \"role\": \"unpublished\",\n \"shop\": \"mystore.myshopify.com\"\n }\n}\n```\n\n```json\n{\n \"message\": \"The theme 'Summer Edition' could not be duplicated due to errors\",\n \"errors\": [\"Maximum number of themes reached\"],\n \"requestId\": \"12345-abcde-67890\"\n}\n```", "flags": { - "path": { - "description": "The path to the directory of the Hydrogen storefront. Defaults to the current directory where the command is run.", - "env": "SHOPIFY_HYDROGEN_FLAG_PATH", + "no-color": { + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", + "allowNo": false, + "type": "boolean" + }, + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", + "allowNo": false, + "type": "boolean" + }, + "json": { + "char": "j", + "description": "Output the result as JSON. Automatically disables color output.", + "env": "SHOPIFY_FLAG_JSON", + "hidden": false, + "name": "json", + "allowNo": false, + "type": "boolean" + }, + "password": { + "description": "Password generated from the Theme Access app or an Admin API token.", + "env": "SHOPIFY_CLI_THEME_TOKEN", + "name": "password", "hasDynamicHelp": false, "multiple": false, - "name": "path", "type": "option" - } - }, - "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "hydrogen:env:list", - "pluginAlias": "@shopify/cli", - "pluginName": "@shopify/cli", - "pluginType": "core", - "strict": true - }, - "hydrogen:env:pull": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/cli-hydrogen", - "description": "Populate your .env with variables from your Hydrogen storefront.", - "descriptionWithMarkdown": "Pulls environment variables from the linked Hydrogen storefront and writes them to an `.env` file.", - "enableJsonFlag": false, - "flags": { - "env": { - "description": "Specifies the environment to perform the operation using its handle. Fetch the handle using the `env list` command.", - "exclusive": [ - "env-branch" - ], + }, + "theme": { + "char": "t", + "description": "Theme ID or name of the remote theme.", + "env": "SHOPIFY_FLAG_THEME_ID", + "name": "theme", "hasDynamicHelp": false, "multiple": false, - "name": "env", "type": "option" }, - "env-branch": { - "deprecated": { - "message": "--env-branch is deprecated. Use --env instead.", - "to": "env" - }, - "description": "Specifies the environment to perform the operation using its Git branch name.", - "env": "SHOPIFY_HYDROGEN_ENVIRONMENT_BRANCH", + "name": { + "char": "n", + "description": "Name of the newly duplicated theme.", + "env": "SHOPIFY_FLAG_NAME", + "name": "name", "hasDynamicHelp": false, "multiple": false, - "name": "env-branch", "type": "option" }, - "env-file": { - "default": ".env", - "description": "Path to an environment file to override existing environment variables. Defaults to the '.env' located in your project path `--path`.", + "store": { + "char": "s", + "description": "Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com, https://example.myshopify.com).", + "env": "SHOPIFY_FLAG_STORE", + "name": "store", "hasDynamicHelp": false, "multiple": false, - "name": "env-file", - "required": false, + "type": "option" + }, + "environment": { + "char": "e", + "description": "The environment to apply to the current command.", + "env": "SHOPIFY_FLAG_ENVIRONMENT", + "name": "environment", + "hasDynamicHelp": false, + "multiple": true, "type": "option" }, "force": { - "allowNo": false, "char": "f", - "description": "Overwrites the destination directory and files if they already exist.", - "env": "SHOPIFY_HYDROGEN_FLAG_FORCE", + "description": "Force the duplicate operation to run without prompts or confirmations.", + "env": "SHOPIFY_FLAG_FORCE", "name": "force", + "allowNo": false, "type": "boolean" - }, - "path": { - "description": "The path to the directory of the Hydrogen storefront. Defaults to the current directory where the command is run.", - "env": "SHOPIFY_HYDROGEN_FLAG_PATH", - "hasDynamicHelp": false, - "multiple": false, - "name": "path", - "type": "option" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "hydrogen:env:pull", + "hiddenAliases": [], + "id": "theme:duplicate", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", - "strict": true - }, - "hydrogen:env:push": { - "aliases": [ + "strict": true, + "summary": "Duplicates a theme from your theme library.", + "usage": [ + "theme duplicate", + "theme duplicate --theme 10 --name 'New Theme'" ], - "args": { - }, - "customPluginName": "@shopify/cli-hydrogen", - "description": "Push environment variables from the local .env file to your linked Hydrogen storefront.", - "enableJsonFlag": false, + "descriptionWithMarkdown": "If you want to duplicate your local theme, you need to run `shopify theme push` first.\n\nIf no theme ID is specified, you're prompted to select the theme that you want to duplicate from the list of themes in your store. You're asked to confirm that you want to duplicate the specified theme.\n\nPrompts and confirmations are not shown when duplicate is run in a CI environment or the `--force` flag is used, therefore you must specify a theme ID using the `--theme` flag.\n\nYou can optionally name the duplicated theme using the `--name` flag.\n\nIf you use the `--json` flag, then theme information is returned in JSON format, which can be used as a machine-readable input for scripts or continuous integration.\n\nSample JSON output:\n\n```json\n{\n \"theme\": {\n \"id\": 108267175958,\n \"name\": \"A Duplicated Theme\",\n \"role\": \"unpublished\",\n \"shop\": \"mystore.myshopify.com\"\n }\n}\n```\n\n```json\n{\n \"message\": \"The theme 'Summer Edition' could not be duplicated due to errors\",\n \"errors\": [\"Maximum number of themes reached\"],\n \"requestId\": \"12345-abcde-67890\"\n}\n```", + "customPluginName": "@shopify/theme" + }, + "theme:info": { + "aliases": [], + "args": {}, + "description": "Displays information about your theme environment, including your current store. Can also retrieve information about a specific theme.", "flags": { - "env": { - "description": "Specifies the environment to perform the operation using its handle. Fetch the handle using the `env list` command.", - "exclusive": [ - "env-branch" - ], - "hasDynamicHelp": false, - "multiple": false, - "name": "env", - "type": "option" + "no-color": { + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", + "allowNo": false, + "type": "boolean" }, - "env-file": { - "default": ".env", - "description": "Path to an environment file to override existing environment variables. Defaults to the '.env' located in your project path `--path`.", - "hasDynamicHelp": false, - "multiple": false, - "name": "env-file", - "required": false, - "type": "option" + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", + "allowNo": false, + "type": "boolean" }, - "path": { - "description": "The path to the directory of the Hydrogen storefront. Defaults to the current directory where the command is run.", - "env": "SHOPIFY_HYDROGEN_FLAG_PATH", - "hasDynamicHelp": false, - "multiple": false, + "json": { + "char": "j", + "description": "Output the result as JSON. Automatically disables color output.", + "env": "SHOPIFY_FLAG_JSON", + "hidden": false, + "name": "json", + "allowNo": false, + "type": "boolean" + }, + "path": { + "description": "The path where you want to run the command. Defaults to the current working directory.", + "env": "SHOPIFY_FLAG_PATH", "name": "path", - "type": "option" - } - }, - "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "hydrogen:env:push", - "pluginAlias": "@shopify/cli", - "pluginName": "@shopify/cli", - "pluginType": "core", - "strict": true - }, - "hydrogen:g": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/cli-hydrogen", - "description": "Shortcut for `hydrogen generate`. See `hydrogen generate --help` for more information.", - "enableJsonFlag": false, - "flags": { - }, - "hasDynamicHelp": false, - "hidden": true, - "hiddenAliases": [ - ], - "id": "hydrogen:g", - "pluginAlias": "@shopify/cli", - "pluginName": "@shopify/cli", - "pluginType": "core", - "strict": false - }, - "hydrogen:generate:route": { - "aliases": [ - ], - "args": { - "routeName": { - "description": "The route to generate. One of home,page,cart,products,collections,policies,blogs,account,search,robots,sitemap,tokenlessApi,all.", - "name": "routeName", - "options": [ - "home", - "page", - "cart", - "products", - "collections", - "policies", - "blogs", - "account", - "search", - "robots", - "sitemap", - "tokenlessApi", - "all" - ], - "required": true - } - }, - "customPluginName": "@shopify/cli-hydrogen", - "description": "Generates a standard Shopify route.", - "descriptionWithMarkdown": "Generates a set of default routes from the starter template.", - "enableJsonFlag": false, - "flags": { - "adapter": { - "description": "Remix adapter used in the route. The default is `@shopify/remix-oxygen`.", - "env": "SHOPIFY_HYDROGEN_FLAG_ADAPTER", + "noCacheDefault": true, "hasDynamicHelp": false, "multiple": false, - "name": "adapter", "type": "option" }, - "force": { - "allowNo": false, - "char": "f", - "description": "Overwrites the destination directory and files if they already exist.", - "env": "SHOPIFY_HYDROGEN_FLAG_FORCE", - "name": "force", - "type": "boolean" - }, - "locale-param": { - "description": "The param name in Remix routes for the i18n locale, if any. Example: `locale` becomes ($locale).", - "env": "SHOPIFY_HYDROGEN_FLAG_ADAPTER", + "password": { + "description": "Password generated from the Theme Access app or an Admin API token.", + "env": "SHOPIFY_CLI_THEME_TOKEN", + "name": "password", "hasDynamicHelp": false, "multiple": false, - "name": "locale-param", "type": "option" }, - "path": { - "description": "The path to the directory of the Hydrogen storefront. Defaults to the current directory where the command is run.", - "env": "SHOPIFY_HYDROGEN_FLAG_PATH", + "store": { + "char": "s", + "description": "Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com, https://example.myshopify.com).", + "env": "SHOPIFY_FLAG_STORE", + "name": "store", "hasDynamicHelp": false, "multiple": false, - "name": "path", "type": "option" }, - "typescript": { + "environment": { + "char": "e", + "description": "The environment to apply to the current command.", + "env": "SHOPIFY_FLAG_ENVIRONMENT", + "name": "environment", + "hasDynamicHelp": false, + "multiple": true, + "type": "option" + }, + "development": { + "char": "d", + "description": "Retrieve info from your development theme.", + "env": "SHOPIFY_FLAG_DEVELOPMENT", + "name": "development", "allowNo": false, - "description": "Generate TypeScript files", - "env": "SHOPIFY_HYDROGEN_FLAG_TYPESCRIPT", - "name": "typescript", "type": "boolean" + }, + "theme": { + "char": "t", + "description": "Theme ID or name of the remote theme.", + "env": "SHOPIFY_FLAG_THEME_ID", + "name": "theme", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "hydrogen:generate:route", + "hiddenAliases": [], + "id": "theme:info", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", - "strict": true - }, - "hydrogen:generate:routes": { - "aliases": [ + "strict": true, + "multiEnvironmentsFlags": [ + "store", + "password" ], - "args": { - }, - "customPluginName": "@shopify/cli-hydrogen", - "description": "Generates all supported standard shopify routes.", - "enableJsonFlag": false, + "customPluginName": "@shopify/theme" + }, + "theme:language-server": { + "aliases": [], + "args": {}, + "description": "Starts the \"Language Server\" (https://shopify.dev/docs/themes/tools/cli/language-server).", "flags": { - "adapter": { - "description": "Remix adapter used in the route. The default is `@shopify/remix-oxygen`.", - "env": "SHOPIFY_HYDROGEN_FLAG_ADAPTER", - "hasDynamicHelp": false, - "multiple": false, - "name": "adapter", - "type": "option" - }, - "force": { + "no-color": { + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", "allowNo": false, - "char": "f", - "description": "Overwrites the destination directory and files if they already exist.", - "env": "SHOPIFY_HYDROGEN_FLAG_FORCE", - "name": "force", "type": "boolean" }, - "locale-param": { - "description": "The param name in Remix routes for the i18n locale, if any. Example: `locale` becomes ($locale).", - "env": "SHOPIFY_HYDROGEN_FLAG_ADAPTER", - "hasDynamicHelp": false, - "multiple": false, - "name": "locale-param", - "type": "option" - }, - "path": { - "description": "The path to the directory of the Hydrogen storefront. Defaults to the current directory where the command is run.", - "env": "SHOPIFY_HYDROGEN_FLAG_PATH", - "hasDynamicHelp": false, - "multiple": false, - "name": "path", - "type": "option" - }, - "typescript": { + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", "allowNo": false, - "description": "Generate TypeScript files", - "env": "SHOPIFY_HYDROGEN_FLAG_TYPESCRIPT", - "name": "typescript", "type": "boolean" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "hydrogen:generate:routes", + "hiddenAliases": [], + "id": "theme:language-server", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", - "strict": true + "strict": true, + "summary": "Start a Language Server Protocol server.", + "descriptionWithMarkdown": "Starts the [Language Server](https://shopify.dev/docs/themes/tools/cli/language-server).", + "multiEnvironmentsFlags": null, + "customPluginName": "@shopify/theme" }, - "hydrogen:init": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/cli-hydrogen", - "description": "Creates a new Hydrogen storefront.", - "descriptionWithMarkdown": "Creates a new Hydrogen storefront.", - "enableJsonFlag": false, + "theme:list": { + "aliases": [], + "args": {}, + "description": "Lists the themes in your store, along with their IDs and statuses.", "flags": { - "force": { + "no-color": { + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", "allowNo": false, - "char": "f", - "description": "Overwrites the destination directory and files if they already exist.", - "env": "SHOPIFY_HYDROGEN_FLAG_FORCE", - "name": "force", "type": "boolean" }, - "git": { - "allowNo": true, - "description": "Init Git and create initial commits.", - "env": "SHOPIFY_HYDROGEN_FLAG_GIT", - "name": "git", + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", + "allowNo": false, "type": "boolean" }, - "install-deps": { - "allowNo": true, - "description": "Auto installs dependencies using the active package manager.", - "env": "SHOPIFY_HYDROGEN_FLAG_INSTALL_DEPS", - "name": "install-deps", + "json": { + "char": "j", + "description": "Output the result as JSON. Automatically disables color output.", + "env": "SHOPIFY_FLAG_JSON", + "hidden": false, + "name": "json", + "allowNo": false, "type": "boolean" }, - "language": { - "description": "Sets the template language to use. One of `js` or `ts`.", - "env": "SHOPIFY_HYDROGEN_FLAG_LANGUAGE", + "path": { + "description": "The path where you want to run the command. Defaults to the current working directory.", + "env": "SHOPIFY_FLAG_PATH", + "name": "path", + "noCacheDefault": true, "hasDynamicHelp": false, "multiple": false, - "name": "language", "type": "option" }, - "markets": { - "description": "Sets the URL structure to support multiple markets. Must be one of: `subfolders`, `domains`, `subdomains`, `none`. Example: `--markets subfolders`.", - "env": "SHOPIFY_HYDROGEN_FLAG_I18N", + "password": { + "description": "Password generated from the Theme Access app or an Admin API token.", + "env": "SHOPIFY_CLI_THEME_TOKEN", + "name": "password", "hasDynamicHelp": false, "multiple": false, - "name": "markets", "type": "option" }, - "mock-shop": { - "allowNo": false, - "description": "Use mock.shop as the data source for the storefront.", - "env": "SHOPIFY_HYDROGEN_FLAG_MOCK_DATA", - "name": "mock-shop", - "type": "boolean" - }, - "package-manager": { - "env": "SHOPIFY_HYDROGEN_FLAG_PACKAGE_MANAGER", + "store": { + "char": "s", + "description": "Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com, https://example.myshopify.com).", + "env": "SHOPIFY_FLAG_STORE", + "name": "store", "hasDynamicHelp": false, - "hidden": true, "multiple": false, - "name": "package-manager", - "options": [ - "npm", - "yarn", - "pnpm", - "unknown" - ], "type": "option" }, - "path": { - "description": "The path to the directory of the new Hydrogen storefront.", - "env": "SHOPIFY_HYDROGEN_FLAG_PATH", + "environment": { + "char": "e", + "description": "The environment to apply to the current command.", + "env": "SHOPIFY_FLAG_ENVIRONMENT", + "name": "environment", "hasDynamicHelp": false, - "multiple": false, - "name": "path", + "multiple": true, "type": "option" }, - "quickstart": { - "allowNo": false, - "description": "Scaffolds a new Hydrogen project with a set of sensible defaults. Equivalent to `shopify hydrogen init --path hydrogen-quickstart --mock-shop --language js --shortcut --markets none`", - "env": "SHOPIFY_HYDROGEN_FLAG_QUICKSTART", - "name": "quickstart", - "type": "boolean" - }, - "shortcut": { - "allowNo": true, - "description": "Creates a global h2 shortcut for Shopify CLI using shell aliases. Deactivate with `--no-shortcut`.", - "env": "SHOPIFY_HYDROGEN_FLAG_SHORTCUT", - "name": "shortcut", - "type": "boolean" + "role": { + "description": "Only list themes with the given role.", + "env": "SHOPIFY_FLAG_ROLE", + "name": "role", + "hasDynamicHelp": false, + "multiple": false, + "options": [ + "live", + "unpublished", + "development" + ], + "type": "option" }, - "styling": { - "description": "Sets the styling strategy to use. One of `tailwind`, `vanilla-extract`, `css-modules`, `postcss`, `none`.", - "env": "SHOPIFY_HYDROGEN_FLAG_STYLING", + "name": { + "description": "Only list themes that contain the given name.", + "env": "SHOPIFY_FLAG_NAME", + "name": "name", "hasDynamicHelp": false, "multiple": false, - "name": "styling", "type": "option" }, - "template": { - "description": "Scaffolds project based on an existing template or example from the Hydrogen repository.", - "env": "SHOPIFY_HYDROGEN_FLAG_TEMPLATE", + "id": { + "description": "Only list theme with the given ID.", + "env": "SHOPIFY_FLAG_ID", + "name": "id", "hasDynamicHelp": false, "multiple": false, - "name": "template", "type": "option" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "hydrogen:init", + "hiddenAliases": [], + "id": "theme:list", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", - "strict": true - }, - "hydrogen:link": { - "aliases": [ + "strict": true, + "multiEnvironmentsFlags": [ + "store", + "password" ], - "args": { - }, - "customPluginName": "@shopify/cli-hydrogen", - "description": "Link a local project to one of your shop's Hydrogen storefronts.", - "descriptionWithMarkdown": "Links your local development environment to a remote Hydrogen storefront. You can link an unlimited number of development environments to a single Hydrogen storefront.\n\n Linking to a Hydrogen storefront enables you to run [dev](https://shopify.dev/docs/api/shopify-cli/hydrogen/hydrogen-dev) and automatically inject your linked Hydrogen storefront's environment variables directly into the server runtime.\n\n After you run the `link` command, you can access the [env list](https://shopify.dev/docs/api/shopify-cli/hydrogen/hydrogen-env-list), [env pull](https://shopify.dev/docs/api/shopify-cli/hydrogen/hydrogen-env-pull), and [unlink](https://shopify.dev/docs/api/shopify-cli/hydrogen/hydrogen-unlink) commands.", - "enableJsonFlag": false, + "customPluginName": "@shopify/theme" + }, + "theme:metafields:pull": { + "aliases": [], + "args": {}, + "description": "Retrieves metafields from Shopify Admin.\n\nIf the metafields file already exists, it will be overwritten.", "flags": { - "force": { + "no-color": { + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", + "allowNo": false, + "type": "boolean" + }, + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", "allowNo": false, - "char": "f", - "description": "Overwrites the destination directory and files if they already exist.", - "env": "SHOPIFY_HYDROGEN_FLAG_FORCE", - "name": "force", "type": "boolean" }, "path": { - "description": "The path to the directory of the Hydrogen storefront. Defaults to the current directory where the command is run.", - "env": "SHOPIFY_HYDROGEN_FLAG_PATH", + "description": "The path where you want to run the command. Defaults to the current working directory.", + "env": "SHOPIFY_FLAG_PATH", + "name": "path", + "noCacheDefault": true, "hasDynamicHelp": false, "multiple": false, - "name": "path", "type": "option" }, - "storefront": { - "description": "The name of a Hydrogen Storefront (e.g. \"Jane's Apparel\")", - "env": "SHOPIFY_HYDROGEN_STOREFRONT", + "password": { + "description": "Password generated from the Theme Access app or an Admin API token.", + "env": "SHOPIFY_CLI_THEME_TOKEN", + "name": "password", "hasDynamicHelp": false, "multiple": false, - "name": "storefront", "type": "option" - } - }, - "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "hydrogen:link", - "pluginAlias": "@shopify/cli", - "pluginName": "@shopify/cli", - "pluginType": "core", - "strict": true - }, - "hydrogen:list": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/cli-hydrogen", - "description": "Returns a list of Hydrogen storefronts available on a given shop.", - "descriptionWithMarkdown": "Lists all remote Hydrogen storefronts available to link to your local development environment.", - "enableJsonFlag": false, - "flags": { - "path": { - "description": "The path to the directory of the Hydrogen storefront. Defaults to the current directory where the command is run.", - "env": "SHOPIFY_HYDROGEN_FLAG_PATH", + }, + "store": { + "char": "s", + "description": "Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com, https://example.myshopify.com).", + "env": "SHOPIFY_FLAG_STORE", + "name": "store", "hasDynamicHelp": false, "multiple": false, - "name": "path", "type": "option" + }, + "environment": { + "char": "e", + "description": "The environment to apply to the current command.", + "env": "SHOPIFY_FLAG_ENVIRONMENT", + "name": "environment", + "hasDynamicHelp": false, + "multiple": true, + "type": "option" + }, + "force": { + "char": "f", + "description": "Proceed without confirmation, if current directory does not seem to be theme directory.", + "env": "SHOPIFY_FLAG_FORCE", + "hidden": true, + "name": "force", + "allowNo": false, + "type": "boolean" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "hydrogen:list", + "hiddenAliases": [], + "id": "theme:metafields:pull", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", - "strict": true + "strict": true, + "summary": "Download metafields definitions from your shop into a local file.", + "descriptionWithMarkdown": "Retrieves metafields from Shopify Admin.\n\nIf the metafields file already exists, it will be overwritten.", + "multiEnvironmentsFlags": null, + "customPluginName": "@shopify/theme" }, - "hydrogen:login": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/cli-hydrogen", - "description": "Login to your Shopify account.", - "descriptionWithMarkdown": "Logs in to the specified shop and saves the shop domain to the project.", - "enableJsonFlag": false, + "theme:open": { + "aliases": [], + "args": {}, + "description": "Returns links that let you preview the specified theme. The following links are returned:\n\n - A link to the \"editor\" (https://shopify.dev/docs/themes/tools/online-editor) for the theme in the Shopify admin.\n - A \"preview link\" (https://help.shopify.com/manual/online-store/themes/adding-themes#share-a-theme-preview-with-others) that you can share with other developers.\n\n If you don't specify a theme, then you're prompted to select the theme to open from the list of the themes in your store.", "flags": { + "no-color": { + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", + "allowNo": false, + "type": "boolean" + }, + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", + "allowNo": false, + "type": "boolean" + }, "path": { - "description": "The path to the directory of the Hydrogen storefront. Defaults to the current directory where the command is run.", - "env": "SHOPIFY_HYDROGEN_FLAG_PATH", + "description": "The path where you want to run the command. Defaults to the current working directory.", + "env": "SHOPIFY_FLAG_PATH", + "name": "path", + "noCacheDefault": true, "hasDynamicHelp": false, "multiple": false, - "name": "path", "type": "option" }, - "shop": { - "char": "s", - "description": "Shop URL. It can be the shop prefix (janes-apparel) or the full myshopify.com URL (janes-apparel.myshopify.com, https://janes-apparel.myshopify.com).", - "env": "SHOPIFY_SHOP", + "password": { + "description": "Password generated from the Theme Access app or an Admin API token.", + "env": "SHOPIFY_CLI_THEME_TOKEN", + "name": "password", "hasDynamicHelp": false, "multiple": false, - "name": "shop", "type": "option" - } - }, - "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "hydrogen:login", - "pluginAlias": "@shopify/cli", - "pluginName": "@shopify/cli", - "pluginType": "core", - "strict": true - }, - "hydrogen:logout": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/cli-hydrogen", - "description": "Logout of your local session.", - "descriptionWithMarkdown": "Log out from the current shop.", - "enableJsonFlag": false, - "flags": { - "path": { - "description": "The path to the directory of the Hydrogen storefront. Defaults to the current directory where the command is run.", - "env": "SHOPIFY_HYDROGEN_FLAG_PATH", + }, + "store": { + "char": "s", + "description": "Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com, https://example.myshopify.com).", + "env": "SHOPIFY_FLAG_STORE", + "name": "store", "hasDynamicHelp": false, "multiple": false, - "name": "path", "type": "option" - } - }, - "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "hydrogen:logout", + }, + "environment": { + "char": "e", + "description": "The environment to apply to the current command.", + "env": "SHOPIFY_FLAG_ENVIRONMENT", + "name": "environment", + "hasDynamicHelp": false, + "multiple": true, + "type": "option" + }, + "development": { + "char": "d", + "description": "Open your development theme.", + "env": "SHOPIFY_FLAG_DEVELOPMENT", + "name": "development", + "allowNo": false, + "type": "boolean" + }, + "editor": { + "char": "E", + "description": "Open the theme editor for the specified theme in the browser.", + "env": "SHOPIFY_FLAG_EDITOR", + "name": "editor", + "allowNo": false, + "type": "boolean" + }, + "live": { + "char": "l", + "description": "Open your live (published) theme.", + "env": "SHOPIFY_FLAG_LIVE", + "name": "live", + "allowNo": false, + "type": "boolean" + }, + "theme": { + "char": "t", + "description": "Theme ID or name of the remote theme.", + "env": "SHOPIFY_FLAG_THEME_ID", + "name": "theme", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + } + }, + "hasDynamicHelp": false, + "hiddenAliases": [], + "id": "theme:open", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", - "strict": true + "strict": true, + "summary": "Opens the preview of your remote theme.", + "descriptionWithMarkdown": "Returns links that let you preview the specified theme. The following links are returned:\n\n - A link to the [editor](https://shopify.dev/docs/themes/tools/online-editor) for the theme in the Shopify admin.\n - A [preview link](https://help.shopify.com/manual/online-store/themes/adding-themes#share-a-theme-preview-with-others) that you can share with other developers.\n\n If you don't specify a theme, then you're prompted to select the theme to open from the list of the themes in your store.", + "multiEnvironmentsFlags": null, + "customPluginName": "@shopify/theme" }, - "hydrogen:preview": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/cli-hydrogen", - "description": "Runs a Hydrogen storefront in an Oxygen worker for production.", - "descriptionWithMarkdown": "Runs a server in your local development environment that serves your Hydrogen app's production build. Requires running the [build](https://shopify.dev/docs/api/shopify-cli/hydrogen/hydrogen-build) command first.", - "enableJsonFlag": false, + "theme:package": { + "aliases": [], + "args": {}, + "description": "Packages your local theme files into a ZIP file that can be uploaded to Shopify.\n\n Only folders that match the \"default Shopify theme folder structure\" (https://shopify.dev/docs/storefronts/themes/tools/cli#directory-structure) are included in the package.\n\n The package includes the `listings` directory if present (required for multi-preset themes per \"Theme Store requirements\" (https://shopify.dev/docs/storefronts/themes/store/requirements#adding-presets-to-your-theme-zip-submission)).\n\n The ZIP file uses the name `theme_name-theme_version.zip`, based on parameters in your \"settings_schema.json\" (https://shopify.dev/docs/storefronts/themes/architecture/config/settings-schema-json) file.", "flags": { - "build": { + "no-color": { + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", "allowNo": false, - "description": "Builds the app before starting the preview server.", - "name": "build", "type": "boolean" }, - "codegen": { + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", "allowNo": false, - "dependsOn": [ - "build" - ], - "description": "Automatically generates GraphQL types for your project’s Storefront API queries.", - "name": "codegen", - "required": false, "type": "boolean" }, - "codegen-config-path": { - "dependsOn": [ - "codegen" - ], - "description": "Specifies a path to a codegen configuration file. Defaults to `/codegen.ts` if this file exists.", + "path": { + "description": "The path where you want to run the command. Defaults to the current working directory.", + "env": "SHOPIFY_FLAG_PATH", + "name": "path", + "noCacheDefault": true, "hasDynamicHelp": false, "multiple": false, - "name": "codegen-config-path", - "required": false, "type": "option" + } + }, + "hasDynamicHelp": false, + "hiddenAliases": [], + "id": "theme:package", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true, + "summary": "Package your theme into a .zip file, ready to upload to the Online Store.", + "descriptionWithMarkdown": "Packages your local theme files into a ZIP file that can be uploaded to Shopify.\n\n Only folders that match the [default Shopify theme folder structure](https://shopify.dev/docs/storefronts/themes/tools/cli#directory-structure) are included in the package.\n\n The package includes the `listings` directory if present (required for multi-preset themes per [Theme Store requirements](https://shopify.dev/docs/storefronts/themes/store/requirements#adding-presets-to-your-theme-zip-submission)).\n\n The ZIP file uses the name `theme_name-theme_version.zip`, based on parameters in your [settings_schema.json](https://shopify.dev/docs/storefronts/themes/architecture/config/settings-schema-json) file.", + "multiEnvironmentsFlags": null, + "customPluginName": "@shopify/theme" + }, + "theme:profile": { + "aliases": [], + "args": {}, + "description": "Profile the Shopify Liquid on a given page.\n\n This command will open a web page with the Speedscope profiler detailing the time spent executing Liquid on the given page.", + "flags": { + "no-color": { + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", + "allowNo": false, + "type": "boolean" }, - "debug": { + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", "allowNo": false, - "description": "Enables inspector connections to the server with a debugger such as Visual Studio Code or Chrome DevTools.", - "env": "SHOPIFY_HYDROGEN_FLAG_DEBUG", - "name": "debug", "type": "boolean" }, - "entry": { - "dependsOn": [ - "build" - ], - "description": "Entry file for the worker. Defaults to `./server`.", - "env": "SHOPIFY_HYDROGEN_FLAG_ENTRY", + "path": { + "description": "The path where you want to run the command. Defaults to the current working directory.", + "env": "SHOPIFY_FLAG_PATH", + "name": "path", + "noCacheDefault": true, "hasDynamicHelp": false, "multiple": false, - "name": "entry", "type": "option" }, - "env": { - "description": "Specifies the environment to perform the operation using its handle. Fetch the handle using the `env list` command.", - "exclusive": [ - "env-branch" - ], + "password": { + "description": "Password generated from the Theme Access app or an Admin API token.", + "env": "SHOPIFY_CLI_THEME_TOKEN", + "name": "password", "hasDynamicHelp": false, "multiple": false, - "name": "env", "type": "option" }, - "env-branch": { - "deprecated": { - "message": "--env-branch is deprecated. Use --env instead.", - "to": "env" - }, - "description": "Specifies the environment to perform the operation using its Git branch name.", - "env": "SHOPIFY_HYDROGEN_ENVIRONMENT_BRANCH", + "store": { + "char": "s", + "description": "Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com, https://example.myshopify.com).", + "env": "SHOPIFY_FLAG_STORE", + "name": "store", "hasDynamicHelp": false, "multiple": false, - "name": "env-branch", "type": "option" }, - "env-file": { - "default": ".env", - "description": "Path to an environment file to override existing environment variables. Defaults to the '.env' located in your project path `--path`.", + "environment": { + "char": "e", + "description": "The environment to apply to the current command.", + "env": "SHOPIFY_FLAG_ENVIRONMENT", + "name": "environment", "hasDynamicHelp": false, - "multiple": false, - "name": "env-file", - "required": false, + "multiple": true, "type": "option" }, - "inspector-port": { - "description": "The port where the inspector is available. Defaults to 9229.", - "env": "SHOPIFY_HYDROGEN_FLAG_INSPECTOR_PORT", + "theme": { + "char": "t", + "description": "Theme ID or name of the remote theme.", + "env": "SHOPIFY_FLAG_THEME_ID", + "name": "theme", "hasDynamicHelp": false, "multiple": false, - "name": "inspector-port", "type": "option" }, - "path": { - "description": "The path to the directory of the Hydrogen storefront. Defaults to the current directory where the command is run.", - "env": "SHOPIFY_HYDROGEN_FLAG_PATH", + "url": { + "description": "The url to be used as context", + "env": "SHOPIFY_FLAG_URL", + "name": "url", + "default": "/", "hasDynamicHelp": false, "multiple": false, - "name": "path", "type": "option" }, - "port": { - "description": "The port to run the server on. Defaults to 3000.", - "env": "SHOPIFY_HYDROGEN_FLAG_PORT", + "store-password": { + "description": "The password for storefronts with password protection.", + "env": "SHOPIFY_FLAG_STORE_PASSWORD", + "name": "store-password", "hasDynamicHelp": false, "multiple": false, - "name": "port", "type": "option" }, - "verbose": { - "allowNo": false, - "description": "Outputs more information about the command's execution.", - "env": "SHOPIFY_HYDROGEN_FLAG_VERBOSE", - "name": "verbose", - "required": false, - "type": "boolean" - }, - "watch": { + "json": { + "char": "j", + "description": "Output the result as JSON. Automatically disables color output.", + "env": "SHOPIFY_FLAG_JSON", + "hidden": false, + "name": "json", "allowNo": false, - "dependsOn": [ - "build" - ], - "description": "Watches for changes and rebuilds the project.", - "name": "watch", "type": "boolean" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "hydrogen:preview", + "hiddenAliases": [], + "id": "theme:profile", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", - "strict": true - }, - "hydrogen:setup": { - "aliases": [ + "strict": true, + "summary": "Profile the Liquid rendering of a theme page.", + "usage": [ + "theme profile", + "theme profile --url /products/classic-leather-jacket" ], - "args": { - }, - "customPluginName": "@shopify/cli-hydrogen", - "description": "Scaffold routes and core functionality.", - "enableJsonFlag": false, + "descriptionWithMarkdown": "Profile the Shopify Liquid on a given page.\n\n This command will open a web page with the Speedscope profiler detailing the time spent executing Liquid on the given page.", + "multiEnvironmentsFlags": null, + "customPluginName": "@shopify/theme" + }, + "theme:publish": { + "aliases": [], + "args": {}, + "description": "Publishes an unpublished theme from your theme library.\n\nIf no theme ID is specified, then you're prompted to select the theme that you want to publish from the list of themes in your store.\n\nYou can run this command only in a directory that matches the \"default Shopify theme folder structure\" (https://shopify.dev/docs/themes/tools/cli#directory-structure).\n\nIf you want to publish your local theme, then you need to run `shopify theme push` first. You're asked to confirm that you want to publish the specified theme. You can skip this confirmation using the `--force` flag.", "flags": { - "force": { + "no-color": { + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", "allowNo": false, - "char": "f", - "description": "Overwrites the destination directory and files if they already exist.", - "env": "SHOPIFY_HYDROGEN_FLAG_FORCE", - "name": "force", "type": "boolean" }, - "install-deps": { - "allowNo": true, - "description": "Auto installs dependencies using the active package manager.", - "env": "SHOPIFY_HYDROGEN_FLAG_INSTALL_DEPS", - "name": "install-deps", + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", + "allowNo": false, "type": "boolean" }, - "markets": { - "description": "Sets the URL structure to support multiple markets. Must be one of: `subfolders`, `domains`, `subdomains`, `none`. Example: `--markets subfolders`.", - "env": "SHOPIFY_HYDROGEN_FLAG_I18N", + "path": { + "description": "The path where you want to run the command. Defaults to the current working directory.", + "env": "SHOPIFY_FLAG_PATH", + "name": "path", + "noCacheDefault": true, "hasDynamicHelp": false, "multiple": false, - "name": "markets", "type": "option" }, - "path": { - "description": "The path to the directory of the Hydrogen storefront. Defaults to the current directory where the command is run.", - "env": "SHOPIFY_HYDROGEN_FLAG_PATH", + "password": { + "description": "Password generated from the Theme Access app or an Admin API token.", + "env": "SHOPIFY_CLI_THEME_TOKEN", + "name": "password", "hasDynamicHelp": false, "multiple": false, - "name": "path", "type": "option" }, - "shortcut": { - "allowNo": true, - "description": "Creates a global h2 shortcut for Shopify CLI using shell aliases. Deactivate with `--no-shortcut`.", - "env": "SHOPIFY_HYDROGEN_FLAG_SHORTCUT", - "name": "shortcut", + "store": { + "char": "s", + "description": "Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com, https://example.myshopify.com).", + "env": "SHOPIFY_FLAG_STORE", + "name": "store", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "environment": { + "char": "e", + "description": "The environment to apply to the current command.", + "env": "SHOPIFY_FLAG_ENVIRONMENT", + "name": "environment", + "hasDynamicHelp": false, + "multiple": true, + "type": "option" + }, + "force": { + "char": "f", + "description": "Skip confirmation.", + "env": "SHOPIFY_FLAG_FORCE", + "name": "force", + "allowNo": false, "type": "boolean" + }, + "theme": { + "char": "t", + "description": "Theme ID or name of the remote theme.", + "env": "SHOPIFY_FLAG_THEME_ID", + "name": "theme", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "hydrogen:setup", + "hiddenAliases": [], + "id": "theme:publish", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", - "strict": true - }, - "hydrogen:setup:css": { - "aliases": [ + "strict": true, + "summary": "Set a remote theme as the live theme.", + "descriptionWithMarkdown": "Publishes an unpublished theme from your theme library.\n\nIf no theme ID is specified, then you're prompted to select the theme that you want to publish from the list of themes in your store.\n\nYou can run this command only in a directory that matches the [default Shopify theme folder structure](https://shopify.dev/docs/themes/tools/cli#directory-structure).\n\nIf you want to publish your local theme, then you need to run `shopify theme push` first. You're asked to confirm that you want to publish the specified theme. You can skip this confirmation using the `--force` flag.", + "multiEnvironmentsFlags": [ + "store", + "password", + "theme" ], - "args": { - "strategy": { - "description": "The CSS strategy to setup. One of tailwind,vanilla-extract,css-modules,postcss", - "name": "strategy", - "options": [ - "tailwind", - "vanilla-extract", - "css-modules", - "postcss" - ] - } - }, - "customPluginName": "@shopify/cli-hydrogen", - "description": "Setup CSS strategies for your project.", - "descriptionWithMarkdown": "Adds support for certain CSS strategies to your project.", - "enableJsonFlag": false, + "customPluginName": "@shopify/theme" + }, + "theme:preview": { + "aliases": [], + "args": {}, + "description": "Applies a JSON overrides file to a theme and creates or updates a preview. This lets you quickly preview changes.\n\n The command returns a preview URL and a preview identifier. You can reuse the preview identifier with `--preview-id` to update an existing preview instead of creating a new one.", "flags": { - "force": { + "no-color": { + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", "allowNo": false, - "char": "f", - "description": "Overwrites the destination directory and files if they already exist.", - "env": "SHOPIFY_HYDROGEN_FLAG_FORCE", - "name": "force", "type": "boolean" }, - "install-deps": { - "allowNo": true, - "description": "Auto installs dependencies using the active package manager.", - "env": "SHOPIFY_HYDROGEN_FLAG_INSTALL_DEPS", - "name": "install-deps", + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", + "allowNo": false, "type": "boolean" }, "path": { - "description": "The path to the directory of the Hydrogen storefront. Defaults to the current directory where the command is run.", - "env": "SHOPIFY_HYDROGEN_FLAG_PATH", + "description": "The path where you want to run the command. Defaults to the current working directory.", + "env": "SHOPIFY_FLAG_PATH", + "name": "path", + "noCacheDefault": true, "hasDynamicHelp": false, "multiple": false, - "name": "path", "type": "option" - } - }, - "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "hydrogen:setup:css", - "pluginAlias": "@shopify/cli", - "pluginName": "@shopify/cli", - "pluginType": "core", - "strict": true - }, - "hydrogen:setup:markets": { - "aliases": [ - ], - "args": { - "strategy": { - "description": "The URL structure strategy to setup multiple markets. One of subfolders,domains,subdomains", - "name": "strategy", - "options": [ - "subfolders", - "domains", - "subdomains" - ] - } - }, - "customPluginName": "@shopify/cli-hydrogen", - "description": "Setup support for multiple markets in your project.", - "descriptionWithMarkdown": "Adds support for multiple [markets](https://shopify.dev/docs/custom-storefronts/hydrogen/markets) to your project by using the URL structure.", - "enableJsonFlag": false, - "flags": { - "path": { - "description": "The path to the directory of the Hydrogen storefront. Defaults to the current directory where the command is run.", - "env": "SHOPIFY_HYDROGEN_FLAG_PATH", + }, + "password": { + "description": "Password generated from the Theme Access app or an Admin API token.", + "env": "SHOPIFY_CLI_THEME_TOKEN", + "name": "password", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "store": { + "char": "s", + "description": "Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com, https://example.myshopify.com).", + "env": "SHOPIFY_FLAG_STORE", + "name": "store", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "environment": { + "char": "e", + "description": "The environment to apply to the current command.", + "env": "SHOPIFY_FLAG_ENVIRONMENT", + "name": "environment", + "hasDynamicHelp": false, + "multiple": true, + "type": "option" + }, + "theme": { + "char": "t", + "description": "Theme ID or name of the remote theme.", + "env": "SHOPIFY_FLAG_THEME_ID", + "name": "theme", + "required": true, + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "overrides": { + "description": "Path to a JSON overrides file.", + "env": "SHOPIFY_FLAG_OVERRIDES", + "name": "overrides", + "required": true, + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "preview-id": { + "description": "An existing preview identifier to update instead of creating a new preview.", + "env": "SHOPIFY_FLAG_PREVIEW_ID", + "name": "preview-id", "hasDynamicHelp": false, "multiple": false, - "name": "path", "type": "option" + }, + "open": { + "description": "Automatically launch the theme preview in your default web browser.", + "env": "SHOPIFY_FLAG_OPEN", + "name": "open", + "allowNo": false, + "type": "boolean" + }, + "json": { + "description": "Output the preview URL and identifier as JSON.", + "env": "SHOPIFY_FLAG_JSON", + "name": "json", + "allowNo": false, + "type": "boolean" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "hydrogen:setup:markets", + "hiddenAliases": [], + "id": "theme:preview", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", - "strict": true + "strict": true, + "summary": "Applies JSON overrides to a theme and returns a preview URL.", + "descriptionWithMarkdown": "Applies a JSON overrides file to a theme and creates or updates a preview. This lets you quickly preview changes.\n\n The command returns a preview URL and a preview identifier. You can reuse the preview identifier with `--preview-id` to update an existing preview instead of creating a new one.", + "multiEnvironmentsFlags": null, + "customPluginName": "@shopify/theme" }, - "hydrogen:setup:vite": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/cli-hydrogen", - "description": "EXPERIMENTAL: Upgrades the project to use Vite.", - "enableJsonFlag": false, + "theme:pull": { + "aliases": [], + "args": {}, + "description": "Retrieves theme files from Shopify.\n\nIf no theme is specified, then you're prompted to select the theme to pull from the list of the themes in your store.", "flags": { + "no-color": { + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", + "allowNo": false, + "type": "boolean" + }, + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", + "allowNo": false, + "type": "boolean" + }, "path": { - "description": "The path to the directory of the Hydrogen storefront. Defaults to the current directory where the command is run.", - "env": "SHOPIFY_HYDROGEN_FLAG_PATH", + "description": "The path where you want to run the command. Defaults to the current working directory.", + "env": "SHOPIFY_FLAG_PATH", + "name": "path", + "noCacheDefault": true, "hasDynamicHelp": false, "multiple": false, - "name": "path", "type": "option" - } - }, - "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "hydrogen:setup:vite", - "pluginAlias": "@shopify/cli", - "pluginName": "@shopify/cli", + }, + "password": { + "description": "Password generated from the Theme Access app or an Admin API token.", + "env": "SHOPIFY_CLI_THEME_TOKEN", + "name": "password", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "store": { + "char": "s", + "description": "Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com, https://example.myshopify.com).", + "env": "SHOPIFY_FLAG_STORE", + "name": "store", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "environment": { + "char": "e", + "description": "The environment to apply to the current command.", + "env": "SHOPIFY_FLAG_ENVIRONMENT", + "name": "environment", + "hasDynamicHelp": false, + "multiple": true, + "type": "option" + }, + "only": { + "char": "o", + "description": "Download only the specified files (Multiple flags allowed). Wrap the value in double quotes if you're using wildcards.", + "env": "SHOPIFY_FLAG_ONLY", + "name": "only", + "hasDynamicHelp": false, + "multiple": true, + "type": "option" + }, + "ignore": { + "char": "x", + "description": "Skip downloading the specified files (Multiple flags allowed). Wrap the value in double quotes if you're using wildcards.", + "env": "SHOPIFY_FLAG_IGNORE", + "name": "ignore", + "hasDynamicHelp": false, + "multiple": true, + "type": "option" + }, + "theme": { + "char": "t", + "description": "Theme ID or name of the remote theme.", + "env": "SHOPIFY_FLAG_THEME_ID", + "name": "theme", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "development": { + "char": "d", + "description": "Pull theme files from your remote development theme.", + "env": "SHOPIFY_FLAG_DEVELOPMENT", + "name": "development", + "allowNo": false, + "type": "boolean" + }, + "live": { + "char": "l", + "description": "Pull theme files from your remote live theme.", + "env": "SHOPIFY_FLAG_LIVE", + "name": "live", + "allowNo": false, + "type": "boolean" + }, + "nodelete": { + "char": "n", + "description": "Prevent deleting local files that don't exist remotely.", + "env": "SHOPIFY_FLAG_NODELETE", + "name": "nodelete", + "allowNo": false, + "type": "boolean" + }, + "force": { + "char": "f", + "description": "Proceed without confirmation, if current directory does not seem to be theme directory.", + "env": "SHOPIFY_FLAG_FORCE", + "hidden": true, + "name": "force", + "allowNo": false, + "type": "boolean" + } + }, + "hasDynamicHelp": false, + "hiddenAliases": [], + "id": "theme:pull", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true, + "summary": "Download your remote theme files locally.", + "descriptionWithMarkdown": "Retrieves theme files from Shopify.\n\nIf no theme is specified, then you're prompted to select the theme to pull from the list of the themes in your store.", + "multiEnvironmentsFlags": [ + "store", + "password", + "path", + [ + "live", + "development", + "theme" + ] + ], + "customPluginName": "@shopify/theme" + }, + "theme:push": { + "aliases": [], + "args": {}, + "description": "Uploads your local theme files to Shopify, overwriting the remote version if specified.\n\n If no theme is specified, then you're prompted to select the theme to overwrite from the list of the themes in your store.\n\n You can run this command only in a directory that matches the \"default Shopify theme folder structure\" (https://shopify.dev/docs/themes/tools/cli#directory-structure).\n\n This command returns the following information:\n\n - A link to the \"editor\" (https://shopify.dev/docs/themes/tools/online-editor) for the theme in the Shopify admin.\n - A \"preview link\" (https://help.shopify.com/manual/online-store/themes/adding-themes#share-a-theme-preview-with-others) that you can share with others.\n\n If you use the `--json` flag, then theme information is returned in JSON format, which can be used as a machine-readable input for scripts or continuous integration.\n\n Sample output:\n\n ```json\n {\n \"theme\": {\n \"id\": 108267175958,\n \"name\": \"MyTheme\",\n \"role\": \"unpublished\",\n \"shop\": \"mystore.myshopify.com\",\n \"editor_url\": \"https://mystore.myshopify.com/admin/themes/108267175958/editor\",\n \"preview_url\": \"https://mystore.myshopify.com/?preview_theme_id=108267175958\"\n }\n }\n ```\n ", + "flags": { + "no-color": { + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", + "allowNo": false, + "type": "boolean" + }, + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", + "allowNo": false, + "type": "boolean" + }, + "path": { + "description": "The path where you want to run the command. Defaults to the current working directory.", + "env": "SHOPIFY_FLAG_PATH", + "name": "path", + "noCacheDefault": true, + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "password": { + "description": "Password generated from the Theme Access app or an Admin API token.", + "env": "SHOPIFY_CLI_THEME_TOKEN", + "name": "password", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "store": { + "char": "s", + "description": "Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com, https://example.myshopify.com).", + "env": "SHOPIFY_FLAG_STORE", + "name": "store", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "environment": { + "char": "e", + "description": "The environment to apply to the current command.", + "env": "SHOPIFY_FLAG_ENVIRONMENT", + "name": "environment", + "hasDynamicHelp": false, + "multiple": true, + "type": "option" + }, + "only": { + "char": "o", + "description": "Upload only the specified files (Multiple flags allowed). Wrap the value in double quotes if you're using wildcards.", + "env": "SHOPIFY_FLAG_ONLY", + "name": "only", + "hasDynamicHelp": false, + "multiple": true, + "type": "option" + }, + "ignore": { + "char": "x", + "description": "Skip uploading the specified files (Multiple flags allowed). Wrap the value in double quotes if you're using wildcards.", + "env": "SHOPIFY_FLAG_IGNORE", + "name": "ignore", + "hasDynamicHelp": false, + "multiple": true, + "type": "option" + }, + "json": { + "char": "j", + "description": "Output the result as JSON. Automatically disables color output.", + "env": "SHOPIFY_FLAG_JSON", + "hidden": false, + "name": "json", + "allowNo": false, + "type": "boolean" + }, + "theme": { + "char": "t", + "description": "Theme ID or name of the remote theme.", + "env": "SHOPIFY_FLAG_THEME_ID", + "name": "theme", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "development": { + "char": "d", + "description": "Push theme files from your remote development theme.", + "env": "SHOPIFY_FLAG_DEVELOPMENT", + "name": "development", + "allowNo": false, + "type": "boolean" + }, + "development-context": { + "char": "c", + "dependsOn": [ + "development" + ], + "description": "Unique identifier for a development theme context (e.g., PR number, branch name). Reuses an existing development theme with this context name, or creates one if none exists.", + "env": "SHOPIFY_FLAG_DEVELOPMENT_CONTEXT", + "exclusive": [ + "theme" + ], + "name": "development-context", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "live": { + "char": "l", + "description": "Push theme files from your remote live theme.", + "env": "SHOPIFY_FLAG_LIVE", + "name": "live", + "allowNo": false, + "type": "boolean" + }, + "unpublished": { + "char": "u", + "description": "Create a new unpublished theme and push to it.", + "env": "SHOPIFY_FLAG_UNPUBLISHED", + "name": "unpublished", + "allowNo": false, + "type": "boolean" + }, + "nodelete": { + "char": "n", + "description": "Prevent deleting remote files that don't exist locally.", + "env": "SHOPIFY_FLAG_NODELETE", + "name": "nodelete", + "allowNo": false, + "type": "boolean" + }, + "allow-live": { + "char": "a", + "description": "Allow push to a live theme.", + "env": "SHOPIFY_FLAG_ALLOW_LIVE", + "name": "allow-live", + "allowNo": false, + "type": "boolean" + }, + "publish": { + "char": "p", + "description": "Publish as the live theme after uploading.", + "env": "SHOPIFY_FLAG_PUBLISH", + "name": "publish", + "allowNo": false, + "type": "boolean" + }, + "force": { + "char": "f", + "description": "Proceed without confirmation, if current directory does not seem to be theme directory.", + "env": "SHOPIFY_FLAG_FORCE", + "hidden": true, + "name": "force", + "allowNo": false, + "type": "boolean" + }, + "strict": { + "description": "Require theme check to pass without errors before pushing. Warnings are allowed.", + "env": "SHOPIFY_FLAG_STRICT_PUSH", + "name": "strict", + "allowNo": false, + "type": "boolean" + }, + "listing": { + "description": "The listing preset to use for multi-preset themes. Applies preset files from listings/[preset-name] directory.", + "env": "SHOPIFY_FLAG_LISTING", + "name": "listing", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + } + }, + "hasDynamicHelp": false, + "hiddenAliases": [], + "id": "theme:push", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true, + "summary": "Uploads your local theme files to the connected store, overwriting the remote version if specified.", + "usage": [ + "theme push", + "theme push --unpublished --json" + ], + "descriptionWithMarkdown": "Uploads your local theme files to Shopify, overwriting the remote version if specified.\n\n If no theme is specified, then you're prompted to select the theme to overwrite from the list of the themes in your store.\n\n You can run this command only in a directory that matches the [default Shopify theme folder structure](https://shopify.dev/docs/themes/tools/cli#directory-structure).\n\n This command returns the following information:\n\n - A link to the [editor](https://shopify.dev/docs/themes/tools/online-editor) for the theme in the Shopify admin.\n - A [preview link](https://help.shopify.com/manual/online-store/themes/adding-themes#share-a-theme-preview-with-others) that you can share with others.\n\n If you use the `--json` flag, then theme information is returned in JSON format, which can be used as a machine-readable input for scripts or continuous integration.\n\n Sample output:\n\n ```json\n {\n \"theme\": {\n \"id\": 108267175958,\n \"name\": \"MyTheme\",\n \"role\": \"unpublished\",\n \"shop\": \"mystore.myshopify.com\",\n \"editor_url\": \"https://mystore.myshopify.com/admin/themes/108267175958/editor\",\n \"preview_url\": \"https://mystore.myshopify.com/?preview_theme_id=108267175958\"\n }\n }\n ```\n ", + "multiEnvironmentsFlags": [ + "store", + "password", + "path", + [ + "live", + "development", + "theme" + ] + ], + "customPluginName": "@shopify/theme" + }, + "theme:rename": { + "aliases": [], + "args": {}, + "description": "Renames a theme in your store.\n\n If no theme is specified, then you're prompted to select the theme that you want to rename from the list of themes in your store.\n ", + "flags": { + "no-color": { + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", + "allowNo": false, + "type": "boolean" + }, + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", + "allowNo": false, + "type": "boolean" + }, + "path": { + "description": "The path where you want to run the command. Defaults to the current working directory.", + "env": "SHOPIFY_FLAG_PATH", + "name": "path", + "noCacheDefault": true, + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "password": { + "description": "Password generated from the Theme Access app or an Admin API token.", + "env": "SHOPIFY_CLI_THEME_TOKEN", + "name": "password", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "store": { + "char": "s", + "description": "Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com, https://example.myshopify.com).", + "env": "SHOPIFY_FLAG_STORE", + "name": "store", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "environment": { + "char": "e", + "description": "The environment to apply to the current command.", + "env": "SHOPIFY_FLAG_ENVIRONMENT", + "name": "environment", + "hasDynamicHelp": false, + "multiple": true, + "type": "option" + }, + "name": { + "char": "n", + "description": "The new name for the theme.", + "env": "SHOPIFY_FLAG_NEW_NAME", + "name": "name", + "required": false, + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "development": { + "char": "d", + "description": "Rename your development theme.", + "env": "SHOPIFY_FLAG_DEVELOPMENT", + "name": "development", + "allowNo": false, + "type": "boolean" + }, + "theme": { + "char": "t", + "description": "Theme ID or name of the remote theme.", + "env": "SHOPIFY_FLAG_THEME_ID", + "name": "theme", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "live": { + "char": "l", + "description": "Rename your remote live theme.", + "env": "SHOPIFY_FLAG_LIVE", + "name": "live", + "allowNo": false, + "type": "boolean" + } + }, + "hasDynamicHelp": false, + "hiddenAliases": [], + "id": "theme:rename", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true, + "summary": "Renames an existing theme.", + "descriptionWithMarkdown": "Renames a theme in your store.\n\n If no theme is specified, then you're prompted to select the theme that you want to rename from the list of themes in your store.\n ", + "multiEnvironmentsFlags": [ + "store", + "password", + "name", + [ + "live", + "development", + "theme" + ] + ], + "customPluginName": "@shopify/theme" + }, + "theme:serve": { + "aliases": [], + "args": {}, + "description": "\n Uploads the current theme as the specified theme, or a \"development theme\" (https://shopify.dev/docs/themes/tools/cli#development-themes), to a store so you can preview it.\n\nThis command returns the following information:\n\n- A link to your development theme at http://127.0.0.1:9292. This URL can hot reload local changes to CSS and sections, or refresh the entire page when a file changes, enabling you to preview changes in real time using the store's data.\n\n You can specify a different network interface and port using `--host` and `--port`.\n\n- A link to the \"editor\" (https://shopify.dev/docs/themes/tools/online-editor) for the theme in the Shopify admin.\n\n- A \"preview link\" (https://help.shopify.com/manual/online-store/themes/adding-themes#share-a-theme-preview-with-others) that you can share with other developers.\n\nIf you already have a development theme for your current environment, then this command replaces the development theme with your local theme. You can override this using the `--theme-editor-sync` flag.\n\n> Note: You can't preview checkout customizations using http://127.0.0.1:9292.\n\nDevelopment themes are deleted when you run `shopify auth logout`. If you need a preview link that can be used after you log out, then you should \"share\" (https://shopify.dev/docs/api/shopify-cli/theme/theme-share) your theme or \"push\" (https://shopify.dev/docs/api/shopify-cli/theme/theme-push) to an unpublished theme on your store.\n\nYou can run this command only in a directory that matches the \"default Shopify theme folder structure\" (https://shopify.dev/docs/themes/tools/cli#directory-structure).", + "flags": { + "no-color": { + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", + "allowNo": false, + "type": "boolean" + }, + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", + "allowNo": false, + "type": "boolean" + }, + "path": { + "description": "The path where you want to run the command. Defaults to the current working directory.", + "env": "SHOPIFY_FLAG_PATH", + "name": "path", + "noCacheDefault": true, + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "password": { + "description": "Password generated from the Theme Access app or an Admin API token.", + "env": "SHOPIFY_CLI_THEME_TOKEN", + "name": "password", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "store": { + "char": "s", + "description": "Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com, https://example.myshopify.com).", + "env": "SHOPIFY_FLAG_STORE", + "name": "store", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "environment": { + "char": "e", + "description": "The environment to apply to the current command.", + "env": "SHOPIFY_FLAG_ENVIRONMENT", + "name": "environment", + "hasDynamicHelp": false, + "multiple": true, + "type": "option" + }, + "host": { + "description": "Set which network interface the web server listens on. The default value is 127.0.0.1.", + "env": "SHOPIFY_FLAG_HOST", + "name": "host", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "live-reload": { + "description": "The live reload mode switches the server behavior when a file is modified:\n- hot-reload Hot reloads local changes to CSS and sections (default)\n- full-page Always refreshes the entire page\n- off Deactivate live reload", + "env": "SHOPIFY_FLAG_LIVE_RELOAD", + "name": "live-reload", + "default": "hot-reload", + "hasDynamicHelp": false, + "multiple": false, + "options": [ + "hot-reload", + "full-page", + "off" + ], + "type": "option" + }, + "error-overlay": { + "description": "Controls the visibility of the error overlay when an theme asset upload fails:\n- silent Prevents the error overlay from appearing.\n- default Displays the error overlay.\n ", + "env": "SHOPIFY_FLAG_ERROR_OVERLAY", + "name": "error-overlay", + "default": "default", + "hasDynamicHelp": false, + "multiple": false, + "options": [ + "silent", + "default" + ], + "type": "option" + }, + "poll": { + "description": "Force polling to detect file changes.", + "env": "SHOPIFY_FLAG_POLL", + "hidden": true, + "name": "poll", + "allowNo": false, + "type": "boolean" + }, + "theme-editor-sync": { + "description": "Synchronize Theme Editor updates in the local theme files.", + "env": "SHOPIFY_FLAG_THEME_EDITOR_SYNC", + "name": "theme-editor-sync", + "allowNo": false, + "type": "boolean" + }, + "port": { + "description": "Local port to serve theme preview from.", + "env": "SHOPIFY_FLAG_PORT", + "name": "port", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "theme": { + "char": "t", + "description": "Theme ID or name of the remote theme.", + "env": "SHOPIFY_FLAG_THEME_ID", + "name": "theme", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "listing": { + "description": "The listing preset to use for multi-preset themes. Applies preset files from listings/[preset-name] directory.", + "env": "SHOPIFY_FLAG_LISTING", + "name": "listing", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "nodelete": { + "char": "n", + "description": "Prevents files from being deleted in the remote theme when a file has been deleted locally. This applies to files that are deleted while the command is running, and files that have been deleted locally before the command is run.", + "env": "SHOPIFY_FLAG_NODELETE", + "name": "nodelete", + "allowNo": false, + "type": "boolean" + }, + "only": { + "char": "o", + "description": "Hot reload only files that match the specified pattern.", + "env": "SHOPIFY_FLAG_ONLY", + "name": "only", + "hasDynamicHelp": false, + "multiple": true, + "type": "option" + }, + "ignore": { + "char": "x", + "description": "Skip hot reloading any files that match the specified pattern.", + "env": "SHOPIFY_FLAG_IGNORE", + "name": "ignore", + "hasDynamicHelp": false, + "multiple": true, + "type": "option" + }, + "force": { + "char": "f", + "description": "Proceed without confirmation, if current directory does not seem to be theme directory.", + "env": "SHOPIFY_FLAG_FORCE", + "hidden": true, + "name": "force", + "allowNo": false, + "type": "boolean" + }, + "notify": { + "description": "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", + "name": "notify", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "open": { + "description": "Automatically launch the theme preview in your default web browser.", + "env": "SHOPIFY_FLAG_OPEN", + "name": "open", + "allowNo": false, + "type": "boolean" + }, + "store-password": { + "description": "The password for storefronts with password protection.", + "env": "SHOPIFY_FLAG_STORE_PASSWORD", + "name": "store-password", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "allow-live": { + "char": "a", + "description": "Allow development on a live theme.", + "env": "SHOPIFY_FLAG_ALLOW_LIVE", + "name": "allow-live", + "allowNo": false, + "type": "boolean" + } + }, + "hasDynamicHelp": false, + "hidden": true, + "hiddenAliases": [], + "id": "theme:serve", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "summary": "Uploads the current theme as a development theme to the connected store, then prints theme editor and preview URLs to your terminal. While running, changes will push to the store in real time.", + "descriptionWithMarkdown": "\n Uploads the current theme as the specified theme, or a [development theme](https://shopify.dev/docs/themes/tools/cli#development-themes), to a store so you can preview it.\n\nThis command returns the following information:\n\n- A link to your development theme at http://127.0.0.1:9292. This URL can hot reload local changes to CSS and sections, or refresh the entire page when a file changes, enabling you to preview changes in real time using the store's data.\n\n You can specify a different network interface and port using `--host` and `--port`.\n\n- A link to the [editor](https://shopify.dev/docs/themes/tools/online-editor) for the theme in the Shopify admin.\n\n- A [preview link](https://help.shopify.com/manual/online-store/themes/adding-themes#share-a-theme-preview-with-others) that you can share with other developers.\n\nIf you already have a development theme for your current environment, then this command replaces the development theme with your local theme. You can override this using the `--theme-editor-sync` flag.\n\n> Note: You can't preview checkout customizations using http://127.0.0.1:9292.\n\nDevelopment themes are deleted when you run `shopify auth logout`. If you need a preview link that can be used after you log out, then you should [share](https://shopify.dev/docs/api/shopify-cli/theme/theme-share) your theme or [push](https://shopify.dev/docs/api/shopify-cli/theme/theme-push) to an unpublished theme on your store.\n\nYou can run this command only in a directory that matches the [default Shopify theme folder structure](https://shopify.dev/docs/themes/tools/cli#directory-structure).", + "multiEnvironmentsFlags": null, + "customPluginName": "@shopify/theme" + }, + "theme:share": { + "aliases": [], + "args": {}, + "description": "Uploads your theme as a new, unpublished theme in your theme library. The theme is given a randomized name.\n\n This command returns a \"preview link\" (https://help.shopify.com/manual/online-store/themes/adding-themes#share-a-theme-preview-with-others) that you can share with others.", + "flags": { + "no-color": { + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", + "allowNo": false, + "type": "boolean" + }, + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", + "allowNo": false, + "type": "boolean" + }, + "path": { + "description": "The path where you want to run the command. Defaults to the current working directory.", + "env": "SHOPIFY_FLAG_PATH", + "name": "path", + "noCacheDefault": true, + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "password": { + "description": "Password generated from the Theme Access app or an Admin API token.", + "env": "SHOPIFY_CLI_THEME_TOKEN", + "name": "password", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "store": { + "char": "s", + "description": "Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com, https://example.myshopify.com).", + "env": "SHOPIFY_FLAG_STORE", + "name": "store", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "environment": { + "char": "e", + "description": "The environment to apply to the current command.", + "env": "SHOPIFY_FLAG_ENVIRONMENT", + "name": "environment", + "hasDynamicHelp": false, + "multiple": true, + "type": "option" + }, + "force": { + "char": "f", + "description": "Proceed without confirmation, if current directory does not seem to be theme directory.", + "env": "SHOPIFY_FLAG_FORCE", + "hidden": true, + "name": "force", + "allowNo": false, + "type": "boolean" + }, + "listing": { + "description": "The listing preset to use for multi-preset themes. Applies preset files from listings/[preset-name] directory.", + "env": "SHOPIFY_FLAG_LISTING", + "name": "listing", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + } + }, + "hasDynamicHelp": false, + "hiddenAliases": [], + "id": "theme:share", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true, + "summary": "Creates a shareable, unpublished, and new theme on your theme library with a randomized name.", + "descriptionWithMarkdown": "Uploads your theme as a new, unpublished theme in your theme library. The theme is given a randomized name.\n\n This command returns a [preview link](https://help.shopify.com/manual/online-store/themes/adding-themes#share-a-theme-preview-with-others) that you can share with others.", + "multiEnvironmentsFlags": [ + "store", + "password", + "path" + ], + "customPluginName": "@shopify/theme" + }, + "plugins": { + "aliases": [], + "args": {}, + "description": "List installed plugins.", + "examples": [ + "<%= config.bin %> <%= command.id %>" + ], + "flags": { + "json": { + "description": "Format output as json.", + "helpGroup": "GLOBAL", + "name": "json", + "allowNo": false, + "type": "boolean" + }, + "core": { + "description": "Show core plugins.", + "name": "core", + "allowNo": false, + "type": "boolean" + } + }, + "hasDynamicHelp": false, + "hidden": true, + "hiddenAliases": [], + "id": "plugins", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true, + "enableJsonFlag": true, + "customPluginName": "@oclif/plugin-plugins" + }, + "plugins:inspect": { + "aliases": [], + "args": { + "plugin": { + "default": ".", + "description": "Plugin to inspect.", + "name": "plugin", + "required": true + } + }, + "description": "Displays installation properties of a plugin.", + "examples": [ + "<%= config.bin %> <%= command.id %> <%- config.pjson.oclif.examplePlugin || \"myplugin\" %> " + ], + "flags": { + "json": { + "description": "Format output as json.", + "helpGroup": "GLOBAL", + "name": "json", + "allowNo": false, + "type": "boolean" + }, + "help": { + "char": "h", + "description": "Show CLI help.", + "name": "help", + "allowNo": false, + "type": "boolean" + }, + "verbose": { + "char": "v", + "name": "verbose", + "allowNo": false, + "type": "boolean" + } + }, + "hasDynamicHelp": false, + "hiddenAliases": [], + "id": "plugins:inspect", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": false, + "usage": "plugins:inspect PLUGIN...", + "enableJsonFlag": true, + "customPluginName": "@oclif/plugin-plugins" + }, + "plugins:install": { + "aliases": [ + "plugins:add" + ], + "args": { + "plugin": { + "description": "Plugin to install.", + "name": "plugin", + "required": true + } + }, + "description": "", + "examples": [ + { + "command": "<%= config.bin %> <%= command.id %> <%- config.pjson.oclif.examplePlugin || \"myplugin\" %> ", + "description": "Install a plugin from npm registry." + }, + { + "command": "<%= config.bin %> <%= command.id %> https://github.com/someuser/someplugin", + "description": "Install a plugin from a github url." + }, + { + "command": "<%= config.bin %> <%= command.id %> someuser/someplugin", + "description": "Install a plugin from a github slug." + } + ], + "flags": { + "json": { + "description": "Format output as json.", + "helpGroup": "GLOBAL", + "name": "json", + "allowNo": false, + "type": "boolean" + }, + "force": { + "char": "f", + "description": "Force npm to fetch remote resources even if a local copy exists on disk.", + "name": "force", + "allowNo": false, + "type": "boolean" + }, + "help": { + "char": "h", + "description": "Show CLI help.", + "name": "help", + "allowNo": false, + "type": "boolean" + }, + "jit": { + "hidden": true, + "name": "jit", + "allowNo": false, + "type": "boolean" + }, + "silent": { + "char": "s", + "description": "Silences npm output.", + "exclusive": [ + "verbose" + ], + "name": "silent", + "allowNo": false, + "type": "boolean" + }, + "verbose": { + "char": "v", + "description": "Show verbose npm output.", + "exclusive": [ + "silent" + ], + "name": "verbose", + "allowNo": false, + "type": "boolean" + } + }, + "hasDynamicHelp": false, + "hiddenAliases": [], + "id": "plugins:install", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", "pluginType": "core", - "strict": true + "strict": false, + "summary": "Installs a plugin into <%= config.bin %>.", + "enableJsonFlag": true, + "customPluginName": "@oclif/plugin-plugins" }, - "hydrogen:shortcut": { + "plugins:link": { + "aliases": [], + "args": { + "path": { + "default": ".", + "description": "path to plugin", + "name": "path", + "required": true + } + }, + "description": "Installation of a linked plugin will override a user-installed or core plugin.\n\ne.g. If you have a user-installed or core plugin that has a 'hello' command, installing a linked plugin with a 'hello' command will override the user-installed or core plugin implementation. This is useful for development work.\n", + "examples": [ + "<%= config.bin %> <%= command.id %> <%- config.pjson.oclif.examplePlugin || \"myplugin\" %> " + ], + "flags": { + "help": { + "char": "h", + "description": "Show CLI help.", + "name": "help", + "allowNo": false, + "type": "boolean" + }, + "install": { + "description": "Install dependencies after linking the plugin.", + "name": "install", + "allowNo": true, + "type": "boolean" + }, + "verbose": { + "char": "v", + "name": "verbose", + "allowNo": false, + "type": "boolean" + } + }, + "hasDynamicHelp": false, + "hiddenAliases": [], + "id": "plugins:link", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true, + "summary": "Links a plugin into the CLI for development.", + "enableJsonFlag": false, + "customPluginName": "@oclif/plugin-plugins" + }, + "plugins:reset": { + "aliases": [], + "args": {}, + "flags": { + "hard": { + "name": "hard", + "summary": "Delete node_modules and package manager related files in addition to uninstalling plugins.", + "allowNo": false, + "type": "boolean" + }, + "reinstall": { + "name": "reinstall", + "summary": "Reinstall all plugins after uninstalling.", + "allowNo": false, + "type": "boolean" + } + }, + "hasDynamicHelp": false, + "hiddenAliases": [], + "id": "plugins:reset", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true, + "summary": "Remove all user-installed and linked plugins.", + "enableJsonFlag": false, + "customPluginName": "@oclif/plugin-plugins" + }, + "plugins:uninstall": { "aliases": [ + "plugins:unlink", + "plugins:remove" ], "args": { + "plugin": { + "description": "plugin to uninstall", + "name": "plugin" + } }, - "customPluginName": "@shopify/cli-hydrogen", - "description": "Creates a global `h2` shortcut for the Hydrogen CLI", - "descriptionWithMarkdown": "Creates a global h2 shortcut for Shopify CLI using shell aliases.\n\n The following shells are supported:\n\n - Bash (using `~/.bashrc`)\n - ZSH (using `~/.zshrc`)\n - Fish (using `~/.config/fish/functions`)\n - PowerShell (added to `$PROFILE`)\n\n After the alias is created, you can call Shopify CLI from anywhere in your project using `h2 `.", + "description": "Removes a plugin from the CLI.", + "examples": [ + "<%= config.bin %> <%= command.id %> <%- config.pjson.oclif.examplePlugin || \"myplugin\" %>" + ], + "flags": { + "help": { + "char": "h", + "description": "Show CLI help.", + "name": "help", + "allowNo": false, + "type": "boolean" + }, + "verbose": { + "char": "v", + "name": "verbose", + "allowNo": false, + "type": "boolean" + } + }, + "hasDynamicHelp": false, + "hiddenAliases": [], + "id": "plugins:uninstall", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": false, + "enableJsonFlag": false, + "customPluginName": "@oclif/plugin-plugins" + }, + "plugins:update": { + "aliases": [], + "args": {}, + "description": "Update installed plugins.", + "flags": { + "help": { + "char": "h", + "description": "Show CLI help.", + "name": "help", + "allowNo": false, + "type": "boolean" + }, + "verbose": { + "char": "v", + "name": "verbose", + "allowNo": false, + "type": "boolean" + } + }, + "hasDynamicHelp": false, + "hiddenAliases": [], + "id": "plugins:update", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true, + "enableJsonFlag": false, + "customPluginName": "@oclif/plugin-plugins" + }, + "config:autocorrect:off": { + "aliases": [], + "args": {}, + "description": "Disable autocorrect. Off by default.\n\n When autocorrection is enabled, Shopify CLI automatically runs a corrected version of your command if a correction is available.\n\n When autocorrection is disabled, you need to confirm that you want to run corrections for mistyped commands.\n", + "flags": {}, + "hasDynamicHelp": false, + "hiddenAliases": [], + "id": "config:autocorrect:off", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true, + "summary": "Disable autocorrect. Off by default.", + "enableJsonFlag": false, + "descriptionWithMarkdown": "Disable autocorrect. Off by default.\n\n When autocorrection is enabled, Shopify CLI automatically runs a corrected version of your command if a correction is available.\n\n When autocorrection is disabled, you need to confirm that you want to run corrections for mistyped commands.\n", + "customPluginName": "@shopify/plugin-did-you-mean" + }, + "config:autocorrect:status": { + "aliases": [], + "args": {}, + "description": "Check whether autocorrect is enabled or disabled. On by default.\n\n When autocorrection is enabled, Shopify CLI automatically runs a corrected version of your command if a correction is available.\n\n When autocorrection is disabled, you need to confirm that you want to run corrections for mistyped commands.\n", + "flags": {}, + "hasDynamicHelp": false, + "hiddenAliases": [], + "id": "config:autocorrect:status", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true, + "summary": "Check whether autocorrect is enabled or disabled. On by default.", + "enableJsonFlag": false, + "descriptionWithMarkdown": "Check whether autocorrect is enabled or disabled. On by default.\n\n When autocorrection is enabled, Shopify CLI automatically runs a corrected version of your command if a correction is available.\n\n When autocorrection is disabled, you need to confirm that you want to run corrections for mistyped commands.\n", + "customPluginName": "@shopify/plugin-did-you-mean" + }, + "config:autocorrect:on": { + "aliases": [], + "args": {}, + "description": "Enable autocorrect. Off by default.\n\n When autocorrection is enabled, Shopify CLI automatically runs a corrected version of your command if a correction is available.\n\n When autocorrection is disabled, you need to confirm that you want to run corrections for mistyped commands.\n", + "flags": {}, + "hasDynamicHelp": false, + "hiddenAliases": [], + "id": "config:autocorrect:on", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true, + "summary": "Enable autocorrect. Off by default.", "enableJsonFlag": false, + "descriptionWithMarkdown": "Enable autocorrect. Off by default.\n\n When autocorrection is enabled, Shopify CLI automatically runs a corrected version of your command if a correction is available.\n\n When autocorrection is disabled, you need to confirm that you want to run corrections for mistyped commands.\n", + "customPluginName": "@shopify/plugin-did-you-mean" + }, + "commands": { + "aliases": [], + "args": {}, + "description": "List all <%= config.bin %> commands.", "flags": { + "json": { + "description": "Format output as json.", + "helpGroup": "GLOBAL", + "name": "json", + "allowNo": false, + "type": "boolean" + }, + "columns": { + "char": "c", + "description": "Only show provided columns (comma-separated).", + "exclusive": [ + "tree" + ], + "name": "columns", + "delimiter": ",", + "hasDynamicHelp": false, + "multiple": true, + "options": [ + "id", + "plugin", + "summary", + "type" + ], + "type": "option" + }, + "deprecated": { + "description": "Show deprecated commands.", + "name": "deprecated", + "allowNo": false, + "type": "boolean" + }, + "extended": { + "char": "x", + "description": "Show extra columns.", + "exclusive": [ + "tree" + ], + "name": "extended", + "allowNo": false, + "type": "boolean" + }, + "hidden": { + "description": "Show hidden commands.", + "name": "hidden", + "allowNo": false, + "type": "boolean" + }, + "no-truncate": { + "description": "Do not truncate output.", + "exclusive": [ + "tree" + ], + "name": "no-truncate", + "allowNo": false, + "type": "boolean" + }, + "sort": { + "description": "Property to sort by.", + "exclusive": [ + "tree" + ], + "name": "sort", + "default": "id", + "hasDynamicHelp": false, + "multiple": false, + "options": [ + "id", + "plugin", + "summary", + "type" + ], + "type": "option" + }, + "tree": { + "description": "Show tree of commands.", + "name": "tree", + "allowNo": false, + "type": "boolean" + } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "hydrogen:shortcut", + "hiddenAliases": [], + "id": "commands", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", - "strict": true + "strict": true, + "enableJsonFlag": true, + "customPluginName": "@oclif/plugin-commands" }, - "hydrogen:unlink": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/cli-hydrogen", - "description": "Unlink a local project from a Hydrogen storefront.", - "descriptionWithMarkdown": "Unlinks your local development environment from a remote Hydrogen storefront.", - "enableJsonFlag": false, + "hydrogen:dev": { + "aliases": [], + "args": {}, + "description": "Runs Hydrogen storefront in an Oxygen worker for development.", "flags": { "path": { "description": "The path to the directory of the Hydrogen storefront. Defaults to the current directory where the command is run.", "env": "SHOPIFY_HYDROGEN_FLAG_PATH", + "name": "path", "hasDynamicHelp": false, "multiple": false, - "name": "path", "type": "option" - } - }, - "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "hydrogen:unlink", - "pluginAlias": "@shopify/cli", - "pluginName": "@shopify/cli", - "pluginType": "core", - "strict": true - }, - "hydrogen:upgrade": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/cli-hydrogen", - "description": "Upgrade Remix and Hydrogen npm dependencies.", - "descriptionWithMarkdown": "Upgrade Hydrogen project dependencies, preview features, fixes and breaking changes. The command also generates an instruction file for each upgrade.", - "enableJsonFlag": false, - "flags": { - "force": { + }, + "entry": { + "description": "Entry file for the worker. Defaults to `./server`.", + "env": "SHOPIFY_HYDROGEN_FLAG_ENTRY", + "name": "entry", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "port": { + "description": "The port to run the server on. Defaults to 3000.", + "env": "SHOPIFY_HYDROGEN_FLAG_PORT", + "name": "port", + "required": false, + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "codegen": { + "description": "Automatically generates GraphQL types for your project’s Storefront API queries.", + "name": "codegen", + "required": false, "allowNo": false, - "char": "f", - "description": "Ignore warnings and force the upgrade to the target version", - "env": "SHOPIFY_HYDROGEN_FLAG_FORCE", - "name": "force", "type": "boolean" }, - "path": { - "description": "The path to the directory of the Hydrogen storefront. Defaults to the current directory where the command is run.", - "env": "SHOPIFY_HYDROGEN_FLAG_PATH", + "codegen-config-path": { + "dependsOn": [ + "codegen" + ], + "description": "Specifies a path to a codegen configuration file. Defaults to `/codegen.ts` if this file exists.", + "name": "codegen-config-path", + "required": false, "hasDynamicHelp": false, "multiple": false, - "name": "path", "type": "option" }, - "version": { - "char": "v", - "description": "A target hydrogen version to update to", + "disable-virtual-routes": { + "description": "Disable rendering fallback routes when a route file doesn't exist.", + "env": "SHOPIFY_HYDROGEN_FLAG_DISABLE_VIRTUAL_ROUTES", + "name": "disable-virtual-routes", + "allowNo": false, + "type": "boolean" + }, + "debug": { + "description": "Enables inspector connections to the server with a debugger such as Visual Studio Code or Chrome DevTools.", + "env": "SHOPIFY_HYDROGEN_FLAG_DEBUG", + "name": "debug", + "allowNo": false, + "type": "boolean" + }, + "inspector-port": { + "description": "The port where the inspector is available. Defaults to 9229.", + "env": "SHOPIFY_HYDROGEN_FLAG_INSPECTOR_PORT", + "name": "inspector-port", "hasDynamicHelp": false, "multiple": false, - "name": "version", + "type": "option" + }, + "env": { + "description": "Specifies the environment to perform the operation using its handle. Fetch the handle using the `env list` command.", + "exclusive": [ + "env-branch" + ], + "name": "env", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "env-branch": { + "deprecated": { + "to": "env", + "message": "--env-branch is deprecated. Use --env instead." + }, + "description": "Specifies the environment to perform the operation using its Git branch name.", + "env": "SHOPIFY_HYDROGEN_ENVIRONMENT_BRANCH", + "name": "env-branch", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "env-file": { + "description": "Path to an environment file to override existing environment variables. Defaults to the '.env' located in your project path `--path`.", + "name": "env-file", "required": false, + "default": ".env", + "hasDynamicHelp": false, + "multiple": false, "type": "option" + }, + "disable-version-check": { + "description": "Skip the version check when running `hydrogen dev`", + "name": "disable-version-check", + "required": false, + "allowNo": false, + "type": "boolean" + }, + "customer-account-push": { + "description": "Use tunneling for local development and push the tunneling domain to admin. Required to use Customer Account API's OAuth flow", + "env": "SHOPIFY_HYDROGEN_FLAG_CUSTOMER_ACCOUNT_PUSH", + "name": "customer-account-push", + "required": false, + "allowNo": false, + "type": "boolean" + }, + "verbose": { + "description": "Outputs more information about the command's execution.", + "env": "SHOPIFY_HYDROGEN_FLAG_VERBOSE", + "name": "verbose", + "required": false, + "allowNo": false, + "type": "boolean" + }, + "host": { + "description": "Expose the server to the local network", + "name": "host", + "required": false, + "allowNo": false, + "type": "boolean" + }, + "disable-deps-optimizer": { + "description": "Disable adding dependencies to Vite's `ssr.optimizeDeps.include` automatically", + "env": "SHOPIFY_HYDROGEN_FLAG_DISABLE_DEPS_OPTIMIZER", + "name": "disable-deps-optimizer", + "allowNo": false, + "type": "boolean" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "hydrogen:upgrade", + "hiddenAliases": [], + "id": "hydrogen:dev", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", - "strict": true - }, - "kitchen-sink": { - "aliases": [ - ], - "args": { - }, - "description": "View all the available UI kit components", + "strict": true, "enableJsonFlag": false, - "flags": { - }, - "hasDynamicHelp": false, - "hidden": true, - "hiddenAliases": [ - "kitchen-sink all" - ], - "id": "kitchen-sink", - "pluginAlias": "@shopify/cli", - "pluginName": "@shopify/cli", - "pluginType": "core", - "strict": true + "descriptionWithMarkdown": "Runs a Hydrogen storefront in a local runtime that emulates an Oxygen worker for development.\n\n If your project is [linked](https://shopify.dev/docs/api/shopify-cli/hydrogen/hydrogen-link) to a Hydrogen storefront, then its environment variables will be loaded with the runtime.", + "customPluginName": "@shopify/cli-hydrogen" }, - "kitchen-sink:async": { - "aliases": [ - ], - "args": { - }, - "description": "View the UI kit components that process async tasks", - "enableJsonFlag": false, + "hydrogen:build": { + "aliases": [], + "args": {}, + "description": "Builds a Hydrogen storefront for production.", "flags": { + "path": { + "description": "The path to the directory of the Hydrogen storefront. Defaults to the current directory where the command is run.", + "env": "SHOPIFY_HYDROGEN_FLAG_PATH", + "name": "path", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "entry": { + "description": "Entry file for the worker. Defaults to `./server`.", + "env": "SHOPIFY_HYDROGEN_FLAG_ENTRY", + "name": "entry", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "sourcemap": { + "description": "Controls whether server sourcemaps are generated. Default to `true`. Deactivate `--no-sourcemaps`.", + "env": "SHOPIFY_HYDROGEN_FLAG_SOURCEMAP", + "name": "sourcemap", + "allowNo": true, + "type": "boolean" + }, + "lockfile-check": { + "description": "Checks that there is exactly one valid lockfile in the project. Defaults to `true`. Deactivate with `--no-lockfile-check`.", + "env": "SHOPIFY_HYDROGEN_FLAG_LOCKFILE_CHECK", + "name": "lockfile-check", + "allowNo": true, + "type": "boolean" + }, + "disable-route-warning": { + "description": "Disables any warnings about missing standard routes.", + "env": "SHOPIFY_HYDROGEN_FLAG_DISABLE_ROUTE_WARNING", + "name": "disable-route-warning", + "allowNo": false, + "type": "boolean" + }, + "codegen": { + "description": "Automatically generates GraphQL types for your project’s Storefront API queries.", + "name": "codegen", + "required": false, + "allowNo": false, + "type": "boolean" + }, + "codegen-config-path": { + "dependsOn": [ + "codegen" + ], + "description": "Specifies a path to a codegen configuration file. Defaults to `/codegen.ts` if this file exists.", + "name": "codegen-config-path", + "required": false, + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "watch": { + "description": "Watches for changes and rebuilds the project writing output to disk.", + "env": "SHOPIFY_HYDROGEN_FLAG_WATCH", + "name": "watch", + "allowNo": false, + "type": "boolean" + }, + "bundle-stats": { + "description": "Show a bundle size summary after building. Defaults to true, use `--no-bundle-stats` to disable.", + "name": "bundle-stats", + "allowNo": true, + "type": "boolean" + }, + "force-client-sourcemap": { + "description": "Client sourcemapping is avoided by default because it makes backend code visible in the browser. Use this flag to force enabling it.", + "env": "SHOPIFY_HYDROGEN_FLAG_FORCE_CLIENT_SOURCEMAP", + "name": "force-client-sourcemap", + "allowNo": false, + "type": "boolean" + } }, "hasDynamicHelp": false, - "hidden": true, - "hiddenAliases": [ - ], - "id": "kitchen-sink:async", + "hiddenAliases": [], + "id": "hydrogen:build", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", - "strict": true - }, - "kitchen-sink:prompts": { - "aliases": [ - ], - "args": { - }, - "description": "View the UI kit components prompts", + "strict": true, "enableJsonFlag": false, - "flags": { - }, - "hasDynamicHelp": false, - "hidden": true, - "hiddenAliases": [ - ], - "id": "kitchen-sink:prompts", - "pluginAlias": "@shopify/cli", - "pluginName": "@shopify/cli", - "pluginType": "core", - "strict": true + "descriptionWithMarkdown": "Builds a Hydrogen storefront for production. The client and app worker files are compiled to a `/dist` folder in your Hydrogen project directory.", + "customPluginName": "@shopify/cli-hydrogen" }, - "kitchen-sink:static": { - "aliases": [ - ], + "hydrogen:check": { + "aliases": [], "args": { + "resource": { + "description": "The resource to check. Currently only 'routes' is supported.", + "name": "resource", + "options": [ + "routes" + ], + "required": true + } }, - "description": "View the UI kit components that display static output", - "enableJsonFlag": false, + "description": "Returns diagnostic information about a Hydrogen storefront.", "flags": { + "path": { + "description": "The path to the directory of the Hydrogen storefront. Defaults to the current directory where the command is run.", + "env": "SHOPIFY_HYDROGEN_FLAG_PATH", + "name": "path", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + } }, "hasDynamicHelp": false, - "hidden": true, - "hiddenAliases": [ - ], - "id": "kitchen-sink:static", + "hiddenAliases": [], + "id": "hydrogen:check", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", - "strict": true - }, - "notifications:generate": { - "aliases": [ - ], - "args": { - }, - "description": "Generate a notifications.json file for the the CLI, appending a new notification to the current file.", + "strict": true, "enableJsonFlag": false, - "flags": { - }, - "hasDynamicHelp": false, - "hidden": true, - "hiddenAliases": [ - ], - "id": "notifications:generate", - "pluginAlias": "@shopify/cli", - "pluginName": "@shopify/cli", - "pluginType": "core", - "strict": true + "descriptionWithMarkdown": "Checks whether your Hydrogen app includes a set of standard Shopify routes.", + "customPluginName": "@shopify/cli-hydrogen" }, - "notifications:list": { - "aliases": [ - ], - "args": { - }, - "description": "List current notifications configured for the CLI.", - "enableJsonFlag": false, + "hydrogen:codegen": { + "aliases": [], + "args": {}, + "description": "Generate types for the Storefront API queries found in your project.", "flags": { - "ignore-errors": { + "path": { + "description": "The path to the directory of the Hydrogen storefront. Defaults to the current directory where the command is run.", + "env": "SHOPIFY_HYDROGEN_FLAG_PATH", + "name": "path", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "codegen-config-path": { + "description": "Specify a path to a codegen configuration file. Defaults to `/codegen.ts` if it exists.", + "name": "codegen-config-path", + "required": false, + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "force-sfapi-version": { + "description": "Force generating Storefront API types for a specific version instead of using the one provided in Hydrogen. A token can also be provided with this format: `:`.", + "hidden": true, + "name": "force-sfapi-version", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "watch": { + "description": "Watch the project for changes to update types on file save.", + "name": "watch", + "required": false, "allowNo": false, - "description": "Don't fail if an error occurs.", - "env": "SHOPIFY_FLAG_IGNORE_ERRORS", - "hidden": false, - "name": "ignore-errors", "type": "boolean" } }, "hasDynamicHelp": false, - "hidden": true, - "hiddenAliases": [ - ], - "id": "notifications:list", + "hiddenAliases": [], + "id": "hydrogen:codegen", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", - "strict": true - }, - "organization:list": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/app", - "description": "Lists the Shopify organizations that you have access to, along with their organization IDs.", - "descriptionWithMarkdown": "Lists the Shopify organizations that you have access to, along with their organization IDs.", + "strict": true, "enableJsonFlag": false, + "descriptionWithMarkdown": "Automatically generates GraphQL types for your project’s Storefront API queries.", + "customPluginName": "@shopify/cli-hydrogen" + }, + "hydrogen:deploy": { + "aliases": [], + "args": {}, + "description": "Builds and deploys a Hydrogen storefront to Oxygen.", "flags": { - "json": { + "entry": { + "description": "Entry file for the worker. Defaults to `./server`.", + "env": "SHOPIFY_HYDROGEN_FLAG_ENTRY", + "name": "entry", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "env": { + "description": "Specifies the environment to perform the operation using its handle. Fetch the handle using the `env list` command.", + "exclusive": [ + "env-branch" + ], + "name": "env", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "env-branch": { + "deprecated": { + "to": "env", + "message": "--env-branch is deprecated. Use --env instead." + }, + "description": "Specifies the environment to perform the operation using its Git branch name.", + "env": "SHOPIFY_HYDROGEN_ENVIRONMENT_BRANCH", + "name": "env-branch", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "env-file": { + "description": "Path to an environment file to override existing environment variables for the deployment.", + "name": "env-file", + "required": false, + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "preview": { + "description": "Deploys to the Preview environment.", + "name": "preview", + "required": false, "allowNo": false, - "char": "j", - "description": "Output the result as JSON. Automatically disables color output.", - "env": "SHOPIFY_FLAG_JSON", - "hidden": false, - "name": "json", "type": "boolean" }, - "no-color": { - "allowNo": false, - "description": "Disable color output.", - "env": "SHOPIFY_FLAG_NO_COLOR", - "hidden": false, - "name": "no-color", + "force": { + "char": "f", + "description": "Forces a deployment to proceed if there are uncommited changes in its Git repository.", + "env": "SHOPIFY_HYDROGEN_FLAG_FORCE", + "name": "force", + "required": false, + "allowNo": false, + "type": "boolean" + }, + "no-verify": { + "description": "Skip the routability verification step after deployment.", + "name": "no-verify", + "required": false, + "allowNo": false, + "type": "boolean" + }, + "auth-bypass-token": { + "description": "Generate an authentication bypass token, which can be used to perform end-to-end tests against the deployment.", + "env": "AUTH_BYPASS_TOKEN", + "name": "auth-bypass-token", + "required": false, + "allowNo": false, + "type": "boolean" + }, + "auth-bypass-token-duration": { + "dependsOn": [ + "auth-bypass-token" + ], + "description": "Specify the duration (in hours) up to 12 hours for the authentication bypass token. Defaults to `2`", + "env": "AUTH_BYPASS_TOKEN_DURATION", + "name": "auth-bypass-token-duration", + "required": false, + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "build-command": { + "description": "Specify a build command to run before deploying. If not specified, `shopify hydrogen build` will be used.", + "name": "build-command", + "required": false, + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "lockfile-check": { + "description": "Checks that there is exactly one valid lockfile in the project. Defaults to `true`. Deactivate with `--no-lockfile-check`.", + "env": "SHOPIFY_HYDROGEN_FLAG_LOCKFILE_CHECK", + "name": "lockfile-check", + "allowNo": true, + "type": "boolean" + }, + "path": { + "description": "The path to the directory of the Hydrogen storefront. Defaults to the current directory where the command is run.", + "env": "SHOPIFY_HYDROGEN_FLAG_PATH", + "name": "path", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "shop": { + "char": "s", + "description": "Shop URL. It can be the shop prefix (janes-apparel) or the full myshopify.com URL (janes-apparel.myshopify.com, https://janes-apparel.myshopify.com).", + "env": "SHOPIFY_SHOP", + "name": "shop", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "json-output": { + "description": "Create a JSON file containing the deployment details in CI environments. Defaults to true, use `--no-json-output` to disable.", + "name": "json-output", + "required": false, + "allowNo": true, "type": "boolean" }, - "verbose": { + "token": { + "char": "t", + "description": "Oxygen deployment token. Defaults to the linked storefront's token if available.", + "env": "SHOPIFY_HYDROGEN_DEPLOYMENT_TOKEN", + "name": "token", + "required": false, + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "metadata-description": { + "description": "Description of the changes in the deployment. Defaults to the commit message of the latest commit if there are no uncommited changes.", + "env": "SHOPIFY_HYDROGEN_FLAG_METADATA_DESCRIPTION", + "name": "metadata-description", + "required": false, + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "metadata-url": { + "env": "SHOPIFY_HYDROGEN_FLAG_METADATA_URL", + "hidden": true, + "name": "metadata-url", + "required": false, + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "metadata-user": { + "description": "User that initiated the deployment. Will be saved and displayed in the Shopify admin", + "env": "SHOPIFY_HYDROGEN_FLAG_METADATA_USER", + "name": "metadata-user", + "required": false, + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "metadata-version": { + "env": "SHOPIFY_HYDROGEN_FLAG_METADATA_VERSION", + "hidden": true, + "name": "metadata-version", + "required": false, + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "force-client-sourcemap": { + "description": "Client sourcemapping is avoided by default because it makes backend code visible in the browser. Use this flag to force enabling it.", + "env": "SHOPIFY_HYDROGEN_FLAG_FORCE_CLIENT_SOURCEMAP", + "name": "force-client-sourcemap", "allowNo": false, - "description": "Increase the verbosity of the output.", - "env": "SHOPIFY_FLAG_VERBOSE", - "hidden": false, - "name": "verbose", "type": "boolean" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "organization:list", + "hiddenAliases": [], + "id": "hydrogen:deploy", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", "strict": true, - "summary": "List Shopify organizations you have access to." + "enableJsonFlag": false, + "descriptionWithMarkdown": "Builds and deploys your Hydrogen storefront to Oxygen. Requires an Oxygen deployment token to be set with the `--token` flag or an environment variable (`SHOPIFY_HYDROGEN_DEPLOYMENT_TOKEN`). If the storefront is [linked](https://shopify.dev/docs/api/shopify-cli/hydrogen/hydrogen-link) then the Oxygen deployment token for the linked storefront will be used automatically.", + "customPluginName": "@shopify/cli-hydrogen" }, - "plugins": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@oclif/plugin-plugins", - "description": "List installed plugins.", - "enableJsonFlag": true, - "examples": [ - "<%= config.bin %> <%= command.id %>" - ], - "flags": { - "core": { - "allowNo": false, - "description": "Show core plugins.", - "name": "core", - "type": "boolean" - }, - "json": { - "allowNo": false, - "description": "Format output as json.", - "helpGroup": "GLOBAL", - "name": "json", - "type": "boolean" - } - }, + "hydrogen:g": { + "aliases": [], + "args": {}, + "description": "Shortcut for `hydrogen generate`. See `hydrogen generate --help` for more information.", + "flags": {}, "hasDynamicHelp": false, "hidden": true, - "hiddenAliases": [ - ], - "id": "plugins", - "pluginAlias": "@shopify/cli", - "pluginName": "@shopify/cli", - "pluginType": "core", - "strict": true - }, - "plugins:inspect": { - "aliases": [ - ], - "args": { - "plugin": { - "default": ".", - "description": "Plugin to inspect.", - "name": "plugin", - "required": true - } - }, - "customPluginName": "@oclif/plugin-plugins", - "description": "Displays installation properties of a plugin.", - "enableJsonFlag": true, - "examples": [ - "<%= config.bin %> <%= command.id %> <%- config.pjson.oclif.examplePlugin || \"myplugin\" %> " - ], - "flags": { - "help": { - "allowNo": false, - "char": "h", - "description": "Show CLI help.", - "name": "help", - "type": "boolean" - }, - "json": { - "allowNo": false, - "description": "Format output as json.", - "helpGroup": "GLOBAL", - "name": "json", - "type": "boolean" - }, - "verbose": { - "allowNo": false, - "char": "v", - "name": "verbose", - "type": "boolean" - } - }, - "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "plugins:inspect", + "hiddenAliases": [], + "id": "hydrogen:g", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", "strict": false, - "usage": "plugins:inspect PLUGIN..." + "enableJsonFlag": false, + "customPluginName": "@shopify/cli-hydrogen" }, - "plugins:install": { - "aliases": [ - "plugins:add" - ], - "args": { - "plugin": { - "description": "Plugin to install.", - "name": "plugin", - "required": true - } - }, - "customPluginName": "@oclif/plugin-plugins", - "description": "", - "enableJsonFlag": true, - "examples": [ - { - "command": "<%= config.bin %> <%= command.id %> <%- config.pjson.oclif.examplePlugin || \"myplugin\" %> ", - "description": "Install a plugin from npm registry." - }, - { - "command": "<%= config.bin %> <%= command.id %> https://github.com/someuser/someplugin", - "description": "Install a plugin from a github url." - }, - { - "command": "<%= config.bin %> <%= command.id %> someuser/someplugin", - "description": "Install a plugin from a github slug." - } - ], + "hydrogen:init": { + "aliases": [], + "args": {}, + "description": "Creates a new Hydrogen storefront.", "flags": { "force": { - "allowNo": false, "char": "f", - "description": "Force npm to fetch remote resources even if a local copy exists on disk.", + "description": "Overwrites the destination directory and files if they already exist.", + "env": "SHOPIFY_HYDROGEN_FLAG_FORCE", "name": "force", - "type": "boolean" - }, - "help": { "allowNo": false, - "char": "h", - "description": "Show CLI help.", - "name": "help", "type": "boolean" }, - "jit": { - "allowNo": false, - "hidden": true, - "name": "jit", - "type": "boolean" + "path": { + "description": "The path to the directory of the new Hydrogen storefront.", + "env": "SHOPIFY_HYDROGEN_FLAG_PATH", + "name": "path", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" }, - "json": { - "allowNo": false, - "description": "Format output as json.", - "helpGroup": "GLOBAL", - "name": "json", - "type": "boolean" + "language": { + "description": "Sets the template language to use. One of `js` or `ts`.", + "env": "SHOPIFY_HYDROGEN_FLAG_LANGUAGE", + "name": "language", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" }, - "silent": { - "allowNo": false, - "char": "s", - "description": "Silences npm output.", - "exclusive": [ - "verbose" - ], - "name": "silent", + "template": { + "description": "Scaffolds project based on an existing template or example from the Hydrogen repository.", + "env": "SHOPIFY_HYDROGEN_FLAG_TEMPLATE", + "name": "template", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "install-deps": { + "description": "Auto installs dependencies using the active package manager.", + "env": "SHOPIFY_HYDROGEN_FLAG_INSTALL_DEPS", + "name": "install-deps", + "allowNo": true, "type": "boolean" }, - "verbose": { + "mock-shop": { + "description": "Use mock.shop as the data source for the storefront.", + "env": "SHOPIFY_HYDROGEN_FLAG_MOCK_DATA", + "name": "mock-shop", "allowNo": false, - "char": "v", - "description": "Show verbose npm output.", - "exclusive": [ - "silent" - ], - "name": "verbose", "type": "boolean" - } - }, - "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "plugins:install", - "pluginAlias": "@shopify/cli", - "pluginName": "@shopify/cli", - "pluginType": "core", - "strict": false, - "summary": "Installs a plugin into <%= config.bin %>." - }, - "plugins:link": { - "aliases": [ - ], - "args": { - "path": { - "default": ".", - "description": "path to plugin", - "name": "path", - "required": true - } - }, - "customPluginName": "@oclif/plugin-plugins", - "description": "Installation of a linked plugin will override a user-installed or core plugin.\n\ne.g. If you have a user-installed or core plugin that has a 'hello' command, installing a linked plugin with a 'hello' command will override the user-installed or core plugin implementation. This is useful for development work.\n", - "enableJsonFlag": false, - "examples": [ - "<%= config.bin %> <%= command.id %> <%- config.pjson.oclif.examplePlugin || \"myplugin\" %> " - ], - "flags": { - "help": { - "allowNo": false, - "char": "h", - "description": "Show CLI help.", - "name": "help", + }, + "styling": { + "description": "Sets the styling strategy to use. One of `tailwind`, `vanilla-extract`, `css-modules`, `postcss`, `none`.", + "env": "SHOPIFY_HYDROGEN_FLAG_STYLING", + "name": "styling", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "markets": { + "description": "Sets the URL structure to support multiple markets. Must be one of: `subfolders`, `domains`, `subdomains`, `none`. Example: `--markets subfolders`.", + "env": "SHOPIFY_HYDROGEN_FLAG_I18N", + "name": "markets", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "shortcut": { + "description": "Creates a global h2 shortcut for Shopify CLI using shell aliases. Deactivate with `--no-shortcut`.", + "env": "SHOPIFY_HYDROGEN_FLAG_SHORTCUT", + "name": "shortcut", + "allowNo": true, "type": "boolean" }, - "install": { + "git": { + "description": "Init Git and create initial commits.", + "env": "SHOPIFY_HYDROGEN_FLAG_GIT", + "name": "git", "allowNo": true, - "description": "Install dependencies after linking the plugin.", - "name": "install", "type": "boolean" }, - "verbose": { + "quickstart": { + "description": "Scaffolds a new Hydrogen project with a set of sensible defaults. Equivalent to `shopify hydrogen init --path hydrogen-quickstart --mock-shop --language js --shortcut --markets none`", + "env": "SHOPIFY_HYDROGEN_FLAG_QUICKSTART", + "name": "quickstart", "allowNo": false, - "char": "v", - "name": "verbose", "type": "boolean" + }, + "package-manager": { + "env": "SHOPIFY_HYDROGEN_FLAG_PACKAGE_MANAGER", + "hidden": true, + "name": "package-manager", + "hasDynamicHelp": false, + "multiple": false, + "options": [ + "npm", + "yarn", + "pnpm", + "unknown" + ], + "type": "option" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "plugins:link", + "hiddenAliases": [], + "id": "hydrogen:init", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", "strict": true, - "summary": "Links a plugin into the CLI for development." - }, - "plugins:reset": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@oclif/plugin-plugins", "enableJsonFlag": false, + "descriptionWithMarkdown": "Creates a new Hydrogen storefront.", + "customPluginName": "@shopify/cli-hydrogen" + }, + "hydrogen:link": { + "aliases": [], + "args": {}, + "description": "Link a local project to one of your shop's Hydrogen storefronts.", "flags": { - "hard": { + "force": { + "char": "f", + "description": "Overwrites the destination directory and files if they already exist.", + "env": "SHOPIFY_HYDROGEN_FLAG_FORCE", + "name": "force", "allowNo": false, - "name": "hard", - "summary": "Delete node_modules and package manager related files in addition to uninstalling plugins.", "type": "boolean" }, - "reinstall": { - "allowNo": false, - "name": "reinstall", - "summary": "Reinstall all plugins after uninstalling.", - "type": "boolean" + "path": { + "description": "The path to the directory of the Hydrogen storefront. Defaults to the current directory where the command is run.", + "env": "SHOPIFY_HYDROGEN_FLAG_PATH", + "name": "path", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "storefront": { + "description": "The name of a Hydrogen Storefront (e.g. \"Jane's Apparel\")", + "env": "SHOPIFY_HYDROGEN_STOREFRONT", + "name": "storefront", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "plugins:reset", + "hiddenAliases": [], + "id": "hydrogen:link", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", "strict": true, - "summary": "Remove all user-installed and linked plugins." - }, - "plugins:uninstall": { - "aliases": [ - "plugins:unlink", - "plugins:remove" - ], - "args": { - "plugin": { - "description": "plugin to uninstall", - "name": "plugin" - } - }, - "customPluginName": "@oclif/plugin-plugins", - "description": "Removes a plugin from the CLI.", "enableJsonFlag": false, - "examples": [ - "<%= config.bin %> <%= command.id %> <%- config.pjson.oclif.examplePlugin || \"myplugin\" %>" - ], + "descriptionWithMarkdown": "Links your local development environment to a remote Hydrogen storefront. You can link an unlimited number of development environments to a single Hydrogen storefront.\n\n Linking to a Hydrogen storefront enables you to run [dev](https://shopify.dev/docs/api/shopify-cli/hydrogen/hydrogen-dev) and automatically inject your linked Hydrogen storefront's environment variables directly into the server runtime.\n\n After you run the `link` command, you can access the [env list](https://shopify.dev/docs/api/shopify-cli/hydrogen/hydrogen-env-list), [env pull](https://shopify.dev/docs/api/shopify-cli/hydrogen/hydrogen-env-pull), and [unlink](https://shopify.dev/docs/api/shopify-cli/hydrogen/hydrogen-unlink) commands.", + "customPluginName": "@shopify/cli-hydrogen" + }, + "hydrogen:list": { + "aliases": [], + "args": {}, + "description": "Returns a list of Hydrogen storefronts available on a given shop.", "flags": { - "help": { - "allowNo": false, - "char": "h", - "description": "Show CLI help.", - "name": "help", - "type": "boolean" - }, - "verbose": { - "allowNo": false, - "char": "v", - "name": "verbose", - "type": "boolean" + "path": { + "description": "The path to the directory of the Hydrogen storefront. Defaults to the current directory where the command is run.", + "env": "SHOPIFY_HYDROGEN_FLAG_PATH", + "name": "path", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "plugins:uninstall", + "hiddenAliases": [], + "id": "hydrogen:list", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", - "strict": false - }, - "plugins:update": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@oclif/plugin-plugins", - "description": "Update installed plugins.", + "strict": true, "enableJsonFlag": false, + "descriptionWithMarkdown": "Lists all remote Hydrogen storefronts available to link to your local development environment.", + "customPluginName": "@shopify/cli-hydrogen" + }, + "hydrogen:login": { + "aliases": [], + "args": {}, + "description": "Login to your Shopify account.", "flags": { - "help": { - "allowNo": false, - "char": "h", - "description": "Show CLI help.", - "name": "help", - "type": "boolean" + "path": { + "description": "The path to the directory of the Hydrogen storefront. Defaults to the current directory where the command is run.", + "env": "SHOPIFY_HYDROGEN_FLAG_PATH", + "name": "path", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" }, - "verbose": { - "allowNo": false, - "char": "v", - "name": "verbose", - "type": "boolean" + "shop": { + "char": "s", + "description": "Shop URL. It can be the shop prefix (janes-apparel) or the full myshopify.com URL (janes-apparel.myshopify.com, https://janes-apparel.myshopify.com).", + "env": "SHOPIFY_SHOP", + "name": "shop", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "plugins:update", + "hiddenAliases": [], + "id": "hydrogen:login", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", - "strict": true - }, - "search": { - "aliases": [ - ], - "args": { - "query": { - "name": "query" - } - }, - "description": "Search shopify.dev for the most relevant content matching a query. Best for discovery — surfacing the relevant pieces of documentation for a topic, rather than retrieving a whole document. To download a full document verbatim, use `doc fetch`.", + "strict": true, "enableJsonFlag": false, - "examples": [ - "# open the search modal on Shopify.dev\n shopify search\n\n # search for a term on Shopify.dev\n shopify search \n\n # search for a phrase on Shopify.dev\n shopify search \"\"\n " - ], - "flags": { - "no-color": { - "allowNo": false, - "description": "Disable color output.", - "env": "SHOPIFY_FLAG_NO_COLOR", - "hidden": false, - "name": "no-color", - "type": "boolean" - }, - "verbose": { - "allowNo": false, - "description": "Increase the verbosity of the output.", - "env": "SHOPIFY_FLAG_VERBOSE", - "hidden": false, - "name": "verbose", - "type": "boolean" + "descriptionWithMarkdown": "Logs in to the specified shop and saves the shop domain to the project.", + "customPluginName": "@shopify/cli-hydrogen" + }, + "hydrogen:logout": { + "aliases": [], + "args": {}, + "description": "Logout of your local session.", + "flags": { + "path": { + "description": "The path to the directory of the Hydrogen storefront. Defaults to the current directory where the command is run.", + "env": "SHOPIFY_HYDROGEN_FLAG_PATH", + "name": "path", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "search", + "hiddenAliases": [], + "id": "hydrogen:logout", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", "strict": true, - "usage": "search [query]" + "enableJsonFlag": false, + "descriptionWithMarkdown": "Log out from the current shop.", + "customPluginName": "@shopify/cli-hydrogen" }, - "store:auth": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/store", - "description": "Authenticates the app against the specified store for store commands and stores an online access token for later reuse.\n\nRe-run this command if the stored token is missing, expires, or no longer has the scopes you need.", - "descriptionWithMarkdown": "Authenticates the app against the specified store for store commands and stores an online access token for later reuse.\n\nRe-run this command if the stored token is missing, expires, or no longer has the scopes you need.", - "examples": [ - "<%= config.bin %> <%= command.id %> --store shop.myshopify.com --scopes read_products,write_products", - "<%= config.bin %> <%= command.id %> --store shop.myshopify.com --scopes read_products,write_products --json" - ], + "hydrogen:preview": { + "aliases": [], + "args": {}, + "description": "Runs a Hydrogen storefront in an Oxygen worker for production.", "flags": { - "json": { - "allowNo": false, - "char": "j", - "description": "Output the result as JSON. Automatically disables color output.", - "env": "SHOPIFY_FLAG_JSON", - "hidden": false, - "name": "json", - "type": "boolean" + "path": { + "description": "The path to the directory of the Hydrogen storefront. Defaults to the current directory where the command is run.", + "env": "SHOPIFY_HYDROGEN_FLAG_PATH", + "name": "path", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" }, - "no-color": { - "allowNo": false, - "description": "Disable color output.", - "env": "SHOPIFY_FLAG_NO_COLOR", - "hidden": false, - "name": "no-color", - "type": "boolean" + "port": { + "description": "The port to run the server on. Defaults to 3000.", + "env": "SHOPIFY_HYDROGEN_FLAG_PORT", + "name": "port", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" }, - "scopes": { - "description": "Comma-separated Admin API scopes to request for the app.", - "env": "SHOPIFY_FLAG_SCOPES", + "env": { + "description": "Specifies the environment to perform the operation using its handle. Fetch the handle using the `env list` command.", + "exclusive": [ + "env-branch" + ], + "name": "env", "hasDynamicHelp": false, "multiple": false, - "name": "scopes", - "required": true, "type": "option" }, - "store": { - "char": "s", - "description": "The myshopify.com domain of the store.", - "env": "SHOPIFY_FLAG_STORE", + "env-branch": { + "deprecated": { + "to": "env", + "message": "--env-branch is deprecated. Use --env instead." + }, + "description": "Specifies the environment to perform the operation using its Git branch name.", + "env": "SHOPIFY_HYDROGEN_ENVIRONMENT_BRANCH", + "name": "env-branch", "hasDynamicHelp": false, "multiple": false, - "name": "store", - "required": true, "type": "option" }, - "verbose": { + "env-file": { + "description": "Path to an environment file to override existing environment variables. Defaults to the '.env' located in your project path `--path`.", + "name": "env-file", + "required": false, + "default": ".env", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "inspector-port": { + "description": "The port where the inspector is available. Defaults to 9229.", + "env": "SHOPIFY_HYDROGEN_FLAG_INSPECTOR_PORT", + "name": "inspector-port", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "debug": { + "description": "Enables inspector connections to the server with a debugger such as Visual Studio Code or Chrome DevTools.", + "env": "SHOPIFY_HYDROGEN_FLAG_DEBUG", + "name": "debug", "allowNo": false, - "description": "Increase the verbosity of the output.", - "env": "SHOPIFY_FLAG_VERBOSE", - "hidden": false, + "type": "boolean" + }, + "verbose": { + "description": "Outputs more information about the command's execution.", + "env": "SHOPIFY_HYDROGEN_FLAG_VERBOSE", "name": "verbose", + "required": false, + "allowNo": false, "type": "boolean" - } - }, - "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "store:auth", - "pluginAlias": "@shopify/cli", - "pluginName": "@shopify/cli", - "pluginType": "core", - "strict": true, - "summary": "Authenticate an app against a store for store commands." - }, - "store:auth:list": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/store", - "description": "Lists stores authenticated directly on this machine with `shopify store auth`.\n\nUse this command to find stores that can be used with store-authenticated commands such as `shopify store execute`.\nTo list stores in a Shopify organization, run `shopify store list`.", - "descriptionWithMarkdown": "Lists stores authenticated directly on this machine with `shopify store auth`.\n\nUse this command to find stores that can be used with store-authenticated commands such as `shopify store execute`.\nTo list stores in a Shopify organization, run `shopify store list`.", - "enableJsonFlag": false, - "examples": [ - "<%= config.bin %> <%= command.id %>", - "<%= config.bin %> <%= command.id %> --json" - ], - "flags": { - "json": { + }, + "build": { + "description": "Builds the app before starting the preview server.", + "name": "build", "allowNo": false, - "char": "j", - "description": "Output the result as JSON. Automatically disables color output.", - "env": "SHOPIFY_FLAG_JSON", - "hidden": false, - "name": "json", "type": "boolean" }, - "no-color": { + "watch": { + "dependsOn": [ + "build" + ], + "description": "Watches for changes and rebuilds the project.", + "name": "watch", "allowNo": false, - "description": "Disable color output.", - "env": "SHOPIFY_FLAG_NO_COLOR", - "hidden": false, - "name": "no-color", "type": "boolean" }, - "verbose": { + "entry": { + "dependsOn": [ + "build" + ], + "description": "Entry file for the worker. Defaults to `./server`.", + "env": "SHOPIFY_HYDROGEN_FLAG_ENTRY", + "name": "entry", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "codegen": { + "dependsOn": [ + "build" + ], + "description": "Automatically generates GraphQL types for your project’s Storefront API queries.", + "name": "codegen", + "required": false, "allowNo": false, - "description": "Increase the verbosity of the output.", - "env": "SHOPIFY_FLAG_VERBOSE", - "hidden": false, - "name": "verbose", "type": "boolean" + }, + "codegen-config-path": { + "dependsOn": [ + "codegen" + ], + "description": "Specifies a path to a codegen configuration file. Defaults to `/codegen.ts` if this file exists.", + "name": "codegen-config-path", + "required": false, + "hasDynamicHelp": false, + "multiple": false, + "type": "option" } }, "hasDynamicHelp": false, - "hidden": true, - "hiddenAliases": [ - ], - "id": "store:auth:list", + "hiddenAliases": [], + "id": "hydrogen:preview", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", "strict": true, - "summary": "List stores authenticated directly with store auth." - }, - "store:create:dev": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/store", - "description": "Creates a new app development store in your organization.", - "descriptionWithMarkdown": "Creates a new app development store in your organization.", "enableJsonFlag": false, + "descriptionWithMarkdown": "Runs a server in your local development environment that serves your Hydrogen app's production build. Requires running the [build](https://shopify.dev/docs/api/shopify-cli/hydrogen/hydrogen-build) command first.", + "customPluginName": "@shopify/cli-hydrogen" + }, + "hydrogen:setup": { + "aliases": [], + "args": {}, + "description": "Scaffold routes and core functionality.", "flags": { - "json": { + "path": { + "description": "The path to the directory of the Hydrogen storefront. Defaults to the current directory where the command is run.", + "env": "SHOPIFY_HYDROGEN_FLAG_PATH", + "name": "path", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "force": { + "char": "f", + "description": "Overwrites the destination directory and files if they already exist.", + "env": "SHOPIFY_HYDROGEN_FLAG_FORCE", + "name": "force", "allowNo": false, - "char": "j", - "description": "Output the result as JSON. Automatically disables color output.", - "env": "SHOPIFY_FLAG_JSON", - "hidden": false, - "name": "json", "type": "boolean" }, - "name": { - "description": "Name for the new development store.", - "env": "SHOPIFY_FLAG_STORE_NAME", + "markets": { + "description": "Sets the URL structure to support multiple markets. Must be one of: `subfolders`, `domains`, `subdomains`, `none`. Example: `--markets subfolders`.", + "env": "SHOPIFY_HYDROGEN_FLAG_I18N", + "name": "markets", "hasDynamicHelp": false, "multiple": false, - "name": "name", - "required": true, "type": "option" }, - "no-color": { - "allowNo": false, - "description": "Disable color output.", - "env": "SHOPIFY_FLAG_NO_COLOR", - "hidden": false, - "name": "no-color", + "shortcut": { + "description": "Creates a global h2 shortcut for Shopify CLI using shell aliases. Deactivate with `--no-shortcut`.", + "env": "SHOPIFY_HYDROGEN_FLAG_SHORTCUT", + "name": "shortcut", + "allowNo": true, "type": "boolean" }, - "organization-id": { - "description": "The numeric organization ID. Auto-selects if you belong to a single organization.", - "env": "SHOPIFY_FLAG_ORGANIZATION_ID", + "install-deps": { + "description": "Auto installs dependencies using the active package manager.", + "env": "SHOPIFY_HYDROGEN_FLAG_INSTALL_DEPS", + "name": "install-deps", + "allowNo": true, + "type": "boolean" + } + }, + "hasDynamicHelp": false, + "hiddenAliases": [], + "id": "hydrogen:setup", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true, + "enableJsonFlag": false, + "customPluginName": "@shopify/cli-hydrogen" + }, + "hydrogen:shortcut": { + "aliases": [], + "args": {}, + "description": "Creates a global `h2` shortcut for the Hydrogen CLI", + "flags": {}, + "hasDynamicHelp": false, + "hiddenAliases": [], + "id": "hydrogen:shortcut", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true, + "enableJsonFlag": false, + "descriptionWithMarkdown": "Creates a global h2 shortcut for Shopify CLI using shell aliases.\n\n The following shells are supported:\n\n - Bash (using `~/.bashrc`)\n - ZSH (using `~/.zshrc`)\n - Fish (using `~/.config/fish/functions`)\n - PowerShell (added to `$PROFILE`)\n\n After the alias is created, you can call Shopify CLI from anywhere in your project using `h2 `.", + "customPluginName": "@shopify/cli-hydrogen" + }, + "hydrogen:unlink": { + "aliases": [], + "args": {}, + "description": "Unlink a local project from a Hydrogen storefront.", + "flags": { + "path": { + "description": "The path to the directory of the Hydrogen storefront. Defaults to the current directory where the command is run.", + "env": "SHOPIFY_HYDROGEN_FLAG_PATH", + "name": "path", "hasDynamicHelp": false, "multiple": false, - "name": "organization-id", "type": "option" - }, - "verbose": { - "allowNo": false, - "description": "Increase the verbosity of the output.", - "env": "SHOPIFY_FLAG_VERBOSE", - "hidden": false, - "name": "verbose", - "type": "boolean" } }, "hasDynamicHelp": false, - "hidden": true, - "hiddenAliases": [ - ], - "id": "store:create:dev", + "hiddenAliases": [], + "id": "hydrogen:unlink", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", "strict": true, - "summary": "Create a new development store." + "enableJsonFlag": false, + "descriptionWithMarkdown": "Unlinks your local development environment from a remote Hydrogen storefront.", + "customPluginName": "@shopify/cli-hydrogen" }, - "store:create:preview": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/store", - "description": "Creates a new Shopify store, with no need for an existing account.", - "descriptionWithMarkdown": "Creates a new Shopify store, with no need for an existing account.", - "examples": [ - "<%= config.bin %> <%= command.id %> --name \"Lavender Candles\"", - "<%= config.bin %> <%= command.id %> --name \"Lavender Candles\" --country US", - "<%= config.bin %> <%= command.id %> --name \"Lavender Candles\" --json" - ], + "hydrogen:upgrade": { + "aliases": [], + "args": {}, + "description": "Upgrade Remix and Hydrogen npm dependencies.", "flags": { - "country": { - "description": "Two-letter country code for the store, such as US, CA, or GB.", - "env": "SHOPIFY_FLAG_PREVIEW_STORE_COUNTRY", + "path": { + "description": "The path to the directory of the Hydrogen storefront. Defaults to the current directory where the command is run.", + "env": "SHOPIFY_HYDROGEN_FLAG_PATH", + "name": "path", "hasDynamicHelp": false, "multiple": false, - "name": "country", - "required": false, "type": "option" }, - "json": { - "allowNo": false, - "char": "j", - "description": "Output the result as JSON. Automatically disables color output.", - "env": "SHOPIFY_FLAG_JSON", - "hidden": false, - "name": "json", - "type": "boolean" - }, - "name": { - "description": "The name of the store.", - "env": "SHOPIFY_FLAG_PREVIEW_STORE_NAME", + "version": { + "char": "v", + "description": "A target hydrogen version to update to", + "name": "version", + "required": false, "hasDynamicHelp": false, "multiple": false, - "name": "name", - "required": false, "type": "option" }, - "no-color": { - "allowNo": false, - "description": "Disable color output.", - "env": "SHOPIFY_FLAG_NO_COLOR", - "hidden": false, - "name": "no-color", - "type": "boolean" - }, - "verbose": { + "force": { + "char": "f", + "description": "Ignore warnings and force the upgrade to the target version", + "env": "SHOPIFY_HYDROGEN_FLAG_FORCE", + "name": "force", "allowNo": false, - "description": "Increase the verbosity of the output.", - "env": "SHOPIFY_FLAG_VERBOSE", - "hidden": false, - "name": "verbose", "type": "boolean" } }, "hasDynamicHelp": false, - "hidden": true, - "hiddenAliases": [ - ], - "id": "store:create:preview", + "hiddenAliases": [], + "id": "hydrogen:upgrade", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", "strict": true, - "summary": "Create a preview Shopify store." + "enableJsonFlag": false, + "descriptionWithMarkdown": "Upgrade Hydrogen project dependencies, preview features, fixes and breaking changes. The command also generates an instruction file for each upgrade.", + "customPluginName": "@shopify/cli-hydrogen" }, - "store:execute": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/store", - "description": "Executes an Admin API GraphQL query or mutation on the specified store using previously stored app authentication.\n\nRun `shopify store auth` first to create stored auth for the store.\n\nMutations are disabled by default. Re-run with `--allow-mutations` if you intend to modify store data.", - "descriptionWithMarkdown": "Executes an Admin API GraphQL query or mutation on the specified store using previously stored app authentication.\n\nRun `shopify store auth` first to create stored auth for the store.\n\nMutations are disabled by default. Re-run with `--allow-mutations` if you intend to modify store data.", - "examples": [ - "<%= config.bin %> <%= command.id %> --store shop.myshopify.com --query \"query { shop { name } }\"", - "<%= config.bin %> <%= command.id %> --store shop.myshopify.com --query-file ./operation.graphql --variables '{\"id\":\"gid://shopify/Product/1\"}'", - "<%= config.bin %> <%= command.id %> --store shop.myshopify.com --query \"mutation { shop { id } }\" --allow-mutations", - "<%= config.bin %> <%= command.id %> --store shop.myshopify.com --query \"query { shop { name } }\" --json" - ], + "hydrogen:customer-account-push": { + "aliases": [], + "args": {}, + "description": "Push project configuration to admin", "flags": { - "allow-mutations": { - "allowNo": false, - "description": "Allow GraphQL mutations to run against the target store.", - "env": "SHOPIFY_FLAG_ALLOW_MUTATIONS", - "name": "allow-mutations", - "type": "boolean" - }, - "json": { - "allowNo": false, - "char": "j", - "description": "Output the result as JSON. Automatically disables color output.", - "env": "SHOPIFY_FLAG_JSON", - "hidden": false, - "name": "json", - "type": "boolean" - }, - "no-color": { - "allowNo": false, - "description": "Disable color output.", - "env": "SHOPIFY_FLAG_NO_COLOR", - "hidden": false, - "name": "no-color", - "type": "boolean" - }, - "output-file": { - "description": "The file name where results should be written, instead of STDOUT.", - "env": "SHOPIFY_FLAG_OUTPUT_FILE", - "hasDynamicHelp": false, - "multiple": false, - "name": "output-file", - "type": "option" - }, - "query": { - "char": "q", - "description": "The GraphQL query or mutation, as a string.", - "env": "SHOPIFY_FLAG_QUERY", + "path": { + "description": "The path to the directory of the Hydrogen storefront. Defaults to the current directory where the command is run.", + "env": "SHOPIFY_HYDROGEN_FLAG_PATH", + "name": "path", "hasDynamicHelp": false, "multiple": false, - "name": "query", - "required": false, "type": "option" }, - "query-file": { - "description": "Path to a file containing the GraphQL query or mutation. Can't be used with --query.", - "env": "SHOPIFY_FLAG_QUERY_FILE", + "storefront-id": { + "description": "The id of the storefront the configuration should be pushed to. Must start with 'gid://shopify/HydrogenStorefront/'", + "name": "storefront-id", "hasDynamicHelp": false, "multiple": false, - "name": "query-file", "type": "option" }, - "store": { - "char": "s", - "description": "The myshopify.com domain of the store.", - "env": "SHOPIFY_FLAG_STORE", - "hasDynamicHelp": false, - "multiple": false, - "name": "store", + "dev-origin": { + "description": "The development domain of your application.", + "name": "dev-origin", "required": true, - "type": "option" - }, - "variable-file": { - "description": "Path to a file containing GraphQL variables in JSON format. Can't be used with --variables.", - "env": "SHOPIFY_FLAG_VARIABLE_FILE", - "exclusive": [ - "variables" - ], "hasDynamicHelp": false, "multiple": false, - "name": "variable-file", "type": "option" }, - "variables": { - "char": "v", - "description": "The values for any GraphQL variables in your query or mutation, in JSON format.", - "env": "SHOPIFY_FLAG_VARIABLES", - "exclusive": [ - "variable-file" - ], + "relative-redirect-uri": { + "description": "The relative url of allowed callback url for Customer Account API OAuth flow. Default is '/account/authorize'", + "name": "relative-redirect-uri", "hasDynamicHelp": false, "multiple": false, - "name": "variables", "type": "option" }, - "verbose": { - "allowNo": false, - "description": "Increase the verbosity of the output.", - "env": "SHOPIFY_FLAG_VERBOSE", - "hidden": false, - "name": "verbose", - "type": "boolean" - }, - "version": { - "description": "The API version to use for the query or mutation. Defaults to the latest stable version.", - "env": "SHOPIFY_FLAG_VERSION", + "relative-logout-uri": { + "description": "The relative url of allowed url that will be redirected to post-logout for Customer Account API OAuth flow. Default to nothing.", + "name": "relative-logout-uri", "hasDynamicHelp": false, "multiple": false, - "name": "version", "type": "option" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "store:execute", + "hiddenAliases": [], + "id": "hydrogen:customer-account-push", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", "strict": true, - "summary": "Execute GraphQL queries and mutations on a store." + "enableJsonFlag": false, + "customPluginName": "@shopify/cli-hydrogen" }, - "store:info": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/store", - "description": "Returns available metadata about a store you have access to, such as its id, display name, subdomain, organization, store owner, type, plan, feature preview, admin URL, and access and save URLs for preview stores.\n\nSome details may be omitted when they are not available for the store.\n\nUse `--json` for machine-readable output.", - "descriptionWithMarkdown": "Returns available metadata about a store you have access to, such as its id, display name, subdomain, organization, store owner, type, plan, feature preview, admin URL, and access and save URLs for preview stores.\n\nSome details may be omitted when they are not available for the store.\n\nUse `--json` for machine-readable output.", - "examples": [ - "<%= config.bin %> <%= command.id %> --store shop.myshopify.com", - "<%= config.bin %> <%= command.id %> --store shop.myshopify.com --json" - ], + "hydrogen:debug:cpu": { + "aliases": [], + "args": {}, + "description": "Builds and profiles the server startup time the app.", "flags": { - "json": { - "allowNo": false, - "char": "j", - "description": "Output the result as JSON. Automatically disables color output.", - "env": "SHOPIFY_FLAG_JSON", - "hidden": false, - "name": "json", - "type": "boolean" - }, - "no-color": { - "allowNo": false, - "description": "Disable color output.", - "env": "SHOPIFY_FLAG_NO_COLOR", - "hidden": false, - "name": "no-color", - "type": "boolean" + "path": { + "description": "The path to the directory of the Hydrogen storefront. Defaults to the current directory where the command is run.", + "env": "SHOPIFY_HYDROGEN_FLAG_PATH", + "name": "path", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" }, - "store": { - "char": "s", - "description": "The myshopify.com domain of the store.", - "env": "SHOPIFY_FLAG_STORE", + "entry": { + "description": "Entry file for the worker. Defaults to `./server`.", + "env": "SHOPIFY_HYDROGEN_FLAG_ENTRY", + "name": "entry", "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" + "output": { + "description": "Specify a path to generate the profile file. Defaults to \"startup.cpuprofile\".", + "name": "output", + "required": false, + "default": "startup.cpuprofile", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "store:info", + "hiddenAliases": [], + "id": "hydrogen:debug:cpu", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", "strict": true, - "summary": "Surface metadata about a Shopify store." + "enableJsonFlag": false, + "descriptionWithMarkdown": "Builds the app and runs the resulting code to profile the server startup time, watching for changes. This command can be used to [debug slow app startup times](https://shopify.dev/docs/custom-storefronts/hydrogen/debugging/cpu-startup) that cause failed deployments in Oxygen.\n\n The profiling results are written to a `.cpuprofile` file that can be viewed with certain tools such as [Flame Chart Visualizer for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=ms-vscode.vscode-js-profile-flame).", + "customPluginName": "@shopify/cli-hydrogen" }, - "store:list": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/store", - "description": "Lists stores in a Shopify organization available to the current CLI account.\n\nWhen more than one organization is available, the command prompts you to pick one unless you provide `--organization-id`.\nIn non-interactive environments, `--organization-id` is required.\n\nRun `<%= config.bin %> organization list` to find organization IDs.", - "descriptionWithMarkdown": "Lists stores in a Shopify organization available to the current CLI account.\n\nWhen more than one organization is available, the command prompts you to pick one unless you provide `--organization-id`.\nIn non-interactive environments, `--organization-id` is required.\n\nRun `<%= config.bin %> organization list` to find organization IDs.", - "examples": [ - "<%= config.bin %> <%= command.id %>", - "<%= config.bin %> <%= command.id %> --organization-id 1234567", - "<%= config.bin %> <%= command.id %> --json" - ], + "hydrogen:env:list": { + "aliases": [], + "args": {}, + "description": "List the environments on your linked Hydrogen storefront.", "flags": { - "json": { - "allowNo": false, - "char": "j", - "description": "Output the result as JSON. Automatically disables color output.", - "env": "SHOPIFY_FLAG_JSON", - "hidden": false, - "name": "json", - "type": "boolean" - }, - "no-color": { - "allowNo": false, - "description": "Disable color output.", - "env": "SHOPIFY_FLAG_NO_COLOR", - "hidden": false, - "name": "no-color", - "type": "boolean" - }, - "organization-id": { - "description": "The numeric organization ID. Auto-selects if you belong to a single organization.", - "env": "SHOPIFY_FLAG_ORGANIZATION_ID", + "path": { + "description": "The path to the directory of the Hydrogen storefront. Defaults to the current directory where the command is run.", + "env": "SHOPIFY_HYDROGEN_FLAG_PATH", + "name": "path", "hasDynamicHelp": false, "multiple": false, - "name": "organization-id", "type": "option" - }, - "verbose": { - "allowNo": false, - "description": "Increase the verbosity of the output.", - "env": "SHOPIFY_FLAG_VERBOSE", - "hidden": false, - "name": "verbose", - "type": "boolean" } }, "hasDynamicHelp": false, - "hidden": true, - "hiddenAliases": [ - ], - "id": "store:list", + "hiddenAliases": [], + "id": "hydrogen:env:list", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", "strict": true, - "summary": "List stores in a Shopify organization." + "enableJsonFlag": false, + "descriptionWithMarkdown": "Lists all environments available on the linked Hydrogen storefront.", + "customPluginName": "@shopify/cli-hydrogen" }, - "theme:check": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/theme", - "description": "Calls and runs \"Theme Check\" (https://shopify.dev/docs/themes/tools/theme-check) to analyze your theme code for errors and to ensure that it follows theme and Liquid best practices. \"Learn more about the checks that Theme Check runs.\" (https://shopify.dev/docs/themes/tools/theme-check/checks)", - "descriptionWithMarkdown": "Calls and runs [Theme Check](https://shopify.dev/docs/themes/tools/theme-check) to analyze your theme code for errors and to ensure that it follows theme and Liquid best practices. [Learn more about the checks that Theme Check runs.](https://shopify.dev/docs/themes/tools/theme-check/checks)", + "hydrogen:env:pull": { + "aliases": [], + "args": {}, + "description": "Populate your .env with variables from your Hydrogen storefront.", "flags": { - "auto-correct": { - "allowNo": false, - "char": "a", - "description": "Automatically fix offenses", - "env": "SHOPIFY_FLAG_AUTO_CORRECT", - "name": "auto-correct", - "required": false, - "type": "boolean" + "env": { + "description": "Specifies the environment to perform the operation using its handle. Fetch the handle using the `env list` command.", + "exclusive": [ + "env-branch" + ], + "name": "env", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" }, - "config": { - "char": "C", - "description": "Use the config provided, overriding .theme-check.yml if present\n Supports all theme-check: config values, e.g., theme-check:theme-app-extension,\n theme-check:recommended, theme-check:all\n For backwards compatibility, :theme_app_extension is also supported ", - "env": "SHOPIFY_FLAG_CONFIG", + "env-branch": { + "deprecated": { + "to": "env", + "message": "--env-branch is deprecated. Use --env instead." + }, + "description": "Specifies the environment to perform the operation using its Git branch name.", + "env": "SHOPIFY_HYDROGEN_ENVIRONMENT_BRANCH", + "name": "env-branch", "hasDynamicHelp": false, "multiple": false, - "name": "config", - "required": false, "type": "option" }, - "environment": { - "char": "e", - "description": "The environment to apply to the current command.", - "env": "SHOPIFY_FLAG_ENVIRONMENT", + "env-file": { + "description": "Path to an environment file to override existing environment variables. Defaults to the '.env' located in your project path `--path`.", + "name": "env-file", + "required": false, + "default": ".env", "hasDynamicHelp": false, - "multiple": true, - "name": "environment", + "multiple": false, "type": "option" }, - "fail-level": { - "default": "error", - "description": "Minimum severity for exit with error code", - "env": "SHOPIFY_FLAG_FAIL_LEVEL", + "path": { + "description": "The path to the directory of the Hydrogen storefront. Defaults to the current directory where the command is run.", + "env": "SHOPIFY_HYDROGEN_FLAG_PATH", + "name": "path", "hasDynamicHelp": false, "multiple": false, - "name": "fail-level", - "options": [ - "crash", - "error", - "suggestion", - "style", - "warning", - "info" - ], - "required": false, "type": "option" }, - "init": { + "force": { + "char": "f", + "description": "Overwrites the destination directory and files if they already exist.", + "env": "SHOPIFY_HYDROGEN_FLAG_FORCE", + "name": "force", "allowNo": false, - "description": "Generate a .theme-check.yml file", - "env": "SHOPIFY_FLAG_INIT", - "name": "init", - "required": false, "type": "boolean" + } + }, + "hasDynamicHelp": false, + "hiddenAliases": [], + "id": "hydrogen:env:pull", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true, + "enableJsonFlag": false, + "descriptionWithMarkdown": "Pulls environment variables from the linked Hydrogen storefront and writes them to an `.env` file.", + "customPluginName": "@shopify/cli-hydrogen" + }, + "hydrogen:env:push": { + "aliases": [], + "args": {}, + "description": "Push environment variables from the local .env file to your linked Hydrogen storefront.", + "flags": { + "env": { + "description": "Specifies the environment to perform the operation using its handle. Fetch the handle using the `env list` command.", + "exclusive": [ + "env-branch" + ], + "name": "env", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" }, - "list": { - "allowNo": false, - "description": "List enabled checks", - "env": "SHOPIFY_FLAG_LIST", - "name": "list", + "env-file": { + "description": "Path to an environment file to override existing environment variables. Defaults to the '.env' located in your project path `--path`.", + "name": "env-file", "required": false, - "type": "boolean" - }, - "no-color": { - "allowNo": false, - "description": "Disable color output.", - "env": "SHOPIFY_FLAG_NO_COLOR", - "hidden": false, - "name": "no-color", - "type": "boolean" - }, - "output": { - "char": "o", - "default": "text", - "description": "The output format to use", - "env": "SHOPIFY_FLAG_OUTPUT", + "default": ".env", "hasDynamicHelp": false, "multiple": false, - "name": "output", - "options": [ - "text", - "json" - ], - "required": false, "type": "option" }, "path": { - "description": "The path where you want to run the command. Defaults to the current working directory.", - "env": "SHOPIFY_FLAG_PATH", + "description": "The path to the directory of the Hydrogen storefront. Defaults to the current directory where the command is run.", + "env": "SHOPIFY_HYDROGEN_FLAG_PATH", + "name": "path", "hasDynamicHelp": false, "multiple": false, - "name": "path", - "noCacheDefault": true, "type": "option" - }, - "print": { - "allowNo": false, - "description": "Output active config to STDOUT", - "env": "SHOPIFY_FLAG_PRINT", - "name": "print", - "required": false, - "type": "boolean" - }, - "verbose": { - "allowNo": false, - "description": "Increase the verbosity of the output.", - "env": "SHOPIFY_FLAG_VERBOSE", - "hidden": false, - "name": "verbose", - "type": "boolean" - }, - "version": { - "allowNo": false, - "char": "v", - "description": "Print Theme Check version", - "env": "SHOPIFY_FLAG_VERSION", - "name": "version", - "required": false, - "type": "boolean" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "theme:check", - "multiEnvironmentsFlags": [ - "path" - ], + "hiddenAliases": [], + "id": "hydrogen:env:push", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", "strict": true, - "summary": "Validate the theme." + "enableJsonFlag": false, + "customPluginName": "@shopify/cli-hydrogen" }, - "theme:console": { - "aliases": [ - ], + "hydrogen:generate:route": { + "aliases": [], "args": { + "routeName": { + "description": "The route to generate. One of home,page,cart,products,collections,policies,blogs,account,search,robots,sitemap,tokenlessApi,all.", + "name": "routeName", + "options": [ + "home", + "page", + "cart", + "products", + "collections", + "policies", + "blogs", + "account", + "search", + "robots", + "sitemap", + "tokenlessApi", + "all" + ], + "required": true + } }, - "customPluginName": "@shopify/theme", - "description": "Starts the Shopify Liquid REPL (read-eval-print loop) tool. This tool provides an interactive terminal interface for evaluating Liquid code and exploring Liquid objects, filters, and tags using real store data.\n\n You can also provide context to the console using a URL, as some Liquid objects are context-specific", - "descriptionWithMarkdown": "Starts the Shopify Liquid REPL (read-eval-print loop) tool. This tool provides an interactive terminal interface for evaluating Liquid code and exploring Liquid objects, filters, and tags using real store data.\n\n You can also provide context to the console using a URL, as some Liquid objects are context-specific", - "flags": { - "environment": { - "char": "e", - "description": "The environment to apply to the current command.", - "env": "SHOPIFY_FLAG_ENVIRONMENT", + "description": "Generates a standard Shopify route.", + "flags": { + "adapter": { + "description": "Remix adapter used in the route. The default is `@shopify/remix-oxygen`.", + "env": "SHOPIFY_HYDROGEN_FLAG_ADAPTER", + "name": "adapter", "hasDynamicHelp": false, - "multiple": true, - "name": "environment", + "multiple": false, "type": "option" }, - "no-color": { + "typescript": { + "description": "Generate TypeScript files", + "env": "SHOPIFY_HYDROGEN_FLAG_TYPESCRIPT", + "name": "typescript", "allowNo": false, - "description": "Disable color output.", - "env": "SHOPIFY_FLAG_NO_COLOR", - "hidden": false, - "name": "no-color", "type": "boolean" }, - "password": { - "description": "Password generated from the Theme Access app or an Admin API token.", - "env": "SHOPIFY_CLI_THEME_TOKEN", + "locale-param": { + "description": "The param name in Remix routes for the i18n locale, if any. Example: `locale` becomes ($locale).", + "env": "SHOPIFY_HYDROGEN_FLAG_ADAPTER", + "name": "locale-param", "hasDynamicHelp": false, "multiple": false, - "name": "password", "type": "option" }, + "force": { + "char": "f", + "description": "Overwrites the destination directory and files if they already exist.", + "env": "SHOPIFY_HYDROGEN_FLAG_FORCE", + "name": "force", + "allowNo": false, + "type": "boolean" + }, "path": { - "description": "The path where you want to run the command. Defaults to the current working directory.", - "env": "SHOPIFY_FLAG_PATH", - "hasDynamicHelp": false, - "multiple": false, + "description": "The path to the directory of the Hydrogen storefront. Defaults to the current directory where the command is run.", + "env": "SHOPIFY_HYDROGEN_FLAG_PATH", "name": "path", - "noCacheDefault": true, - "type": "option" - }, - "store": { - "char": "s", - "description": "Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com, https://example.myshopify.com).", - "env": "SHOPIFY_FLAG_STORE", - "hasDynamicHelp": false, - "multiple": false, - "name": "store", - "type": "option" - }, - "store-password": { - "description": "The password for storefronts with password protection.", - "env": "SHOPIFY_FLAG_STORE_PASSWORD", - "hasDynamicHelp": false, - "multiple": false, - "name": "store-password", - "type": "option" - }, - "url": { - "default": "/", - "description": "The url to be used as context", - "env": "SHOPIFY_FLAG_URL", "hasDynamicHelp": false, "multiple": false, - "name": "url", "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": "theme:console", - "multiEnvironmentsFlags": null, + "hiddenAliases": [], + "id": "hydrogen:generate:route", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", "strict": true, - "summary": "Shopify Liquid REPL (read-eval-print loop) tool", - "usage": [ - "theme console", - "theme console --url /products/classic-leather-jacket" - ] + "enableJsonFlag": false, + "descriptionWithMarkdown": "Generates a set of default routes from the starter template.", + "customPluginName": "@shopify/cli-hydrogen" }, - "theme:delete": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/theme", - "description": "Deletes a theme from your store.\n\n You can specify multiple themes by ID. If no theme is specified, then you're prompted to select the theme that you want to delete from the list of themes in your store.\n\n You're asked to confirm that you want to delete the specified themes before they are deleted. You can skip this confirmation using the `--force` flag.", - "descriptionWithMarkdown": "Deletes a theme from your store.\n\n You can specify multiple themes by ID. If no theme is specified, then you're prompted to select the theme that you want to delete from the list of themes in your store.\n\n You're asked to confirm that you want to delete the specified themes before they are deleted. You can skip this confirmation using the `--force` flag.", + "hydrogen:generate:routes": { + "aliases": [], + "args": {}, + "description": "Generates all supported standard shopify routes.", "flags": { - "development": { + "adapter": { + "description": "Remix adapter used in the route. The default is `@shopify/remix-oxygen`.", + "env": "SHOPIFY_HYDROGEN_FLAG_ADAPTER", + "name": "adapter", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "typescript": { + "description": "Generate TypeScript files", + "env": "SHOPIFY_HYDROGEN_FLAG_TYPESCRIPT", + "name": "typescript", "allowNo": false, - "char": "d", - "description": "Delete your development theme.", - "env": "SHOPIFY_FLAG_DEVELOPMENT", - "name": "development", "type": "boolean" }, - "environment": { - "char": "e", - "description": "The environment to apply to the current command.", - "env": "SHOPIFY_FLAG_ENVIRONMENT", + "locale-param": { + "description": "The param name in Remix routes for the i18n locale, if any. Example: `locale` becomes ($locale).", + "env": "SHOPIFY_HYDROGEN_FLAG_ADAPTER", + "name": "locale-param", "hasDynamicHelp": false, - "multiple": true, - "name": "environment", + "multiple": false, "type": "option" }, "force": { - "allowNo": false, "char": "f", - "description": "Skip confirmation.", - "env": "SHOPIFY_FLAG_FORCE", + "description": "Overwrites the destination directory and files if they already exist.", + "env": "SHOPIFY_HYDROGEN_FLAG_FORCE", "name": "force", - "type": "boolean" - }, - "no-color": { "allowNo": false, - "description": "Disable color output.", - "env": "SHOPIFY_FLAG_NO_COLOR", - "hidden": false, - "name": "no-color", "type": "boolean" }, - "password": { - "description": "Password generated from the Theme Access app or an Admin API token.", - "env": "SHOPIFY_CLI_THEME_TOKEN", + "path": { + "description": "The path to the directory of the Hydrogen storefront. Defaults to the current directory where the command is run.", + "env": "SHOPIFY_HYDROGEN_FLAG_PATH", + "name": "path", "hasDynamicHelp": false, "multiple": false, - "name": "password", "type": "option" - }, + } + }, + "hasDynamicHelp": false, + "hiddenAliases": [], + "id": "hydrogen:generate:routes", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true, + "enableJsonFlag": false, + "customPluginName": "@shopify/cli-hydrogen" + }, + "hydrogen:setup:css": { + "aliases": [], + "args": { + "strategy": { + "description": "The CSS strategy to setup. One of tailwind,vanilla-extract,css-modules,postcss", + "name": "strategy", + "options": [ + "tailwind", + "vanilla-extract", + "css-modules", + "postcss" + ] + } + }, + "description": "Setup CSS strategies for your project.", + "flags": { "path": { - "description": "The path where you want to run the command. Defaults to the current working directory.", - "env": "SHOPIFY_FLAG_PATH", + "description": "The path to the directory of the Hydrogen storefront. Defaults to the current directory where the command is run.", + "env": "SHOPIFY_HYDROGEN_FLAG_PATH", + "name": "path", "hasDynamicHelp": false, "multiple": false, - "name": "path", - "noCacheDefault": true, "type": "option" }, - "show-all": { + "force": { + "char": "f", + "description": "Overwrites the destination directory and files if they already exist.", + "env": "SHOPIFY_HYDROGEN_FLAG_FORCE", + "name": "force", "allowNo": false, - "char": "a", - "description": "Include others development themes in theme list.", - "env": "SHOPIFY_FLAG_SHOW_ALL", - "name": "show-all", "type": "boolean" }, - "store": { - "char": "s", - "description": "Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com, https://example.myshopify.com).", - "env": "SHOPIFY_FLAG_STORE", + "install-deps": { + "description": "Auto installs dependencies using the active package manager.", + "env": "SHOPIFY_HYDROGEN_FLAG_INSTALL_DEPS", + "name": "install-deps", + "allowNo": true, + "type": "boolean" + } + }, + "hasDynamicHelp": false, + "hiddenAliases": [], + "id": "hydrogen:setup:css", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true, + "enableJsonFlag": false, + "descriptionWithMarkdown": "Adds support for certain CSS strategies to your project.", + "customPluginName": "@shopify/cli-hydrogen" + }, + "hydrogen:setup:markets": { + "aliases": [], + "args": { + "strategy": { + "description": "The URL structure strategy to setup multiple markets. One of subfolders,domains,subdomains", + "name": "strategy", + "options": [ + "subfolders", + "domains", + "subdomains" + ] + } + }, + "description": "Setup support for multiple markets in your project.", + "flags": { + "path": { + "description": "The path to the directory of the Hydrogen storefront. Defaults to the current directory where the command is run.", + "env": "SHOPIFY_HYDROGEN_FLAG_PATH", + "name": "path", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + } + }, + "hasDynamicHelp": false, + "hiddenAliases": [], + "id": "hydrogen:setup:markets", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true, + "enableJsonFlag": false, + "descriptionWithMarkdown": "Adds support for multiple [markets](https://shopify.dev/docs/custom-storefronts/hydrogen/markets) to your project by using the URL structure.", + "customPluginName": "@shopify/cli-hydrogen" + }, + "hydrogen:setup:vite": { + "aliases": [], + "args": {}, + "description": "EXPERIMENTAL: Upgrades the project to use Vite.", + "flags": { + "path": { + "description": "The path to the directory of the Hydrogen storefront. Defaults to the current directory where the command is run.", + "env": "SHOPIFY_HYDROGEN_FLAG_PATH", + "name": "path", "hasDynamicHelp": false, "multiple": false, - "name": "store", "type": "option" - }, - "theme": { - "char": "t", - "description": "Theme ID or name of the remote theme.", - "env": "SHOPIFY_FLAG_THEME_ID", - "hasDynamicHelp": false, - "multiple": true, - "name": "theme", - "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": "theme:delete", - "multiEnvironmentsFlags": [ - "store", - "password", - [ - "development", - "theme" - ] - ], + "hiddenAliases": [], + "id": "hydrogen:setup:vite", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", "strict": true, - "summary": "Delete remote themes from the connected store. This command can't be undone." + "enableJsonFlag": false, + "customPluginName": "@shopify/cli-hydrogen" }, - "theme:dev": { - "aliases": [ - ], + "flow:env:search": { + "aliases": [], "args": { + "root-type": { + "description": "The Admin API type name to search within (e.g. 'Order', 'Product', 'Customer').", + "name": "root-type", + "required": true + }, + "search-term": { + "description": "The search term to look for (e.g. 'email', 'customer name of order').", + "name": "search-term", + "required": true + } }, - "customPluginName": "@shopify/theme", - "description": "\n Uploads the current theme as the specified theme, or a \"development theme\" (https://shopify.dev/docs/themes/tools/cli#development-themes), to a store so you can preview it.\n\nThis command returns the following information:\n\n- A link to your development theme at http://127.0.0.1:9292. This URL can hot reload local changes to CSS and sections, or refresh the entire page when a file changes, enabling you to preview changes in real time using the store's data.\n\n You can specify a different network interface and port using `--host` and `--port`.\n\n- A link to the \"editor\" (https://shopify.dev/docs/themes/tools/online-editor) for the theme in the Shopify admin.\n\n- A \"preview link\" (https://help.shopify.com/manual/online-store/themes/adding-themes#share-a-theme-preview-with-others) that you can share with other developers.\n\nIf you already have a development theme for your current environment, then this command replaces the development theme with your local theme. You can override this using the `--theme-editor-sync` flag.\n\n> Note: You can't preview checkout customizations using http://127.0.0.1:9292.\n\nDevelopment themes are deleted when you run `shopify auth logout`. If you need a preview link that can be used after you log out, then you should \"share\" (https://shopify.dev/docs/api/shopify-cli/theme/theme-share) your theme or \"push\" (https://shopify.dev/docs/api/shopify-cli/theme/theme-push) to an unpublished theme on your store.\n\nYou can run this command only in a directory that matches the \"default Shopify theme folder structure\" (https://shopify.dev/docs/themes/tools/cli#directory-structure).", - "descriptionWithMarkdown": "\n Uploads the current theme as the specified theme, or a [development theme](https://shopify.dev/docs/themes/tools/cli#development-themes), to a store so you can preview it.\n\nThis command returns the following information:\n\n- A link to your development theme at http://127.0.0.1:9292. This URL can hot reload local changes to CSS and sections, or refresh the entire page when a file changes, enabling you to preview changes in real time using the store's data.\n\n You can specify a different network interface and port using `--host` and `--port`.\n\n- A link to the [editor](https://shopify.dev/docs/themes/tools/online-editor) for the theme in the Shopify admin.\n\n- A [preview link](https://help.shopify.com/manual/online-store/themes/adding-themes#share-a-theme-preview-with-others) that you can share with other developers.\n\nIf you already have a development theme for your current environment, then this command replaces the development theme with your local theme. You can override this using the `--theme-editor-sync` flag.\n\n> Note: You can't preview checkout customizations using http://127.0.0.1:9292.\n\nDevelopment themes are deleted when you run `shopify auth logout`. If you need a preview link that can be used after you log out, then you should [share](https://shopify.dev/docs/api/shopify-cli/theme/theme-share) your theme or [push](https://shopify.dev/docs/api/shopify-cli/theme/theme-push) to an unpublished theme on your store.\n\nYou can run this command only in a directory that matches the [default Shopify theme folder structure](https://shopify.dev/docs/themes/tools/cli#directory-structure).", + "description": "Searches the Flow environment for field paths under a given Admin API root type. Use this before writing conditions, Liquid, or task config that reference `{{ order.customer.email }}`-style paths.\n\nPass a single root_type and search term as positional args. Use `--workflow-id` to pin the search to a specific workflow's Admin API version.", + "examples": [ + "<%= config.bin %> <%= command.id %> Order \"customer email\"", + "<%= config.bin %> <%= command.id %> Product tags --data-type LIST" + ], "flags": { - "allow-live": { + "no-color": { + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", "allowNo": false, - "char": "a", - "description": "Allow development on a live theme.", - "env": "SHOPIFY_FLAG_ALLOW_LIVE", - "name": "allow-live", "type": "boolean" }, - "environment": { - "char": "e", - "description": "The environment to apply to the current command.", - "env": "SHOPIFY_FLAG_ENVIRONMENT", - "hasDynamicHelp": false, - "multiple": true, - "name": "environment", - "type": "option" - }, - "error-overlay": { - "default": "default", - "description": "Controls the visibility of the error overlay when an theme asset upload fails:\n- silent Prevents the error overlay from appearing.\n- default Displays the error overlay.\n ", - "env": "SHOPIFY_FLAG_ERROR_OVERLAY", - "hasDynamicHelp": false, - "multiple": false, - "name": "error-overlay", - "options": [ - "silent", - "default" - ], - "type": "option" + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", + "allowNo": false, + "type": "boolean" }, - "force": { + "json": { + "char": "j", + "description": "Output the result as JSON. Automatically disables color output.", + "env": "SHOPIFY_FLAG_JSON", + "hidden": false, + "name": "json", "allowNo": false, - "char": "f", - "description": "Proceed without confirmation, if current directory does not seem to be theme directory.", - "env": "SHOPIFY_FLAG_FORCE", - "hidden": true, - "name": "force", "type": "boolean" }, - "host": { - "description": "Set which network interface the web server listens on. The default value is 127.0.0.1.", - "env": "SHOPIFY_FLAG_HOST", + "store": { + "char": "s", + "description": "The myshopify.com domain. Falls back to flow.toml.", + "env": "SHOPIFY_FLAG_STORE", + "name": "store", "hasDynamicHelp": false, "multiple": false, - "name": "host", "type": "option" }, - "ignore": { - "char": "x", - "description": "Skip hot reloading any files that match the specified pattern.", - "env": "SHOPIFY_FLAG_IGNORE", + "data-type": { + "description": "Filter by data type. 'LIST' for array paths only, 'ALL' for all scalar-ending types.", + "name": "data-type", + "default": "ALL", "hasDynamicHelp": false, - "multiple": true, - "name": "ignore", + "multiple": false, + "options": [ + "LIST", + "ALL" + ], "type": "option" }, - "listing": { - "description": "The listing preset to use for multi-preset themes. Applies preset files from listings/[preset-name] directory.", - "env": "SHOPIFY_FLAG_LISTING", + "workflow-id": { + "description": "Pin the search to a workflow's Admin API version.", + "env": "SHOPIFY_FLAG_FLOW_WORKFLOW_ID", + "name": "workflow-id", "hasDynamicHelp": false, "multiple": false, - "name": "listing", "type": "option" }, - "live-reload": { - "default": "hot-reload", - "description": "The live reload mode switches the server behavior when a file is modified:\n- hot-reload Hot reloads local changes to CSS and sections (default)\n- full-page Always refreshes the entire page\n- off Deactivate live reload", - "env": "SHOPIFY_FLAG_LIVE_RELOAD", + "workflow-version": { + "description": "Workflow definition version (paired with --workflow-id).", + "env": "SHOPIFY_FLAG_FLOW_WORKFLOW_VERSION", + "name": "workflow-version", "hasDynamicHelp": false, "multiple": false, - "name": "live-reload", - "options": [ - "hot-reload", - "full-page", - "off" - ], "type": "option" - }, + } + }, + "hasDynamicHelp": false, + "hiddenAliases": [], + "id": "flow:env:search", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true, + "summary": "Discover Flow environment field paths.", + "descriptionWithMarkdown": "Searches the Flow environment for field paths under a given Admin API root type. Use this before writing conditions, Liquid, or task config that reference `{{ order.customer.email }}`-style paths.\n\nPass a single root_type and search term as positional args. Use `--workflow-id` to pin the search to a specific workflow's Admin API version.", + "customPluginName": "@shopify/store" + }, + "flow:init": { + "aliases": [], + "args": { + "path": { + "description": "Project directory to create (or reuse). Defaults to the current directory.", + "name": "path", + "required": false + } + }, + "description": "Writes a `flow.toml` with the store handle and workflow directory. Pass a path to create (or use) a project directory; defaults to the current directory.\n\nLifecycle commands (`workflow validate/push/pull/diff/activate/deactivate/status`) read this file when their --store and --workflows-dir flags aren't passed. Refuses to overwrite an existing file unless --force is set.", + "examples": [ + "<%= config.bin %> <%= command.id %> --store shop.myshopify.com", + "<%= config.bin %> <%= command.id %> ./shop1 --store shop.myshopify.com", + "<%= config.bin %> <%= command.id %> ./shop1 --store shop.myshopify.com --workflows-dir flows --force" + ], + "flags": { "no-color": { - "allowNo": false, "description": "Disable color output.", "env": "SHOPIFY_FLAG_NO_COLOR", "hidden": false, "name": "no-color", + "allowNo": false, "type": "boolean" }, - "nodelete": { + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", "allowNo": false, - "char": "n", - "description": "Prevents files from being deleted in the remote theme when a file has been deleted locally. This applies to files that are deleted while the command is running, and files that have been deleted locally before the command is run.", - "env": "SHOPIFY_FLAG_NODELETE", - "name": "nodelete", "type": "boolean" }, - "notify": { - "description": "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", + "store": { + "char": "s", + "description": "The myshopify.com domain of the store this project tracks.", + "env": "SHOPIFY_FLAG_STORE", + "name": "store", + "required": true, "hasDynamicHelp": false, "multiple": false, - "name": "notify", "type": "option" }, - "only": { - "char": "o", - "description": "Hot reload only files that match the specified pattern.", - "env": "SHOPIFY_FLAG_ONLY", + "workflows-dir": { + "description": "Directory (relative to flow.toml) where workflow JSON files live. Defaults to `workflows`.", + "env": "SHOPIFY_FLAG_FLOW_WORKFLOWS_DIR", + "name": "workflows-dir", "hasDynamicHelp": false, - "multiple": true, - "name": "only", + "multiple": false, "type": "option" }, - "open": { + "force": { + "description": "Overwrite an existing flow.toml.", + "name": "force", "allowNo": false, - "description": "Automatically launch the theme preview in your default web browser.", - "env": "SHOPIFY_FLAG_OPEN", - "name": "open", "type": "boolean" + } + }, + "hasDynamicHelp": false, + "hiddenAliases": [], + "id": "flow:init", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true, + "summary": "Initialize a Flow IaC project (creates the directory and writes flow.toml).", + "descriptionWithMarkdown": "Writes a `flow.toml` with the store handle and workflow directory. Pass a path to create (or use) a project directory; defaults to the current directory.\n\nLifecycle commands (`workflow validate/push/pull/diff/activate/deactivate/status`) read this file when their --store and --workflows-dir flags aren't passed. Refuses to overwrite an existing file unless --force is set.", + "customPluginName": "@shopify/store" + }, + "flow:resource:search": { + "aliases": [], + "args": { + "type": { + "description": "Resource type to search.", + "name": "type", + "options": [ + "PRODUCT", + "PRODUCT_VARIANT", + "CUSTOMER", + "SEGMENT", + "COLLECTION", + "ORDER", + "DRAFT_ORDER", + "APP_INSTALLATION", + "ARTICLE", + "BLOG", + "PAGE", + "DISCOUNT", + "LOCATION", + "MARKETING_AUTOMATION", + "METAOBJECT_DEFINITION", + "METAOBJECT", + "COMPANY", + "COMPANY_LOCATION", + "PRODUCT_NETWORK_ORDER" + ], + "required": true }, - "password": { - "description": "Password generated from the Theme Access app or an Admin API token.", - "env": "SHOPIFY_CLI_THEME_TOKEN", - "hasDynamicHelp": false, - "multiple": false, - "name": "password", - "type": "option" - }, - "path": { - "description": "The path where you want to run the command. Defaults to the current working directory.", - "env": "SHOPIFY_FLAG_PATH", - "hasDynamicHelp": false, - "multiple": false, - "name": "path", - "noCacheDefault": true, - "type": "option" - }, - "poll": { + "query": { + "description": "Query string. Wildcards (* or ?) are not supported. Empty string returns up to --limit.", + "name": "query", + "required": true + } + }, + "description": "Searches the Admin API for a specific resource type. Returns title, GID, and path. `PRODUCT_VARIANT` also returns inventory_item_id; `LOCATION` also returns inventory_group_id and inventory_group_name.\n\nRoutes through Sidekick — uses the `shop.admin.graphql` Identity scope, not Flow's own scope.", + "examples": [ + "<%= config.bin %> <%= command.id %> PRODUCT shoes", + "<%= config.bin %> <%= command.id %> CUSTOMER \"\" --limit 5" + ], + "flags": { + "no-color": { + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", "allowNo": false, - "description": "Force polling to detect file changes.", - "env": "SHOPIFY_FLAG_POLL", - "hidden": true, - "name": "poll", "type": "boolean" }, - "port": { - "description": "Local port to serve theme preview from. Must be between 1 and 65535.", - "env": "SHOPIFY_FLAG_PORT", - "hasDynamicHelp": false, - "multiple": false, - "name": "port", - "type": "option" + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", + "allowNo": false, + "type": "boolean" }, - "standard-events-inspector": { + "json": { + "char": "j", + "description": "Output the result as JSON. Automatically disables color output.", + "env": "SHOPIFY_FLAG_JSON", + "hidden": false, + "name": "json", "allowNo": false, - "description": "Inject the standard events inspector into storefront HTML.", - "env": "SHOPIFY_FLAG_STANDARD_EVENTS_INSPECTOR", - "name": "standard-events-inspector", "type": "boolean" }, "store": { "char": "s", - "description": "Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com, https://example.myshopify.com).", + "description": "The myshopify.com domain. Falls back to flow.toml.", "env": "SHOPIFY_FLAG_STORE", - "hasDynamicHelp": false, - "multiple": false, "name": "store", - "type": "option" - }, - "store-password": { - "description": "The password for storefronts with password protection.", - "env": "SHOPIFY_FLAG_STORE_PASSWORD", - "hasDynamicHelp": false, - "multiple": false, - "name": "store-password", - "type": "option" - }, - "theme": { - "char": "t", - "description": "Theme ID or name of the remote theme.", - "env": "SHOPIFY_FLAG_THEME_ID", "hasDynamicHelp": false, "multiple": false, - "name": "theme", - "type": "option" - }, - "theme-editor-sync": { - "allowNo": false, - "description": "Synchronize Theme Editor updates in the local theme files.", - "env": "SHOPIFY_FLAG_THEME_EDITOR_SYNC", - "name": "theme-editor-sync", - "type": "boolean" + "type": "option" }, - "verbose": { - "allowNo": false, - "description": "Increase the verbosity of the output.", - "env": "SHOPIFY_FLAG_VERBOSE", - "hidden": false, - "name": "verbose", - "type": "boolean" + "limit": { + "description": "Number of resources to return (default 10, max 50).", + "name": "limit", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "theme:dev", - "multiEnvironmentsFlags": null, + "hiddenAliases": [], + "id": "flow:resource:search", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", "strict": true, - "summary": "Uploads the current theme as a development theme to the connected store, then prints theme editor and preview URLs to your terminal. While running, changes will push to the store in real time." + "summary": "Search for a Shopify resource by query (Sidekick-routed).", + "descriptionWithMarkdown": "Searches the Admin API for a specific resource type. Returns title, GID, and path. `PRODUCT_VARIANT` also returns inventory_item_id; `LOCATION` also returns inventory_group_id and inventory_group_name.\n\nRoutes through Sidekick — uses the `shop.admin.graphql` Identity scope, not Flow's own scope.", + "customPluginName": "@shopify/store" }, - "theme:duplicate": { - "aliases": [ - ], + "flow:shopifyql:columns": { + "aliases": [], "args": { + "query": { + "description": "A ShopifyQL query string.", + "name": "query", + "required": true + } }, - "customPluginName": "@shopify/theme", - "description": "If you want to duplicate your local theme, you need to run `shopify theme push` first.\n\nIf no theme ID is specified, you're prompted to select the theme that you want to duplicate from the list of themes in your store. You're asked to confirm that you want to duplicate the specified theme.\n\nPrompts and confirmations are not shown when duplicate is run in a CI environment or the `--force` flag is used, therefore you must specify a theme ID using the `--theme` flag.\n\nYou can optionally name the duplicated theme using the `--name` flag.\n\nIf you use the `--json` flag, then theme information is returned in JSON format, which can be used as a machine-readable input for scripts or continuous integration.\n\nSample JSON output:\n\n```json\n{\n \"theme\": {\n \"id\": 108267175958,\n \"name\": \"A Duplicated Theme\",\n \"role\": \"unpublished\",\n \"shop\": \"mystore.myshopify.com\"\n }\n}\n```\n\n```json\n{\n \"message\": \"The theme 'Summer Edition' could not be duplicated due to errors\",\n \"errors\": [\"Maximum number of themes reached\"],\n \"requestId\": \"12345-abcde-67890\"\n}\n```", - "descriptionWithMarkdown": "If you want to duplicate your local theme, you need to run `shopify theme push` first.\n\nIf no theme ID is specified, you're prompted to select the theme that you want to duplicate from the list of themes in your store. You're asked to confirm that you want to duplicate the specified theme.\n\nPrompts and confirmations are not shown when duplicate is run in a CI environment or the `--force` flag is used, therefore you must specify a theme ID using the `--theme` flag.\n\nYou can optionally name the duplicated theme using the `--name` flag.\n\nIf you use the `--json` flag, then theme information is returned in JSON format, which can be used as a machine-readable input for scripts or continuous integration.\n\nSample JSON output:\n\n```json\n{\n \"theme\": {\n \"id\": 108267175958,\n \"name\": \"A Duplicated Theme\",\n \"role\": \"unpublished\",\n \"shop\": \"mystore.myshopify.com\"\n }\n}\n```\n\n```json\n{\n \"message\": \"The theme 'Summer Edition' could not be duplicated due to errors\",\n \"errors\": [\"Maximum number of themes reached\"],\n \"requestId\": \"12345-abcde-67890\"\n}\n```", + "description": "Given a ShopifyQL query string, returns the column names + types the query will produce. These are the fields Flow would add to the environment when the query runs.", + "examples": [ + "<%= config.bin %> <%= command.id %> \"FROM sales SHOW gross_sales BY product_title SINCE -7d\"" + ], "flags": { - "environment": { - "char": "e", - "description": "The environment to apply to the current command.", - "env": "SHOPIFY_FLAG_ENVIRONMENT", - "hasDynamicHelp": false, - "multiple": true, - "name": "environment", - "type": "option" + "no-color": { + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", + "allowNo": false, + "type": "boolean" }, - "force": { + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", "allowNo": false, - "char": "f", - "description": "Force the duplicate operation to run without prompts or confirmations.", - "env": "SHOPIFY_FLAG_FORCE", - "name": "force", "type": "boolean" }, "json": { - "allowNo": false, "char": "j", "description": "Output the result as JSON. Automatically disables color output.", "env": "SHOPIFY_FLAG_JSON", "hidden": false, "name": "json", + "allowNo": false, "type": "boolean" }, - "name": { - "char": "n", - "description": "Name of the newly duplicated theme.", - "env": "SHOPIFY_FLAG_NAME", + "store": { + "char": "s", + "description": "The myshopify.com domain. Falls back to flow.toml.", + "env": "SHOPIFY_FLAG_STORE", + "name": "store", "hasDynamicHelp": false, "multiple": false, - "name": "name", "type": "option" - }, + } + }, + "hasDynamicHelp": false, + "hiddenAliases": [], + "id": "flow:shopifyql:columns", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true, + "summary": "Resolve the columns a ShopifyQL query produces.", + "descriptionWithMarkdown": "Given a ShopifyQL query string, returns the column names + types the query will produce. These are the fields Flow would add to the environment when the query runs.", + "customPluginName": "@shopify/store" + }, + "flow:task:describe": { + "aliases": [], + "args": { + "task": { + "description": "Task reference as `@` (one or more).", + "name": "task", + "required": true + } + }, + "description": "Returns the full configuration schema and return-field definitions for one or more Flow tasks.\n\nEach task is identified as `@` (e.g. `shopify::admin::order_created@0.1`).", + "examples": [ + "<%= config.bin %> <%= command.id %> shopify::admin::order_created@0.1", + "<%= config.bin %> <%= command.id %> shopify::admin::order_created@0.1 shopify::admin::add_order_tags@0.1" + ], + "flags": { "no-color": { - "allowNo": false, "description": "Disable color output.", "env": "SHOPIFY_FLAG_NO_COLOR", "hidden": false, "name": "no-color", + "allowNo": false, "type": "boolean" }, - "password": { - "description": "Password generated from the Theme Access app or an Admin API token.", - "env": "SHOPIFY_CLI_THEME_TOKEN", - "hasDynamicHelp": false, - "multiple": false, - "name": "password", - "type": "option" + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", + "allowNo": false, + "type": "boolean" + }, + "json": { + "char": "j", + "description": "Output the result as JSON. Automatically disables color output.", + "env": "SHOPIFY_FLAG_JSON", + "hidden": false, + "name": "json", + "allowNo": false, + "type": "boolean" }, "store": { "char": "s", - "description": "Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com, https://example.myshopify.com).", + "description": "The myshopify.com domain. Falls back to flow.toml.", "env": "SHOPIFY_FLAG_STORE", - "hasDynamicHelp": false, - "multiple": false, "name": "store", - "type": "option" - }, - "theme": { - "char": "t", - "description": "Theme ID or name of the remote theme.", - "env": "SHOPIFY_FLAG_THEME_ID", "hasDynamicHelp": false, "multiple": false, - "name": "theme", "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": "theme:duplicate", + "hiddenAliases": [], + "id": "flow:task:describe", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", - "strict": true, - "summary": "Duplicates a theme from your theme library.", - "usage": [ - "theme duplicate", - "theme duplicate --theme 10 --name 'New Theme'" - ] + "strict": false, + "summary": "Describe one or more Flow tasks: configuration and return schemas.", + "descriptionWithMarkdown": "Returns the full configuration schema and return-field definitions for one or more Flow tasks.\n\nEach task is identified as `@` (e.g. `shopify::admin::order_created@0.1`).", + "customPluginName": "@shopify/store" }, - "theme:info": { - "aliases": [ - ], + "flow:task:search": { + "aliases": [], "args": { + "query": { + "description": "Search query (one or more, space-separated).", + "name": "query", + "required": true + } }, - "customPluginName": "@shopify/theme", - "description": "Displays information about your theme environment, including your current store. Can also retrieve information about a specific theme.", + "description": "Finds Flow tasks (triggers, conditions, actions, foreach, wait) by natural-language query. Pass each goal as a separate positional argument.\n\nReturns task references (id + version) you can pass to `flow task describe` to get the full configuration schema.", + "examples": [ + "<%= config.bin %> <%= command.id %> \"order created trigger\"", + "<%= config.bin %> <%= command.id %> \"add order tags\" \"send email\"", + "<%= config.bin %> <%= command.id %> \"tagging\" --type action" + ], "flags": { - "development": { + "no-color": { + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", "allowNo": false, - "char": "d", - "description": "Retrieve info from your development theme.", - "env": "SHOPIFY_FLAG_DEVELOPMENT", - "name": "development", "type": "boolean" }, - "environment": { - "char": "e", - "description": "The environment to apply to the current command.", - "env": "SHOPIFY_FLAG_ENVIRONMENT", - "hasDynamicHelp": false, - "multiple": true, - "name": "environment", - "type": "option" + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", + "allowNo": false, + "type": "boolean" }, "json": { - "allowNo": false, "char": "j", "description": "Output the result as JSON. Automatically disables color output.", "env": "SHOPIFY_FLAG_JSON", "hidden": false, "name": "json", - "type": "boolean" - }, - "no-color": { "allowNo": false, - "description": "Disable color output.", - "env": "SHOPIFY_FLAG_NO_COLOR", - "hidden": false, - "name": "no-color", "type": "boolean" }, - "password": { - "description": "Password generated from the Theme Access app or an Admin API token.", - "env": "SHOPIFY_CLI_THEME_TOKEN", - "hasDynamicHelp": false, - "multiple": false, - "name": "password", - "type": "option" - }, - "path": { - "description": "The path where you want to run the command. Defaults to the current working directory.", - "env": "SHOPIFY_FLAG_PATH", - "hasDynamicHelp": false, - "multiple": false, - "name": "path", - "noCacheDefault": true, - "type": "option" - }, "store": { "char": "s", - "description": "Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com, https://example.myshopify.com).", + "description": "The myshopify.com domain. Falls back to flow.toml.", "env": "SHOPIFY_FLAG_STORE", + "name": "store", "hasDynamicHelp": false, "multiple": false, - "name": "store", "type": "option" }, - "theme": { - "char": "t", - "description": "Theme ID or name of the remote theme.", - "env": "SHOPIFY_FLAG_THEME_ID", + "type": { + "description": "Filter by task type.", + "name": "type", "hasDynamicHelp": false, "multiple": false, - "name": "theme", + "options": [ + "trigger", + "action", + "condition", + "foreach", + "wait" + ], "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": "theme:info", - "multiEnvironmentsFlags": [ - "store", - "password" - ], + "hiddenAliases": [], + "id": "flow:task:search", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", - "strict": true + "strict": false, + "summary": "Search Flow tasks (triggers, conditions, actions, etc.).", + "descriptionWithMarkdown": "Finds Flow tasks (triggers, conditions, actions, foreach, wait) by natural-language query. Pass each goal as a separate positional argument.\n\nReturns task references (id + version) you can pass to `flow task describe` to get the full configuration schema.", + "customPluginName": "@shopify/store" }, - "theme:init": { - "aliases": [ - ], + "flow:template:save": { + "aliases": [], "args": { - "name": { - "description": "Name of the new theme", - "name": "name", - "required": false + "template-id": { + "description": "Template identifier from a prior `flow template search` result.", + "name": "template-id", + "required": true } }, - "customPluginName": "@shopify/theme", - "description": "Clones a Git repository to your local machine to use as the starting point for building a theme.\n\n If no Git repository is specified, then this command creates a copy of Shopify's \"Skeleton theme\" (https://github.com/Shopify/skeleton-theme.git), with the specified name in the current folder. If no name is provided, then you're prompted to enter one.\n\n > Caution: If you're building a theme for the Shopify Theme Store, then you can use our example theme as a starting point. However, the theme that you submit needs to be \"substantively different from existing themes\" (https://shopify.dev/docs/themes/store/requirements#uniqueness) so that it provides added value for users.\n ", - "descriptionWithMarkdown": "Clones a Git repository to your local machine to use as the starting point for building a theme.\n\n If no Git repository is specified, then this command creates a copy of Shopify's [Skeleton theme](https://github.com/Shopify/skeleton-theme.git), with the specified name in the current folder. If no name is provided, then you're prompted to enter one.\n\n > Caution: If you're building a theme for the Shopify Theme Store, then you can use our example theme as a starting point. However, the theme that you submit needs to be [substantively different from existing themes](https://shopify.dev/docs/themes/store/requirements#uniqueness) so that it provides added value for users.\n ", + "description": "Fetches the template by ID (always the leading version) and writes its `workflow_json` to `//workflow.flow.json`. Sets `root.workflow_name` to the template's title — rename it before push if you want a different name.\n\nNo lockfile is written; the file is \"new\" until you `push` it.\n\nGet a template ID from `shopify flow template search` — each result carries a stable `template_id` field.", + "examples": [ + "<%= config.bin %> <%= command.id %> 01HQK000000000000000000000 --as fraud-prevention", + "<%= config.bin %> <%= command.id %> 01HQK... --as high-value-orders --force" + ], "flags": { - "clone-url": { - "char": "u", - "default": "https://github.com/Shopify/skeleton-theme.git", - "description": "The Git URL to clone from. Defaults to Shopify's Skeleton theme.", - "env": "SHOPIFY_FLAG_CLONE_URL", - "hasDynamicHelp": false, - "multiple": false, - "name": "clone-url", - "type": "option" - }, - "latest": { - "allowNo": false, - "char": "l", - "description": "Downloads the latest release of the `clone-url`", - "env": "SHOPIFY_FLAG_LATEST", - "name": "latest", - "type": "boolean" - }, "no-color": { - "allowNo": false, "description": "Disable color output.", "env": "SHOPIFY_FLAG_NO_COLOR", "hidden": false, "name": "no-color", + "allowNo": false, + "type": "boolean" + }, + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", + "allowNo": false, "type": "boolean" }, - "path": { - "description": "The path where you want to run the command. Defaults to the current working directory.", - "env": "SHOPIFY_FLAG_PATH", + "store": { + "char": "s", + "description": "The myshopify.com domain. Falls back to flow.toml.", + "env": "SHOPIFY_FLAG_STORE", + "name": "store", "hasDynamicHelp": false, "multiple": false, - "name": "path", - "noCacheDefault": true, "type": "option" }, - "verbose": { + "as": { + "char": "a", + "description": "Slug for the new workflow folder. Creates workflows//workflow.flow.json.", + "name": "as", + "required": true, + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "workflows-dir": { + "description": "Workflows directory (relative to cwd). Falls back to flow.toml.", + "env": "SHOPIFY_FLAG_FLOW_WORKFLOWS_DIR", + "name": "workflows-dir", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "force": { + "description": "Overwrite an existing workflow.flow.json at the target path.", + "name": "force", "allowNo": false, - "description": "Increase the verbosity of the output.", - "env": "SHOPIFY_FLAG_VERBOSE", - "hidden": false, - "name": "verbose", "type": "boolean" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "theme:init", - "multiEnvironmentsFlags": null, + "hiddenAliases": [], + "id": "flow:template:save", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", "strict": true, - "summary": "Clones a Git repository to use as a starting point for building a new theme.", - "usage": "theme init [name] [flags]" + "summary": "Save a Flow template into your IaC project as a new workflow.", + "descriptionWithMarkdown": "Fetches the template by ID (always the leading version) and writes its `workflow_json` to `//workflow.flow.json`. Sets `root.workflow_name` to the template's title — rename it before push if you want a different name.\n\nNo lockfile is written; the file is \"new\" until you `push` it.\n\nGet a template ID from `shopify flow template search` — each result carries a stable `template_id` field.", + "customPluginName": "@shopify/store" }, - "theme:language-server": { - "aliases": [ - ], + "flow:template:search": { + "aliases": [], "args": { + "query": { + "description": "Search query (one or more, space-separated).", + "name": "query", + "required": true + } }, - "customPluginName": "@shopify/theme", - "description": "Starts the \"Language Server\" (https://shopify.dev/docs/themes/tools/cli/language-server).", - "descriptionWithMarkdown": "Starts the [Language Server](https://shopify.dev/docs/themes/tools/cli/language-server).", + "description": "Searches Flow's template catalog for workflows matching one or more business goals. Pass each goal as a separate positional argument.\n\nThe result is a JSON object keyed by query, with templates and their workflow_json structures. Use a returned template's workflow_json as the starting point for a new workflow.", + "examples": [ + "<%= config.bin %> <%= command.id %> \"fraud prevention\"", + "<%= config.bin %> <%= command.id %> \"fraud prevention\" \"high risk orders\"" + ], "flags": { "no-color": { - "allowNo": false, "description": "Disable color output.", "env": "SHOPIFY_FLAG_NO_COLOR", "hidden": false, "name": "no-color", + "allowNo": false, "type": "boolean" }, "verbose": { - "allowNo": false, "description": "Increase the verbosity of the output.", "env": "SHOPIFY_FLAG_VERBOSE", "hidden": false, "name": "verbose", + "allowNo": false, + "type": "boolean" + }, + "json": { + "char": "j", + "description": "Output the result as JSON. Automatically disables color output.", + "env": "SHOPIFY_FLAG_JSON", + "hidden": false, + "name": "json", + "allowNo": false, "type": "boolean" + }, + "store": { + "char": "s", + "description": "The myshopify.com domain. Falls back to flow.toml.", + "env": "SHOPIFY_FLAG_STORE", + "name": "store", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "theme:language-server", - "multiEnvironmentsFlags": null, + "hiddenAliases": [], + "id": "flow:template:search", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", - "strict": true, - "summary": "Start a Language Server Protocol server." + "strict": false, + "summary": "Search Flow templates by business goal.", + "descriptionWithMarkdown": "Searches Flow's template catalog for workflows matching one or more business goals. Pass each goal as a separate positional argument.\n\nThe result is a JSON object keyed by query, with templates and their workflow_json structures. Use a returned template's workflow_json as the starting point for a new workflow.", + "customPluginName": "@shopify/store" }, - "theme:list": { - "aliases": [ - ], + "flow:type:show": { + "aliases": [], "args": { + "type": { + "description": "The GraphQL type name to inspect.", + "name": "type", + "required": true + } }, - "customPluginName": "@shopify/theme", - "description": "Lists the themes in your store, along with their IDs and statuses.", + "description": "Prints the field structure, argument signatures, and return types for a single Admin API GraphQL type. Use to understand the shape of a type before referencing its fields in workflow JSON.", + "examples": [ + "<%= config.bin %> <%= command.id %> Product", + "<%= config.bin %> <%= command.id %> Order --workflow-id 01HQK..." + ], "flags": { - "environment": { - "char": "e", - "description": "The environment to apply to the current command.", - "env": "SHOPIFY_FLAG_ENVIRONMENT", - "hasDynamicHelp": false, - "multiple": true, - "name": "environment", - "type": "option" + "no-color": { + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", + "allowNo": false, + "type": "boolean" }, - "id": { - "description": "Only list theme with the given ID.", - "env": "SHOPIFY_FLAG_ID", - "hasDynamicHelp": false, - "multiple": false, - "name": "id", - "type": "option" + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", + "allowNo": false, + "type": "boolean" }, "json": { - "allowNo": false, "char": "j", "description": "Output the result as JSON. Automatically disables color output.", "env": "SHOPIFY_FLAG_JSON", "hidden": false, "name": "json", + "allowNo": false, "type": "boolean" }, - "name": { - "description": "Only list themes that contain the given name.", - "env": "SHOPIFY_FLAG_NAME", + "store": { + "char": "s", + "description": "The myshopify.com domain. Falls back to flow.toml.", + "env": "SHOPIFY_FLAG_STORE", + "name": "store", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "workflow-id": { + "description": "Pin the lookup to a workflow's Admin API version.", + "env": "SHOPIFY_FLAG_FLOW_WORKFLOW_ID", + "name": "workflow-id", "hasDynamicHelp": false, "multiple": false, - "name": "name", "type": "option" }, + "workflow-version": { + "description": "Workflow definition version (paired with --workflow-id).", + "env": "SHOPIFY_FLAG_FLOW_WORKFLOW_VERSION", + "name": "workflow-version", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + } + }, + "hasDynamicHelp": false, + "hiddenAliases": [], + "id": "flow:type:show", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true, + "summary": "Show a GraphQL type's structure (fields, arguments, return types).", + "descriptionWithMarkdown": "Prints the field structure, argument signatures, and return types for a single Admin API GraphQL type. Use to understand the shape of a type before referencing its fields in workflow JSON.", + "customPluginName": "@shopify/store" + }, + "flow:workflow:activate": { + "aliases": [], + "args": { + "file": { + "description": "Optional path to the workflow JSON file (lockfile drives activation).", + "name": "file", + "required": false + } + }, + "description": "Activates a workflow definition. Three input modes:\n\n- positional file path: read lockfile sibling for workflow_id and workflow_definition_version\n- `--workflow-id X --workflow-version Y`: activate that exact pair\n- `--workflow-id X --use-latest`: resolve the current main version via lookup, then activate\n\nHidden workflows must be unhidden first (push with hidden:false).", + "examples": [ + "<%= config.bin %> <%= command.id %> ./workflow.flow.json --store shop.myshopify.com", + "<%= config.bin %> <%= command.id %> --workflow-id 01HQK... --workflow-version 01HQL... --store shop.myshopify.com", + "<%= config.bin %> <%= command.id %> --workflow-id 01HQK... --use-latest --store shop.myshopify.com" + ], + "flags": { "no-color": { - "allowNo": false, "description": "Disable color output.", "env": "SHOPIFY_FLAG_NO_COLOR", "hidden": false, "name": "no-color", + "allowNo": false, "type": "boolean" }, - "password": { - "description": "Password generated from the Theme Access app or an Admin API token.", - "env": "SHOPIFY_CLI_THEME_TOKEN", - "hasDynamicHelp": false, - "multiple": false, - "name": "password", - "type": "option" + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", + "allowNo": false, + "type": "boolean" }, - "path": { - "description": "The path where you want to run the command. Defaults to the current working directory.", - "env": "SHOPIFY_FLAG_PATH", + "json": { + "char": "j", + "description": "Output the result as JSON. Automatically disables color output.", + "env": "SHOPIFY_FLAG_JSON", + "hidden": false, + "name": "json", + "allowNo": false, + "type": "boolean" + }, + "store": { + "char": "s", + "description": "The myshopify.com domain. Falls back to the `store` field in flow.toml.", + "env": "SHOPIFY_FLAG_STORE", + "name": "store", "hasDynamicHelp": false, "multiple": false, - "name": "path", - "noCacheDefault": true, "type": "option" }, - "role": { - "description": "Only list themes with the given role.", - "env": "SHOPIFY_FLAG_ROLE", + "workflow-id": { + "description": "Workflow ID (escape hatch when no lockfile).", + "env": "SHOPIFY_FLAG_FLOW_WORKFLOW_ID", + "name": "workflow-id", "hasDynamicHelp": false, "multiple": false, - "name": "role", - "options": [ - "live", - "unpublished", - "development" - ], "type": "option" }, - "store": { - "char": "s", - "description": "Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com, https://example.myshopify.com).", - "env": "SHOPIFY_FLAG_STORE", + "workflow-version": { + "description": "Workflow definition version. Required with --workflow-id unless --use-latest.", + "env": "SHOPIFY_FLAG_FLOW_WORKFLOW_VERSION", + "name": "workflow-version", "hasDynamicHelp": false, "multiple": false, - "name": "store", "type": "option" }, - "verbose": { + "use-latest": { + "description": "Resolve the latest main version via workflow_lookup.", + "name": "use-latest", "allowNo": false, - "description": "Increase the verbosity of the output.", - "env": "SHOPIFY_FLAG_VERBOSE", - "hidden": false, - "name": "verbose", "type": "boolean" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "theme:list", - "multiEnvironmentsFlags": [ - "store", - "password" - ], + "hiddenAliases": [], + "id": "flow:workflow:activate", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", - "strict": true + "strict": true, + "summary": "Activate a workflow on a shop.", + "descriptionWithMarkdown": "Activates a workflow definition. Three input modes:\n\n- positional file path: read lockfile sibling for workflow_id and workflow_definition_version\n- `--workflow-id X --workflow-version Y`: activate that exact pair\n- `--workflow-id X --use-latest`: resolve the current main version via lookup, then activate\n\nHidden workflows must be unhidden first (push with hidden:false).", + "customPluginName": "@shopify/store" }, - "theme:metafields:pull": { - "aliases": [ - ], + "flow:workflow:deactivate": { + "aliases": [], "args": { + "file": { + "description": "Optional path to the workflow JSON file (lockfile drives deactivation).", + "name": "file", + "required": false + } }, - "customPluginName": "@shopify/theme", - "description": "Retrieves metafields from Shopify Admin.\n\nIf the metafields file already exists, it will be overwritten.", - "descriptionWithMarkdown": "Retrieves metafields from Shopify Admin.\n\nIf the metafields file already exists, it will be overwritten.", + "description": "Deactivates a workflow definition. Same input modes as `activate`: positional file (lockfile), `--workflow-id` + `--workflow-version`, or `--workflow-id` + `--use-latest`.", + "examples": [ + "<%= config.bin %> <%= command.id %> ./workflow.flow.json --store shop.myshopify.com", + "<%= config.bin %> <%= command.id %> --workflow-id 01HQK... --workflow-version 01HQL... --store shop.myshopify.com" + ], "flags": { - "environment": { - "char": "e", - "description": "The environment to apply to the current command.", - "env": "SHOPIFY_FLAG_ENVIRONMENT", - "hasDynamicHelp": false, - "multiple": true, - "name": "environment", - "type": "option" - }, - "force": { - "allowNo": false, - "char": "f", - "description": "Proceed without confirmation, if current directory does not seem to be theme directory.", - "env": "SHOPIFY_FLAG_FORCE", - "hidden": true, - "name": "force", - "type": "boolean" - }, "no-color": { - "allowNo": false, "description": "Disable color output.", "env": "SHOPIFY_FLAG_NO_COLOR", "hidden": false, "name": "no-color", + "allowNo": false, "type": "boolean" }, - "password": { - "description": "Password generated from the Theme Access app or an Admin API token.", - "env": "SHOPIFY_CLI_THEME_TOKEN", + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", + "allowNo": false, + "type": "boolean" + }, + "json": { + "char": "j", + "description": "Output the result as JSON. Automatically disables color output.", + "env": "SHOPIFY_FLAG_JSON", + "hidden": false, + "name": "json", + "allowNo": false, + "type": "boolean" + }, + "store": { + "char": "s", + "description": "The myshopify.com domain. Falls back to the `store` field in flow.toml.", + "env": "SHOPIFY_FLAG_STORE", + "name": "store", "hasDynamicHelp": false, "multiple": false, - "name": "password", "type": "option" }, - "path": { - "description": "The path where you want to run the command. Defaults to the current working directory.", - "env": "SHOPIFY_FLAG_PATH", + "workflow-id": { + "description": "Workflow ID (escape hatch when no lockfile).", + "env": "SHOPIFY_FLAG_FLOW_WORKFLOW_ID", + "name": "workflow-id", "hasDynamicHelp": false, "multiple": false, - "name": "path", - "noCacheDefault": true, "type": "option" }, - "store": { - "char": "s", - "description": "Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com, https://example.myshopify.com).", - "env": "SHOPIFY_FLAG_STORE", + "workflow-version": { + "description": "Workflow definition version. Required with --workflow-id unless --use-latest.", + "env": "SHOPIFY_FLAG_FLOW_WORKFLOW_VERSION", + "name": "workflow-version", "hasDynamicHelp": false, "multiple": false, - "name": "store", "type": "option" }, - "verbose": { + "use-latest": { + "description": "Resolve the latest main version via workflow_lookup.", + "name": "use-latest", "allowNo": false, - "description": "Increase the verbosity of the output.", - "env": "SHOPIFY_FLAG_VERBOSE", - "hidden": false, - "name": "verbose", "type": "boolean" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "theme:metafields:pull", - "multiEnvironmentsFlags": null, + "hiddenAliases": [], + "id": "flow:workflow:deactivate", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", "strict": true, - "summary": "Download metafields definitions from your shop into a local file." + "summary": "Deactivate a workflow on a shop.", + "descriptionWithMarkdown": "Deactivates a workflow definition. Same input modes as `activate`: positional file (lockfile), `--workflow-id` + `--workflow-version`, or `--workflow-id` + `--use-latest`.", + "customPluginName": "@shopify/store" }, - "theme:open": { - "aliases": [ - ], + "flow:workflow:diff": { + "aliases": [], "args": { + "file": { + "description": "Path to the workflow JSON file.", + "name": "file", + "required": true + } }, - "customPluginName": "@shopify/theme", - "description": "Returns links that let you preview the specified theme. The following links are returned:\n\n - A link to the \"editor\" (https://shopify.dev/docs/themes/tools/online-editor) for the theme in the Shopify admin.\n - A \"preview link\" (https://help.shopify.com/manual/online-store/themes/adding-themes#share-a-theme-preview-with-others) that you can share with other developers.\n\n If you don't specify a theme, then you're prompted to select the theme to open from the list of the themes in your store.", - "descriptionWithMarkdown": "Returns links that let you preview the specified theme. The following links are returned:\n\n - A link to the [editor](https://shopify.dev/docs/themes/tools/online-editor) for the theme in the Shopify admin.\n - A [preview link](https://help.shopify.com/manual/online-store/themes/adding-themes#share-a-theme-preview-with-others) that you can share with other developers.\n\n If you don't specify a theme, then you're prompted to select the theme to open from the list of the themes in your store.", + "description": "Compares a local Flow workflow file against the remote workflow on the shop. Both sides are normalized (sorted keys, stable indent) so the diff reflects real changes, not formatting noise.\n\nDefault direction is `--- remote` / `+++ local` so added lines = what `push` would change.\n\nExit code: 0 if no differences, 1 if differences found.\n\nTo inspect a remote workflow without a local file, use `shopify flow workflow show `.", + "examples": [ + "<%= config.bin %> <%= command.id %> ./workflow.flow.json --store shop.myshopify.com", + "<%= config.bin %> <%= command.id %> ./workflow.flow.json --json" + ], "flags": { - "development": { - "allowNo": false, - "char": "d", - "description": "Open your development theme.", - "env": "SHOPIFY_FLAG_DEVELOPMENT", - "name": "development", - "type": "boolean" - }, - "editor": { + "no-color": { + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", "allowNo": false, - "char": "E", - "description": "Open the theme editor for the specified theme in the browser.", - "env": "SHOPIFY_FLAG_EDITOR", - "name": "editor", "type": "boolean" }, - "environment": { - "char": "e", - "description": "The environment to apply to the current command.", - "env": "SHOPIFY_FLAG_ENVIRONMENT", - "hasDynamicHelp": false, - "multiple": true, - "name": "environment", - "type": "option" - }, - "live": { + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", "allowNo": false, - "char": "l", - "description": "Open your live (published) theme.", - "env": "SHOPIFY_FLAG_LIVE", - "name": "live", "type": "boolean" }, - "no-color": { - "allowNo": false, - "description": "Disable color output.", - "env": "SHOPIFY_FLAG_NO_COLOR", + "json": { + "char": "j", + "description": "Output the result as JSON. Automatically disables color output.", + "env": "SHOPIFY_FLAG_JSON", "hidden": false, - "name": "no-color", + "name": "json", + "allowNo": false, "type": "boolean" }, - "password": { - "description": "Password generated from the Theme Access app or an Admin API token.", - "env": "SHOPIFY_CLI_THEME_TOKEN", - "hasDynamicHelp": false, - "multiple": false, - "name": "password", - "type": "option" - }, - "path": { - "description": "The path where you want to run the command. Defaults to the current working directory.", - "env": "SHOPIFY_FLAG_PATH", + "store": { + "char": "s", + "description": "The myshopify.com domain. Falls back to the `store` field in flow.toml.", + "env": "SHOPIFY_FLAG_STORE", + "name": "store", "hasDynamicHelp": false, "multiple": false, - "name": "path", - "noCacheDefault": true, "type": "option" }, - "store": { - "char": "s", - "description": "Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com, https://example.myshopify.com).", - "env": "SHOPIFY_FLAG_STORE", + "workflow-id": { + "description": "Compare against a specific workflow ID instead of the lockfile.", + "env": "SHOPIFY_FLAG_FLOW_WORKFLOW_ID", + "name": "workflow-id", "hasDynamicHelp": false, "multiple": false, - "name": "store", "type": "option" }, - "theme": { - "char": "t", - "description": "Theme ID or name of the remote theme.", - "env": "SHOPIFY_FLAG_THEME_ID", + "workflow-version": { + "description": "Optional definition version override.", + "env": "SHOPIFY_FLAG_FLOW_WORKFLOW_VERSION", + "name": "workflow-version", "hasDynamicHelp": false, "multiple": false, - "name": "theme", "type": "option" }, - "verbose": { + "local-only": { + "description": "Print the normalized local workflow only (no diff).", + "name": "local-only", "allowNo": false, - "description": "Increase the verbosity of the output.", - "env": "SHOPIFY_FLAG_VERBOSE", - "hidden": false, - "name": "verbose", "type": "boolean" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "theme:open", - "multiEnvironmentsFlags": null, + "hiddenAliases": [], + "id": "flow:workflow:diff", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", "strict": true, - "summary": "Opens the preview of your remote theme." + "summary": "Show the diff between a local workflow file and the remote workflow.", + "descriptionWithMarkdown": "Compares a local Flow workflow file against the remote workflow on the shop. Both sides are normalized (sorted keys, stable indent) so the diff reflects real changes, not formatting noise.\n\nDefault direction is `--- remote` / `+++ local` so added lines = what `push` would change.\n\nExit code: 0 if no differences, 1 if differences found.\n\nTo inspect a remote workflow without a local file, use `shopify flow workflow show `.", + "customPluginName": "@shopify/store" }, - "theme:package": { - "aliases": [ + "flow:workflow:list": { + "aliases": [], + "args": {}, + "description": "Prints every workflow on the shop with id, name, hidden flag, and last-updated timestamp. Doesn't read or modify any local files.\n\nUse this to discover workflow IDs for `shopify flow workflow show ` or `shopify flow workflow pull --workflow-id `. For a project-scoped view that also classifies local files, use `shopify flow workflow status`.", + "examples": [ + "<%= config.bin %> <%= command.id %>", + "<%= config.bin %> <%= command.id %> --include-hidden --json" ], - "args": { - }, - "customPluginName": "@shopify/theme", - "description": "Packages your local theme files into a ZIP file that can be uploaded to Shopify.\n\n Only folders that match the \"default Shopify theme folder structure\" (https://shopify.dev/docs/storefronts/themes/tools/cli#directory-structure) are included in the package.\n\n The package includes the `listings` directory if present (required for multi-preset themes per \"Theme Store requirements\" (https://shopify.dev/docs/storefronts/themes/store/requirements#adding-presets-to-your-theme-zip-submission)).\n\n The ZIP file uses the name `theme_name-theme_version.zip`, based on parameters in your \"settings_schema.json\" (https://shopify.dev/docs/storefronts/themes/architecture/config/settings-schema-json) file.", - "descriptionWithMarkdown": "Packages your local theme files into a ZIP file that can be uploaded to Shopify.\n\n Only folders that match the [default Shopify theme folder structure](https://shopify.dev/docs/storefronts/themes/tools/cli#directory-structure) are included in the package.\n\n The package includes the `listings` directory if present (required for multi-preset themes per [Theme Store requirements](https://shopify.dev/docs/storefronts/themes/store/requirements#adding-presets-to-your-theme-zip-submission)).\n\n The ZIP file uses the name `theme_name-theme_version.zip`, based on parameters in your [settings_schema.json](https://shopify.dev/docs/storefronts/themes/architecture/config/settings-schema-json) file.", "flags": { "no-color": { - "allowNo": false, "description": "Disable color output.", "env": "SHOPIFY_FLAG_NO_COLOR", "hidden": false, "name": "no-color", + "allowNo": false, "type": "boolean" }, - "path": { - "description": "The path where you want to run the command. Defaults to the current working directory.", - "env": "SHOPIFY_FLAG_PATH", - "hasDynamicHelp": false, - "multiple": false, - "name": "path", - "noCacheDefault": true, - "type": "option" - }, "verbose": { - "allowNo": false, "description": "Increase the verbosity of the output.", "env": "SHOPIFY_FLAG_VERBOSE", "hidden": false, "name": "verbose", + "allowNo": false, + "type": "boolean" + }, + "json": { + "char": "j", + "description": "Output the result as JSON. Automatically disables color output.", + "env": "SHOPIFY_FLAG_JSON", + "hidden": false, + "name": "json", + "allowNo": false, + "type": "boolean" + }, + "store": { + "char": "s", + "description": "The myshopify.com domain. Falls back to flow.toml.", + "env": "SHOPIFY_FLAG_STORE", + "name": "store", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + }, + "include-hidden": { + "description": "Include hidden workflows.", + "name": "include-hidden", + "allowNo": false, "type": "boolean" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "theme:package", - "multiEnvironmentsFlags": null, + "hiddenAliases": [], + "id": "flow:workflow:list", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", "strict": true, - "summary": "Package your theme into a .zip file, ready to upload to the Online Store." + "summary": "List remote workflows on a shop.", + "descriptionWithMarkdown": "Prints every workflow on the shop with id, name, hidden flag, and last-updated timestamp. Doesn't read or modify any local files.\n\nUse this to discover workflow IDs for `shopify flow workflow show ` or `shopify flow workflow pull --workflow-id `. For a project-scoped view that also classifies local files, use `shopify flow workflow status`.", + "customPluginName": "@shopify/store" }, - "theme:preview": { - "aliases": [ - ], + "flow:workflow:preview": { + "aliases": [], "args": { + "file": { + "description": "Path to the workflow JSON file.", + "name": "file", + "required": true + } }, - "customPluginName": "@shopify/theme", - "description": "Applies a JSON overrides file to a theme and creates or updates a preview. This lets you quickly preview changes.\n\n The command returns a preview URL and a preview identifier. You can reuse the preview identifier with `--preview-id` to update an existing preview instead of creating a new one.", - "descriptionWithMarkdown": "Applies a JSON overrides file to a theme and creates or updates a preview. This lets you quickly preview changes.\n\n The command returns a preview URL and a preview identifier. You can reuse the preview identifier with `--preview-id` to update an existing preview instead of creating a new one.", + "description": "Reads a local `.flow.json` file, calls the Flow MCP preview transform endpoint, and prints the resulting JSON. The output is the payload the `/public/mcp-preview` iframe expects via `postMessage({type: \"mcp-workflow-preview\", workflow: })`.\n\nThis is the data path used by the Flow VSCode extension. The extension shells out to this command transparently when the user opens or saves a `.flow.json` file — no manual invocation required.", + "examples": [ + "<%= config.bin %> <%= command.id %> ./workflow.flow.json --store shop.myshopify.com" + ], "flags": { - "environment": { - "char": "e", - "description": "The environment to apply to the current command.", - "env": "SHOPIFY_FLAG_ENVIRONMENT", - "hasDynamicHelp": false, - "multiple": true, - "name": "environment", - "type": "option" - }, - "json": { - "allowNo": false, - "description": "Output the preview URL and identifier as JSON.", - "env": "SHOPIFY_FLAG_JSON", - "name": "json", - "type": "boolean" - }, "no-color": { - "allowNo": false, "description": "Disable color output.", "env": "SHOPIFY_FLAG_NO_COLOR", "hidden": false, "name": "no-color", + "allowNo": false, "type": "boolean" }, - "open": { + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", "allowNo": false, - "description": "Automatically launch the theme preview in your default web browser.", - "env": "SHOPIFY_FLAG_OPEN", - "name": "open", "type": "boolean" }, - "overrides": { - "description": "Path to a JSON overrides file.", - "env": "SHOPIFY_FLAG_OVERRIDES", - "hasDynamicHelp": false, - "multiple": false, - "name": "overrides", - "required": true, - "type": "option" - }, - "password": { - "description": "Password generated from the Theme Access app or an Admin API token.", - "env": "SHOPIFY_CLI_THEME_TOKEN", - "hasDynamicHelp": false, - "multiple": false, - "name": "password", - "type": "option" - }, - "path": { - "description": "The path where you want to run the command. Defaults to the current working directory.", - "env": "SHOPIFY_FLAG_PATH", - "hasDynamicHelp": false, - "multiple": false, - "name": "path", - "noCacheDefault": true, - "type": "option" - }, - "preview-id": { - "description": "An existing preview identifier to update instead of creating a new preview.", - "env": "SHOPIFY_FLAG_PREVIEW_ID", - "hasDynamicHelp": false, - "multiple": false, - "name": "preview-id", - "type": "option" + "json": { + "char": "j", + "description": "Output the result as JSON. Automatically disables color output.", + "env": "SHOPIFY_FLAG_JSON", + "hidden": false, + "name": "json", + "allowNo": false, + "type": "boolean" }, "store": { "char": "s", - "description": "Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com, https://example.myshopify.com).", + "description": "The myshopify.com domain. Falls back to the `store` field in flow.toml.", "env": "SHOPIFY_FLAG_STORE", - "hasDynamicHelp": false, - "multiple": false, "name": "store", - "type": "option" - }, - "theme": { - "char": "t", - "description": "Theme ID or name of the remote theme.", - "env": "SHOPIFY_FLAG_THEME_ID", "hasDynamicHelp": false, "multiple": false, - "name": "theme", - "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": "theme:preview", - "multiEnvironmentsFlags": null, + }, + "hasDynamicHelp": false, + "hiddenAliases": [], + "id": "flow:workflow:preview", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", "strict": true, - "summary": "Applies JSON overrides to a theme and returns a preview URL." + "summary": "Transform a local workflow JSON file into the preview payload used by the MCP preview iframe.", + "descriptionWithMarkdown": "Reads a local `.flow.json` file, calls the Flow MCP preview transform endpoint, and prints the resulting JSON. The output is the payload the `/public/mcp-preview` iframe expects via `postMessage({type: \"mcp-workflow-preview\", workflow: })`.\n\nThis is the data path used by the Flow VSCode extension. The extension shells out to this command transparently when the user opens or saves a `.flow.json` file — no manual invocation required.", + "customPluginName": "@shopify/store" }, - "theme:profile": { - "aliases": [ + "flow:workflow:pull": { + "aliases": [], + "args": {}, + "description": "Without `--all`: fetches a single workflow + leading definition from the shop and writes it to `--out` (normalized JSON). Sibling `.flow.lock.json` is also written.\n\nWith `--all`: lists every workflow on the shop and writes one file per workflow into the project's workflows directory. Skips files that already exist unless `--force` is set. Requires `flow.toml` (run `shopify flow init` first), or pass `--workflows-dir`.", + "examples": [ + "<%= config.bin %> <%= command.id %> --workflow-id 01HQK... --out ./workflow.flow.json --store shop.myshopify.com", + "<%= config.bin %> <%= command.id %> --all", + "<%= config.bin %> <%= command.id %> --all --include-hidden --force" ], - "args": { - }, - "customPluginName": "@shopify/theme", - "description": "Profile the Shopify Liquid on a given page.\n\n This command will open a web page with the Speedscope profiler detailing the time spent executing Liquid on the given page.", - "descriptionWithMarkdown": "Profile the Shopify Liquid on a given page.\n\n This command will open a web page with the Speedscope profiler detailing the time spent executing Liquid on the given page.", "flags": { - "environment": { - "char": "e", - "description": "The environment to apply to the current command.", - "env": "SHOPIFY_FLAG_ENVIRONMENT", - "hasDynamicHelp": false, - "multiple": true, - "name": "environment", - "type": "option" + "no-color": { + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", + "allowNo": false, + "type": "boolean" }, - "json": { + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", "allowNo": false, + "type": "boolean" + }, + "json": { "char": "j", "description": "Output the result as JSON. Automatically disables color output.", "env": "SHOPIFY_FLAG_JSON", "hidden": false, "name": "json", - "type": "boolean" - }, - "no-color": { "allowNo": false, - "description": "Disable color output.", - "env": "SHOPIFY_FLAG_NO_COLOR", - "hidden": false, - "name": "no-color", "type": "boolean" }, - "password": { - "description": "Password generated from the Theme Access app or an Admin API token.", - "env": "SHOPIFY_CLI_THEME_TOKEN", + "store": { + "char": "s", + "description": "The myshopify.com domain. Falls back to the `store` field in flow.toml.", + "env": "SHOPIFY_FLAG_STORE", + "name": "store", "hasDynamicHelp": false, "multiple": false, - "name": "password", "type": "option" }, - "path": { - "description": "The path where you want to run the command. Defaults to the current working directory.", - "env": "SHOPIFY_FLAG_PATH", + "workflow-id": { + "description": "ID of a single workflow to pull. Mutually exclusive with --all.", + "env": "SHOPIFY_FLAG_FLOW_WORKFLOW_ID", + "exclusive": [ + "all" + ], + "name": "workflow-id", "hasDynamicHelp": false, "multiple": false, - "name": "path", - "noCacheDefault": true, "type": "option" }, - "store": { - "char": "s", - "description": "Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com, https://example.myshopify.com).", - "env": "SHOPIFY_FLAG_STORE", + "workflow-version": { + "description": "Optional definition version when pulling a single workflow. Defaults to leading.", + "env": "SHOPIFY_FLAG_FLOW_WORKFLOW_VERSION", + "name": "workflow-version", "hasDynamicHelp": false, "multiple": false, - "name": "store", "type": "option" }, - "store-password": { - "description": "The password for storefronts with password protection.", - "env": "SHOPIFY_FLAG_STORE_PASSWORD", + "out": { + "description": "Path to write a single workflow JSON. Required without --all.", + "env": "SHOPIFY_FLAG_FLOW_OUT", + "name": "out", "hasDynamicHelp": false, "multiple": false, - "name": "store-password", "type": "option" }, - "theme": { - "char": "t", - "description": "Theme ID or name of the remote theme.", - "env": "SHOPIFY_FLAG_THEME_ID", - "hasDynamicHelp": false, - "multiple": false, - "name": "theme", - "type": "option" + "all": { + "description": "Pull every workflow on the shop into the project workflows directory.", + "exclusive": [ + "workflow-id" + ], + "name": "all", + "allowNo": false, + "type": "boolean" }, - "url": { - "default": "/", - "description": "The url to be used as context", - "env": "SHOPIFY_FLAG_URL", + "workflows-dir": { + "description": "Directory to write workflows into when using --all. Falls back to flow.toml.", + "env": "SHOPIFY_FLAG_FLOW_WORKFLOWS_DIR", + "name": "workflows-dir", "hasDynamicHelp": false, "multiple": false, - "name": "url", "type": "option" }, - "verbose": { + "include-hidden": { + "description": "Include hidden workflows when pulling --all.", + "name": "include-hidden", + "allowNo": false, + "type": "boolean" + }, + "force": { + "description": "Overwrite existing files when pulling --all. (Single-pull always overwrites the --out path.)", + "name": "force", "allowNo": false, - "description": "Increase the verbosity of the output.", - "env": "SHOPIFY_FLAG_VERBOSE", - "hidden": false, - "name": "verbose", "type": "boolean" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "theme:profile", - "multiEnvironmentsFlags": null, + "hiddenAliases": [], + "id": "flow:workflow:pull", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", "strict": true, - "summary": "Profile the Liquid rendering of a theme page.", - "usage": [ - "theme profile", - "theme profile --url /products/classic-leather-jacket" - ] + "summary": "Pull a Flow workflow definition (or all of them) into local files with lockfiles.", + "descriptionWithMarkdown": "Without `--all`: fetches a single workflow + leading definition from the shop and writes it to `--out` (normalized JSON). Sibling `.flow.lock.json` is also written.\n\nWith `--all`: lists every workflow on the shop and writes one file per workflow into the project's workflows directory. Skips files that already exist unless `--force` is set. Requires `flow.toml` (run `shopify flow init` first), or pass `--workflows-dir`.", + "customPluginName": "@shopify/store" }, - "theme:publish": { - "aliases": [ - ], + "flow:workflow:push": { + "aliases": [], "args": { + "file": { + "description": "Path to the workflow JSON file.", + "name": "file", + "required": true + } }, - "customPluginName": "@shopify/theme", - "description": "Publishes an unpublished theme from your theme library.\n\nIf no theme ID is specified, then you're prompted to select the theme that you want to publish from the list of themes in your store.\n\nYou can run this command only in a directory that matches the \"default Shopify theme folder structure\" (https://shopify.dev/docs/themes/tools/cli#directory-structure).\n\nIf you want to publish your local theme, then you need to run `shopify theme push` first. You're asked to confirm that you want to publish the specified theme. You can skip this confirmation using the `--force` flag.", - "descriptionWithMarkdown": "Publishes an unpublished theme from your theme library.\n\nIf no theme ID is specified, then you're prompted to select the theme that you want to publish from the list of themes in your store.\n\nYou can run this command only in a directory that matches the [default Shopify theme folder structure](https://shopify.dev/docs/themes/tools/cli#directory-structure).\n\nIf you want to publish your local theme, then you need to run `shopify theme push` first. You're asked to confirm that you want to publish the specified theme. You can skip this confirmation using the `--force` flag.", + "description": "Creates or updates a workflow on the shop. Writes a sibling `.flow.lock.json` with the returned workflow_id, version, and payload SHA. Workflows are pushed `hidden: false` so they can be activated.", + "examples": [ + "<%= config.bin %> <%= command.id %> ./workflow.flow.json --store shop.myshopify.com" + ], "flags": { - "environment": { - "char": "e", - "description": "The environment to apply to the current command.", - "env": "SHOPIFY_FLAG_ENVIRONMENT", - "hasDynamicHelp": false, - "multiple": true, - "name": "environment", - "type": "option" - }, - "force": { - "allowNo": false, - "char": "f", - "description": "Skip confirmation.", - "env": "SHOPIFY_FLAG_FORCE", - "name": "force", - "type": "boolean" - }, "no-color": { - "allowNo": false, "description": "Disable color output.", "env": "SHOPIFY_FLAG_NO_COLOR", "hidden": false, "name": "no-color", + "allowNo": false, "type": "boolean" }, - "password": { - "description": "Password generated from the Theme Access app or an Admin API token.", - "env": "SHOPIFY_CLI_THEME_TOKEN", - "hasDynamicHelp": false, - "multiple": false, - "name": "password", - "type": "option" + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", + "allowNo": false, + "type": "boolean" }, - "path": { - "description": "The path where you want to run the command. Defaults to the current working directory.", - "env": "SHOPIFY_FLAG_PATH", - "hasDynamicHelp": false, - "multiple": false, - "name": "path", - "noCacheDefault": true, - "type": "option" + "json": { + "char": "j", + "description": "Output the result as JSON. Automatically disables color output.", + "env": "SHOPIFY_FLAG_JSON", + "hidden": false, + "name": "json", + "allowNo": false, + "type": "boolean" }, "store": { "char": "s", - "description": "Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com, https://example.myshopify.com).", + "description": "The myshopify.com domain. Falls back to the `store` field in flow.toml.", "env": "SHOPIFY_FLAG_STORE", - "hasDynamicHelp": false, - "multiple": false, "name": "store", - "type": "option" - }, - "theme": { - "char": "t", - "description": "Theme ID or name of the remote theme.", - "env": "SHOPIFY_FLAG_THEME_ID", "hasDynamicHelp": false, "multiple": false, - "name": "theme", "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": "theme:publish", - "multiEnvironmentsFlags": [ - "store", - "password", - "theme" - ], + "hiddenAliases": [], + "id": "flow:workflow:push", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", "strict": true, - "summary": "Set a remote theme as the live theme." + "summary": "Push a Flow workflow JSON file to a shop and write a lockfile.", + "descriptionWithMarkdown": "Creates or updates a workflow on the shop. Writes a sibling `.flow.lock.json` with the returned workflow_id, version, and payload SHA. Workflows are pushed `hidden: false` so they can be activated.", + "customPluginName": "@shopify/store" }, - "theme:pull": { - "aliases": [ - ], + "flow:workflow:show": { + "aliases": [], "args": { + "workflow-id": { + "description": "ID of the workflow to show.", + "name": "workflow-id", + "required": true + } }, - "customPluginName": "@shopify/theme", - "description": "Retrieves theme files from Shopify.\n\nIf no theme is specified, then you're prompted to select the theme to pull from the list of the themes in your store.", - "descriptionWithMarkdown": "Retrieves theme files from Shopify.\n\nIf no theme is specified, then you're prompted to select the theme to pull from the list of the themes in your store.", + "description": "Fetches a workflow from the shop and prints its full definition (sorted keys, 2-space indent) to stdout. Doesn't write any files. Use this to inspect a remote workflow without committing to a local file.\n\nTo save the result, redirect to a file. To pull and create a tracked local copy with a lockfile, use `shopify flow workflow pull`.", + "examples": [ + "<%= config.bin %> <%= command.id %> 01HQK000000000000000000000", + "<%= config.bin %> <%= command.id %> 01HQK... --workflow-version 01HQL... --store shop.myshopify.com", + "<%= config.bin %> <%= command.id %> 01HQK... > /tmp/workflow.json" + ], "flags": { - "development": { + "no-color": { + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", "allowNo": false, - "char": "d", - "description": "Pull theme files from your remote development theme.", - "env": "SHOPIFY_FLAG_DEVELOPMENT", - "name": "development", "type": "boolean" }, - "environment": { - "char": "e", - "description": "The environment to apply to the current command.", - "env": "SHOPIFY_FLAG_ENVIRONMENT", - "hasDynamicHelp": false, - "multiple": true, - "name": "environment", - "type": "option" - }, - "force": { + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", "allowNo": false, - "char": "f", - "description": "Proceed without confirmation, if current directory does not seem to be theme directory.", - "env": "SHOPIFY_FLAG_FORCE", - "hidden": true, - "name": "force", "type": "boolean" }, - "ignore": { - "char": "x", - "description": "Skip downloading the specified files (Multiple flags allowed). Wrap the value in double quotes if you're using wildcards.", - "env": "SHOPIFY_FLAG_IGNORE", + "store": { + "char": "s", + "description": "The myshopify.com domain. Falls back to the `store` field in flow.toml.", + "env": "SHOPIFY_FLAG_STORE", + "name": "store", "hasDynamicHelp": false, - "multiple": true, - "name": "ignore", + "multiple": false, "type": "option" }, - "live": { - "allowNo": false, - "char": "l", - "description": "Pull theme files from your remote live theme.", - "env": "SHOPIFY_FLAG_LIVE", - "name": "live", - "type": "boolean" - }, + "workflow-version": { + "description": "Optional definition version. Defaults to leading.", + "env": "SHOPIFY_FLAG_FLOW_WORKFLOW_VERSION", + "name": "workflow-version", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + } + }, + "hasDynamicHelp": false, + "hiddenAliases": [], + "id": "flow:workflow:show", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true, + "summary": "Print a remote workflow definition as normalized JSON.", + "descriptionWithMarkdown": "Fetches a workflow from the shop and prints its full definition (sorted keys, 2-space indent) to stdout. Doesn't write any files. Use this to inspect a remote workflow without committing to a local file.\n\nTo save the result, redirect to a file. To pull and create a tracked local copy with a lockfile, use `shopify flow workflow pull`.", + "customPluginName": "@shopify/store" + }, + "flow:workflow:status": { + "aliases": [], + "args": {}, + "description": "Walks the project's workflows directory and classifies every workflow:\n\n- `clean` — local file matches the remote workflow\n- `drifted` — local file and remote differ; `push` would update remote, `pull` would update local\n- `new` — local file with no lockfile; never pushed\n- `orphaned` — lockfile points to a workflow that no longer exists on the shop\n- `unknown` — workflow exists on the shop but isn't tracked locally; `pull --workflow-id ` to bring it in\n\nExits 1 if anything other than `clean` is present.", + "examples": [ + "<%= config.bin %> <%= command.id %>", + "<%= config.bin %> <%= command.id %> --json" + ], + "flags": { "no-color": { - "allowNo": false, "description": "Disable color output.", "env": "SHOPIFY_FLAG_NO_COLOR", "hidden": false, "name": "no-color", + "allowNo": false, "type": "boolean" }, - "nodelete": { + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", "allowNo": false, - "char": "n", - "description": "Prevent deleting local files that don't exist remotely.", - "env": "SHOPIFY_FLAG_NODELETE", - "name": "nodelete", "type": "boolean" }, - "only": { - "char": "o", - "description": "Download only the specified files (Multiple flags allowed). Wrap the value in double quotes if you're using wildcards.", - "env": "SHOPIFY_FLAG_ONLY", - "hasDynamicHelp": false, - "multiple": true, - "name": "only", - "type": "option" - }, - "password": { - "description": "Password generated from the Theme Access app or an Admin API token.", - "env": "SHOPIFY_CLI_THEME_TOKEN", - "hasDynamicHelp": false, - "multiple": false, - "name": "password", - "type": "option" - }, - "path": { - "description": "The path where you want to run the command. Defaults to the current working directory.", - "env": "SHOPIFY_FLAG_PATH", - "hasDynamicHelp": false, - "multiple": false, - "name": "path", - "noCacheDefault": true, - "type": "option" + "json": { + "char": "j", + "description": "Output the result as JSON. Automatically disables color output.", + "env": "SHOPIFY_FLAG_JSON", + "hidden": false, + "name": "json", + "allowNo": false, + "type": "boolean" }, "store": { "char": "s", - "description": "Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com, https://example.myshopify.com).", + "description": "The myshopify.com domain. Falls back to the `store` field in flow.toml.", "env": "SHOPIFY_FLAG_STORE", + "name": "store", "hasDynamicHelp": false, "multiple": false, - "name": "store", "type": "option" }, - "theme": { - "char": "t", - "description": "Theme ID or name of the remote theme.", - "env": "SHOPIFY_FLAG_THEME_ID", + "workflows-dir": { + "description": "Directory containing workflow JSON files. Falls back to flow.toml.", + "env": "SHOPIFY_FLAG_FLOW_WORKFLOWS_DIR", + "name": "workflows-dir", "hasDynamicHelp": false, "multiple": false, - "name": "theme", "type": "option" }, - "verbose": { + "include-hidden": { + "description": "Include hidden remote workflows in the unknown-detection list.", + "name": "include-hidden", "allowNo": false, - "description": "Increase the verbosity of the output.", - "env": "SHOPIFY_FLAG_VERBOSE", - "hidden": false, - "name": "verbose", "type": "boolean" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "theme:pull", - "multiEnvironmentsFlags": [ - "store", - "password", - "path", - [ - "live", - "development", - "theme" - ] - ], + "hiddenAliases": [], + "id": "flow:workflow:status", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", "strict": true, - "summary": "Download your remote theme files locally." + "summary": "Show drift between local workflow files and the shop.", + "descriptionWithMarkdown": "Walks the project's workflows directory and classifies every workflow:\n\n- `clean` — local file matches the remote workflow\n- `drifted` — local file and remote differ; `push` would update remote, `pull` would update local\n- `new` — local file with no lockfile; never pushed\n- `orphaned` — lockfile points to a workflow that no longer exists on the shop\n- `unknown` — workflow exists on the shop but isn't tracked locally; `pull --workflow-id ` to bring it in\n\nExits 1 if anything other than `clean` is present.", + "customPluginName": "@shopify/store" }, - "theme:push": { - "aliases": [ - ], + "flow:workflow:validate": { + "aliases": [], "args": { + "file": { + "description": "Path to the workflow JSON file.", + "name": "file", + "required": true + } }, - "customPluginName": "@shopify/theme", - "description": "Uploads your local theme files to Shopify, overwriting the remote version if specified.\n\n If no theme is specified, then you're prompted to select the theme to overwrite from the list of the themes in your store.\n\n You can run this command only in a directory that matches the \"default Shopify theme folder structure\" (https://shopify.dev/docs/themes/tools/cli#directory-structure).\n\n This command returns the following information:\n\n - A link to the \"editor\" (https://shopify.dev/docs/themes/tools/online-editor) for the theme in the Shopify admin.\n - A \"preview link\" (https://help.shopify.com/manual/online-store/themes/adding-themes#share-a-theme-preview-with-others) that you can share with others.\n\n If you use the `--json` flag, then theme information is returned in JSON format, which can be used as a machine-readable input for scripts or continuous integration.\n\n Sample output:\n\n ```json\n {\n \"theme\": {\n \"id\": 108267175958,\n \"name\": \"MyTheme\",\n \"role\": \"unpublished\",\n \"shop\": \"mystore.myshopify.com\",\n \"editor_url\": \"https://mystore.myshopify.com/admin/themes/108267175958/editor\",\n \"preview_url\": \"https://mystore.myshopify.com/?preview_theme_id=108267175958\"\n }\n }\n ```\n ", - "descriptionWithMarkdown": "Uploads your local theme files to Shopify, overwriting the remote version if specified.\n\n If no theme is specified, then you're prompted to select the theme to overwrite from the list of the themes in your store.\n\n You can run this command only in a directory that matches the [default Shopify theme folder structure](https://shopify.dev/docs/themes/tools/cli#directory-structure).\n\n This command returns the following information:\n\n - A link to the [editor](https://shopify.dev/docs/themes/tools/online-editor) for the theme in the Shopify admin.\n - A [preview link](https://help.shopify.com/manual/online-store/themes/adding-themes#share-a-theme-preview-with-others) that you can share with others.\n\n If you use the `--json` flag, then theme information is returned in JSON format, which can be used as a machine-readable input for scripts or continuous integration.\n\n Sample output:\n\n ```json\n {\n \"theme\": {\n \"id\": 108267175958,\n \"name\": \"MyTheme\",\n \"role\": \"unpublished\",\n \"shop\": \"mystore.myshopify.com\",\n \"editor_url\": \"https://mystore.myshopify.com/admin/themes/108267175958/editor\",\n \"preview_url\": \"https://mystore.myshopify.com/?preview_theme_id=108267175958\"\n }\n }\n ```\n ", + "description": "Runs the same validation as push but in dry-run mode (`is_eval`). No DB writes; returns validation errors against the shop scope.", + "examples": [ + "<%= config.bin %> <%= command.id %> ./workflow.flow.json --store shop.myshopify.com" + ], "flags": { - "allow-live": { + "no-color": { + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", "allowNo": false, - "char": "a", - "description": "Allow push to a live theme.", - "env": "SHOPIFY_FLAG_ALLOW_LIVE", - "name": "allow-live", "type": "boolean" }, - "development": { + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", "allowNo": false, - "char": "d", - "description": "Push theme files from your remote development theme.", - "env": "SHOPIFY_FLAG_DEVELOPMENT", - "name": "development", "type": "boolean" }, - "development-context": { - "char": "c", - "dependsOn": [ - "development" - ], - "description": "Unique identifier for a development theme context (e.g., PR number, branch name). Reuses an existing development theme with this context name, or creates one if none exists.", - "env": "SHOPIFY_FLAG_DEVELOPMENT_CONTEXT", - "exclusive": [ - "theme" - ], - "hasDynamicHelp": false, - "multiple": false, - "name": "development-context", - "type": "option" + "json": { + "char": "j", + "description": "Output the result as JSON. Automatically disables color output.", + "env": "SHOPIFY_FLAG_JSON", + "hidden": false, + "name": "json", + "allowNo": false, + "type": "boolean" }, - "environment": { - "char": "e", - "description": "The environment to apply to the current command.", - "env": "SHOPIFY_FLAG_ENVIRONMENT", + "store": { + "char": "s", + "description": "The myshopify.com domain. Falls back to the `store` field in flow.toml.", + "env": "SHOPIFY_FLAG_STORE", + "name": "store", "hasDynamicHelp": false, - "multiple": true, - "name": "environment", + "multiple": false, "type": "option" - }, - "force": { + } + }, + "hasDynamicHelp": false, + "hiddenAliases": [], + "id": "flow:workflow:validate", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true, + "summary": "Validate a Flow workflow JSON file against a shop without persisting.", + "descriptionWithMarkdown": "Runs the same validation as push but in dry-run mode (`is_eval`). No DB writes; returns validation errors against the shop scope.", + "customPluginName": "@shopify/store" + }, + "store:auth": { + "aliases": [], + "args": {}, + "description": "Authenticates the app against the specified store for store commands and stores an online access token for later reuse.\n\nRe-run this command if the stored token is missing, expires, or no longer has the scopes you need.", + "examples": [ + "<%= config.bin %> <%= command.id %> --store shop.myshopify.com --scopes read_products,write_products", + "<%= config.bin %> <%= command.id %> --store shop.myshopify.com --scopes read_products,write_products --json" + ], + "flags": { + "no-color": { + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", "allowNo": false, - "char": "f", - "description": "Proceed without confirmation, if current directory does not seem to be theme directory.", - "env": "SHOPIFY_FLAG_FORCE", - "hidden": true, - "name": "force", "type": "boolean" }, - "ignore": { - "char": "x", - "description": "Skip uploading the specified files (Multiple flags allowed). Wrap the value in double quotes if you're using wildcards.", - "env": "SHOPIFY_FLAG_IGNORE", - "hasDynamicHelp": false, - "multiple": true, - "name": "ignore", - "type": "option" + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", + "allowNo": false, + "type": "boolean" }, "json": { - "allowNo": false, "char": "j", "description": "Output the result as JSON. Automatically disables color output.", "env": "SHOPIFY_FLAG_JSON", "hidden": false, "name": "json", + "allowNo": false, "type": "boolean" }, - "listing": { - "description": "The listing preset to use for multi-preset themes. Applies preset files from listings/[preset-name] directory.", - "env": "SHOPIFY_FLAG_LISTING", + "store": { + "char": "s", + "description": "The myshopify.com domain of the store to authenticate against.", + "env": "SHOPIFY_FLAG_STORE", + "name": "store", + "required": true, "hasDynamicHelp": false, "multiple": false, - "name": "listing", "type": "option" }, - "live": { - "allowNo": false, - "char": "l", - "description": "Push theme files from your remote live theme.", - "env": "SHOPIFY_FLAG_LIVE", - "name": "live", - "type": "boolean" - }, + "scopes": { + "description": "Comma-separated Admin API scopes to request for the app.", + "env": "SHOPIFY_FLAG_SCOPES", + "name": "scopes", + "required": true, + "hasDynamicHelp": false, + "multiple": false, + "type": "option" + } + }, + "hasDynamicHelp": false, + "hiddenAliases": [], + "id": "store:auth", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true, + "summary": "Authenticate an app against a store for store commands.", + "descriptionWithMarkdown": "Authenticates the app against the specified store for store commands and stores an online access token for later reuse.\n\nRe-run this command if the stored token is missing, expires, or no longer has the scopes you need.", + "customPluginName": "@shopify/store" + }, + "store:execute": { + "aliases": [], + "args": {}, + "description": "Executes an Admin API GraphQL query or mutation on the specified store using previously stored app authentication.\n\nRun `shopify store auth` first to create stored auth for the store.\n\nMutations are disabled by default. Re-run with `--allow-mutations` if you intend to modify store data.", + "examples": [ + "<%= config.bin %> <%= command.id %> --store shop.myshopify.com --query \"query { shop { name } }\"", + "<%= config.bin %> <%= command.id %> --store shop.myshopify.com --query-file ./operation.graphql --variables '{\"id\":\"gid://shopify/Product/1\"}'", + "<%= config.bin %> <%= command.id %> --store shop.myshopify.com --query \"mutation { shop { id } }\" --allow-mutations", + "<%= config.bin %> <%= command.id %> --store shop.myshopify.com --query \"query { shop { name } }\" --json" + ], + "flags": { "no-color": { - "allowNo": false, "description": "Disable color output.", "env": "SHOPIFY_FLAG_NO_COLOR", "hidden": false, "name": "no-color", + "allowNo": false, "type": "boolean" }, - "nodelete": { + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", "allowNo": false, - "char": "n", - "description": "Prevent deleting remote files that don't exist locally.", - "env": "SHOPIFY_FLAG_NODELETE", - "name": "nodelete", "type": "boolean" }, - "only": { - "char": "o", - "description": "Upload only the specified files (Multiple flags allowed). Wrap the value in double quotes if you're using wildcards.", - "env": "SHOPIFY_FLAG_ONLY", + "json": { + "char": "j", + "description": "Output the result as JSON. Automatically disables color output.", + "env": "SHOPIFY_FLAG_JSON", + "hidden": false, + "name": "json", + "allowNo": false, + "type": "boolean" + }, + "query": { + "char": "q", + "description": "The GraphQL query or mutation, as a string.", + "env": "SHOPIFY_FLAG_QUERY", + "name": "query", + "required": false, "hasDynamicHelp": false, - "multiple": true, - "name": "only", + "multiple": false, "type": "option" }, - "password": { - "description": "Password generated from the Theme Access app or an Admin API token.", - "env": "SHOPIFY_CLI_THEME_TOKEN", + "query-file": { + "description": "Path to a file containing the GraphQL query or mutation. Can't be used with --query.", + "env": "SHOPIFY_FLAG_QUERY_FILE", + "name": "query-file", "hasDynamicHelp": false, "multiple": false, - "name": "password", "type": "option" }, - "path": { - "description": "The path where you want to run the command. Defaults to the current working directory.", - "env": "SHOPIFY_FLAG_PATH", + "variables": { + "char": "v", + "description": "The values for any GraphQL variables in your query or mutation, in JSON format.", + "env": "SHOPIFY_FLAG_VARIABLES", + "exclusive": [ + "variable-file" + ], + "name": "variables", "hasDynamicHelp": false, "multiple": false, - "name": "path", - "noCacheDefault": true, "type": "option" }, - "publish": { - "allowNo": false, - "char": "p", - "description": "Publish as the live theme after uploading.", - "env": "SHOPIFY_FLAG_PUBLISH", - "name": "publish", - "type": "boolean" + "variable-file": { + "description": "Path to a file containing GraphQL variables in JSON format. Can't be used with --variables.", + "env": "SHOPIFY_FLAG_VARIABLE_FILE", + "exclusive": [ + "variables" + ], + "name": "variable-file", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" }, "store": { "char": "s", - "description": "Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com, https://example.myshopify.com).", + "description": "The myshopify.com domain of the store to execute against.", "env": "SHOPIFY_FLAG_STORE", + "name": "store", + "required": true, "hasDynamicHelp": false, "multiple": false, - "name": "store", "type": "option" }, - "strict": { - "allowNo": false, - "description": "Require theme check to pass without errors before pushing. Warnings are allowed.", - "env": "SHOPIFY_FLAG_STRICT_PUSH", - "name": "strict", - "type": "boolean" - }, - "theme": { - "char": "t", - "description": "Theme ID or name of the remote theme.", - "env": "SHOPIFY_FLAG_THEME_ID", + "version": { + "description": "The API version to use for the query or mutation. Defaults to the latest stable version.", + "env": "SHOPIFY_FLAG_VERSION", + "name": "version", "hasDynamicHelp": false, "multiple": false, - "name": "theme", "type": "option" }, - "unpublished": { - "allowNo": false, - "char": "u", - "description": "Create a new unpublished theme and push to it.", - "env": "SHOPIFY_FLAG_UNPUBLISHED", - "name": "unpublished", - "type": "boolean" + "output-file": { + "description": "The file name where results should be written, instead of STDOUT.", + "env": "SHOPIFY_FLAG_OUTPUT_FILE", + "name": "output-file", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" }, - "verbose": { + "allow-mutations": { + "description": "Allow GraphQL mutations to run against the target store.", + "env": "SHOPIFY_FLAG_ALLOW_MUTATIONS", + "name": "allow-mutations", "allowNo": false, - "description": "Increase the verbosity of the output.", - "env": "SHOPIFY_FLAG_VERBOSE", - "hidden": false, - "name": "verbose", "type": "boolean" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "theme:push", - "multiEnvironmentsFlags": [ - "store", - "password", - "path", - [ - "live", - "development", - "theme" - ] - ], + "hiddenAliases": [], + "id": "store:execute", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", "strict": true, - "summary": "Uploads your local theme files to the connected store, overwriting the remote version if specified.", - "usage": [ - "theme push", - "theme push --unpublished --json" - ] + "summary": "Execute GraphQL queries and mutations on a store.", + "descriptionWithMarkdown": "Executes an Admin API GraphQL query or mutation on the specified store using previously stored app authentication.\n\nRun `shopify store auth` first to create stored auth for the store.\n\nMutations are disabled by default. Re-run with `--allow-mutations` if you intend to modify store data.", + "customPluginName": "@shopify/store" }, - "theme:rename": { - "aliases": [ + "search": { + "aliases": [], + "args": { + "query": { + "name": "query" + } + }, + "description": "Starts a search on shopify.dev.", + "examples": [ + "# open the search modal on Shopify.dev\n shopify search\n\n # search for a term on Shopify.dev\n shopify search \n\n # search for a phrase on Shopify.dev\n shopify search \"\"\n " ], + "flags": {}, + "hasDynamicHelp": false, + "hiddenAliases": [], + "id": "search", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true, + "usage": "search [query]", + "enableJsonFlag": false + }, + "upgrade": { + "aliases": [], + "args": {}, + "description": "Upgrades Shopify CLI using your package manager.", + "flags": {}, + "hasDynamicHelp": false, + "hiddenAliases": [], + "id": "upgrade", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true, + "summary": "Upgrades Shopify CLI.", + "enableJsonFlag": false, + "descriptionWithMarkdown": "Upgrades Shopify CLI using your package manager." + }, + "version": { + "aliases": [], + "args": {}, + "description": "Shopify CLI version currently installed.", + "flags": {}, + "hasDynamicHelp": false, + "hiddenAliases": [], + "id": "version", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true, + "enableJsonFlag": false + }, + "help": { + "aliases": [], "args": { + "command": { + "description": "Command to show help for.", + "name": "command", + "required": false + } }, - "customPluginName": "@shopify/theme", - "description": "Renames a theme in your store.\n\n If no theme is specified, then you're prompted to select the theme that you want to rename from the list of themes in your store.\n ", - "descriptionWithMarkdown": "Renames a theme in your store.\n\n If no theme is specified, then you're prompted to select the theme that you want to rename from the list of themes in your store.\n ", + "description": "Display help for Shopify CLI", "flags": { - "development": { - "allowNo": false, - "char": "d", - "description": "Rename your development theme.", - "env": "SHOPIFY_FLAG_DEVELOPMENT", - "name": "development", - "type": "boolean" - }, - "environment": { - "char": "e", - "description": "The environment to apply to the current command.", - "env": "SHOPIFY_FLAG_ENVIRONMENT", - "hasDynamicHelp": false, - "multiple": true, - "name": "environment", - "type": "option" - }, - "live": { - "allowNo": false, - "char": "l", - "description": "Rename your remote live theme.", - "env": "SHOPIFY_FLAG_LIVE", - "name": "live", - "type": "boolean" - }, - "name": { + "nested-commands": { "char": "n", - "description": "The new name for the theme.", - "env": "SHOPIFY_FLAG_NEW_NAME", - "hasDynamicHelp": false, - "multiple": false, - "name": "name", - "required": false, - "type": "option" - }, - "no-color": { + "description": "Include all nested commands in the output.", + "env": "SHOPIFY_FLAG_CLI_NESTED_COMMANDS", + "name": "nested-commands", "allowNo": false, - "description": "Disable color output.", - "env": "SHOPIFY_FLAG_NO_COLOR", - "hidden": false, - "name": "no-color", "type": "boolean" - }, - "password": { - "description": "Password generated from the Theme Access app or an Admin API token.", - "env": "SHOPIFY_CLI_THEME_TOKEN", - "hasDynamicHelp": false, - "multiple": false, - "name": "password", - "type": "option" - }, - "path": { - "description": "The path where you want to run the command. Defaults to the current working directory.", - "env": "SHOPIFY_FLAG_PATH", - "hasDynamicHelp": false, - "multiple": false, - "name": "path", - "noCacheDefault": true, - "type": "option" - }, - "store": { - "char": "s", - "description": "Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com, https://example.myshopify.com).", - "env": "SHOPIFY_FLAG_STORE", - "hasDynamicHelp": false, - "multiple": false, - "name": "store", - "type": "option" - }, - "theme": { - "char": "t", - "description": "Theme ID or name of the remote theme.", - "env": "SHOPIFY_FLAG_THEME_ID", + } + }, + "hasDynamicHelp": false, + "hiddenAliases": [], + "id": "help", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": false, + "usage": "help [command] [flags]", + "enableJsonFlag": false + }, + "auth:logout": { + "aliases": [], + "args": {}, + "description": "Logs you out of the Shopify account or Partner account and store.", + "flags": {}, + "hasDynamicHelp": false, + "hiddenAliases": [], + "id": "auth:logout", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true, + "enableJsonFlag": false + }, + "auth:login": { + "aliases": [], + "args": {}, + "description": "Logs you in to your Shopify account.", + "flags": { + "alias": { + "description": "Alias of the session you want to login to.", + "env": "SHOPIFY_FLAG_AUTH_ALIAS", + "name": "alias", "hasDynamicHelp": false, "multiple": false, - "name": "theme", "type": "option" - }, - "verbose": { + } + }, + "hasDynamicHelp": false, + "hiddenAliases": [], + "id": "auth:login", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true, + "enableJsonFlag": false + }, + "debug:command-flags": { + "aliases": [], + "args": {}, + "description": "View all the available command flags", + "flags": { + "csv": { + "description": "Output as CSV", + "env": "SHOPIFY_FLAG_OUTPUT_CSV", + "name": "csv", "allowNo": false, - "description": "Increase the verbosity of the output.", - "env": "SHOPIFY_FLAG_VERBOSE", - "hidden": false, - "name": "verbose", "type": "boolean" } }, "hasDynamicHelp": false, + "hidden": true, + "hiddenAliases": [], + "id": "debug:command-flags", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true, + "enableJsonFlag": false + }, + "kitchen-sink": { + "aliases": [], + "args": {}, + "description": "View all the available UI kit components", + "flags": {}, + "hasDynamicHelp": false, + "hidden": true, "hiddenAliases": [ + "kitchen-sink all" ], - "id": "theme:rename", - "multiEnvironmentsFlags": [ - "store", - "password", - "name", - [ - "live", - "development", - "theme" - ] - ], + "id": "kitchen-sink", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", "strict": true, - "summary": "Renames an existing theme." + "enableJsonFlag": false }, - "theme:share": { - "aliases": [ - ], - "args": { - }, - "customPluginName": "@shopify/theme", - "description": "Uploads your theme as a new, unpublished theme in your theme library. The theme is given a randomized name.\n\n This command returns a \"preview link\" (https://help.shopify.com/manual/online-store/themes/adding-themes#share-a-theme-preview-with-others) that you can share with others.", - "descriptionWithMarkdown": "Uploads your theme as a new, unpublished theme in your theme library. The theme is given a randomized name.\n\n This command returns a [preview link](https://help.shopify.com/manual/online-store/themes/adding-themes#share-a-theme-preview-with-others) that you can share with others.", + "kitchen-sink:async": { + "aliases": [], + "args": {}, + "description": "View the UI kit components that process async tasks", + "flags": {}, + "hasDynamicHelp": false, + "hidden": true, + "hiddenAliases": [], + "id": "kitchen-sink:async", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true, + "enableJsonFlag": false + }, + "kitchen-sink:prompts": { + "aliases": [], + "args": {}, + "description": "View the UI kit components prompts", + "flags": {}, + "hasDynamicHelp": false, + "hidden": true, + "hiddenAliases": [], + "id": "kitchen-sink:prompts", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true, + "enableJsonFlag": false + }, + "kitchen-sink:static": { + "aliases": [], + "args": {}, + "description": "View the UI kit components that display static output", + "flags": {}, + "hasDynamicHelp": false, + "hidden": true, + "hiddenAliases": [], + "id": "kitchen-sink:static", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true, + "enableJsonFlag": false + }, + "doctor-release": { + "aliases": [], + "args": {}, + "description": "Run CLI doctor-release tests", + "flags": {}, + "hasDynamicHelp": false, + "hidden": true, + "hiddenAliases": [], + "id": "doctor-release", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true, + "enableJsonFlag": false + }, + "doctor-release:theme": { + "aliases": [], + "args": {}, + "description": "Run all theme command doctor-release tests", "flags": { - "environment": { - "char": "e", - "description": "The environment to apply to the current command.", - "env": "SHOPIFY_FLAG_ENVIRONMENT", - "hasDynamicHelp": false, - "multiple": true, - "name": "environment", - "type": "option" - }, - "force": { - "allowNo": false, - "char": "f", - "description": "Proceed without confirmation, if current directory does not seem to be theme directory.", - "env": "SHOPIFY_FLAG_FORCE", - "hidden": true, - "name": "force", - "type": "boolean" - }, - "listing": { - "description": "The listing preset to use for multi-preset themes. Applies preset files from listings/[preset-name] directory.", - "env": "SHOPIFY_FLAG_LISTING", - "hasDynamicHelp": false, - "multiple": false, - "name": "listing", - "type": "option" - }, "no-color": { - "allowNo": false, "description": "Disable color output.", "env": "SHOPIFY_FLAG_NO_COLOR", "hidden": false, "name": "no-color", + "allowNo": false, "type": "boolean" }, - "password": { - "description": "Password generated from the Theme Access app or an Admin API token.", - "env": "SHOPIFY_CLI_THEME_TOKEN", + "verbose": { + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", + "allowNo": false, + "type": "boolean" + }, + "path": { + "char": "p", + "description": "The path to run tests in. Defaults to current directory.", + "env": "SHOPIFY_FLAG_PATH", + "name": "path", + "default": "/Users/gasse/src/github.com/Shopify/cli/packages/cli", "hasDynamicHelp": false, "multiple": false, - "name": "password", "type": "option" }, - "path": { - "description": "The path where you want to run the command. Defaults to the current working directory.", - "env": "SHOPIFY_FLAG_PATH", + "environment": { + "char": "e", + "description": "The environment to use from shopify.theme.toml (required for store-connected tests).", + "env": "SHOPIFY_FLAG_ENVIRONMENT", + "name": "environment", + "required": true, "hasDynamicHelp": false, "multiple": false, - "name": "path", - "noCacheDefault": true, "type": "option" }, "store": { "char": "s", - "description": "Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com, https://example.myshopify.com).", + "description": "Store URL (overrides environment).", "env": "SHOPIFY_FLAG_STORE", + "name": "store", "hasDynamicHelp": false, "multiple": false, - "name": "store", "type": "option" }, - "verbose": { - "allowNo": false, - "description": "Increase the verbosity of the output.", - "env": "SHOPIFY_FLAG_VERBOSE", - "hidden": false, - "name": "verbose", - "type": "boolean" + "password": { + "description": "Password from Theme Access app (overrides environment).", + "env": "SHOPIFY_FLAG_PASSWORD", + "name": "password", + "hasDynamicHelp": false, + "multiple": false, + "type": "option" } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "theme:share", - "multiEnvironmentsFlags": [ - "store", - "password", - "path" - ], + "hidden": true, + "hiddenAliases": [], + "id": "doctor-release:theme", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", "strict": true, - "summary": "Creates a shareable, unpublished, and new theme on your theme library with a randomized name." + "enableJsonFlag": false }, - "upgrade": { - "aliases": [ - ], - "args": { - }, - "description": "Upgrades Shopify CLI using your package manager.", - "descriptionWithMarkdown": "Upgrades Shopify CLI using your package manager.", - "enableJsonFlag": false, + "docs:generate": { + "aliases": [], + "args": {}, + "description": "Generate CLI commands documentation", + "flags": {}, + "hasDynamicHelp": false, + "hidden": true, + "hiddenAliases": [], + "id": "docs:generate", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true, + "enableJsonFlag": false + }, + "notifications:list": { + "aliases": [], + "args": {}, + "description": "List current notifications configured for the CLI.", "flags": { + "ignore-errors": { + "description": "Don't fail if an error occurs.", + "env": "SHOPIFY_FLAG_IGNORE_ERRORS", + "hidden": false, + "name": "ignore-errors", + "allowNo": false, + "type": "boolean" + } }, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "upgrade", + "hidden": true, + "hiddenAliases": [], + "id": "notifications:list", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", "strict": true, - "summary": "Upgrades Shopify CLI." + "enableJsonFlag": false }, - "version": { - "aliases": [ - ], - "args": { - }, - "description": "Shopify CLI version currently installed.", + "notifications:generate": { + "aliases": [], + "args": {}, + "description": "Generate a notifications.json file for the the CLI, appending a new notification to the current file.", + "flags": {}, + "hasDynamicHelp": false, + "hidden": true, + "hiddenAliases": [], + "id": "notifications:generate", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true, + "enableJsonFlag": false + }, + "cache:clear": { + "aliases": [], + "args": {}, + "description": "Clear the CLI cache, used to store some API responses and handle notifications status", + "flags": {}, + "hasDynamicHelp": false, + "hidden": true, + "hiddenAliases": [], + "id": "cache:clear", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true, + "enableJsonFlag": false + }, + "config:autoupgrade:off": { + "aliases": [], + "args": {}, + "description": "Disable automatic upgrades for Shopify CLI.\n\n When auto-upgrade is disabled, Shopify CLI won't automatically update. Run `shopify upgrade` to update manually.\n\n To enable auto-upgrade, run `shopify config autoupgrade on`.\n", + "flags": {}, + "hasDynamicHelp": false, + "hiddenAliases": [], + "id": "config:autoupgrade:off", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true, + "summary": "Disable automatic upgrades for Shopify CLI.", "enableJsonFlag": false, - "flags": { - }, + "descriptionWithMarkdown": "Disable automatic upgrades for Shopify CLI.\n\n When auto-upgrade is disabled, Shopify CLI won't automatically update. Run `shopify upgrade` to update manually.\n\n To enable auto-upgrade, run `shopify config autoupgrade on`.\n" + }, + "config:autoupgrade:on": { + "aliases": [], + "args": {}, + "description": "Enable automatic upgrades for Shopify CLI.\n\n When auto-upgrade is enabled, Shopify CLI automatically updates to the latest version once per day. Major version upgrades are skipped and must be done manually.\n\n To disable auto-upgrade, run `shopify config autoupgrade off`.\n", + "flags": {}, "hasDynamicHelp": false, - "hiddenAliases": [ - ], - "id": "version", + "hiddenAliases": [], + "id": "config:autoupgrade:on", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true, + "summary": "Enable automatic upgrades for Shopify CLI.", + "enableJsonFlag": false, + "descriptionWithMarkdown": "Enable automatic upgrades for Shopify CLI.\n\n When auto-upgrade is enabled, Shopify CLI automatically updates to the latest version once per day. Major version upgrades are skipped and must be done manually.\n\n To disable auto-upgrade, run `shopify config autoupgrade off`.\n" + }, + "config:autoupgrade:status": { + "aliases": [], + "args": {}, + "description": "Check whether auto-upgrade is enabled, disabled, or not yet configured.\n\n When auto-upgrade is enabled, Shopify CLI automatically updates to the latest version after each command.\n\n Run `shopify config autoupgrade on` or `shopify config autoupgrade off` to configure it.\n", + "flags": {}, + "hasDynamicHelp": false, + "hiddenAliases": [], + "id": "config:autoupgrade:status", "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", - "strict": true + "strict": true, + "summary": "Check whether auto-upgrade is enabled, disabled, or not yet configured.", + "enableJsonFlag": false, + "descriptionWithMarkdown": "Check whether auto-upgrade is enabled, disabled, or not yet configured.\n\n When auto-upgrade is enabled, Shopify CLI automatically updates to the latest version after each command.\n\n Run `shopify config autoupgrade on` or `shopify config autoupgrade off` to configure it.\n" } }, - "version": "4.3.0" + "version": "3.94.0" } \ No newline at end of file diff --git a/packages/flow-cli/.gitignore b/packages/flow-cli/.gitignore new file mode 100644 index 00000000000..85f3285575e --- /dev/null +++ b/packages/flow-cli/.gitignore @@ -0,0 +1,2 @@ +bundle/ +flow.mjs diff --git a/packages/flow-cli/bin/bundle.js b/packages/flow-cli/bin/bundle.js new file mode 100644 index 00000000000..17b31bef41b --- /dev/null +++ b/packages/flow-cli/bin/bundle.js @@ -0,0 +1,33 @@ +import {build} from 'esbuild' +import {copy} from 'esbuild-plugin-copy' +import glob from 'fast-glob' +import {fileURLToPath} from 'url' +import {dirname, resolve} from 'path' +import ShopifyStacktraceyPlugin from '../../../bin/bundling/esbuild-plugin-stacktracey.js' + +const __dirname = dirname(fileURLToPath(import.meta.url)) +const repoRoot = resolve(__dirname, '../../..') + +const wasmTomlPatchFile = glob.sync(`${repoRoot}/node_modules/.pnpm/**/toml_patch_bg.wasm`)[0] + +await build({ + bundle: true, + entryPoints: [{in: './src/main.ts', out: 'main'}], + outdir: './bundle', + platform: 'node', + format: 'esm', + splitting: true, + treeShaking: true, + minifyWhitespace: true, + minifySyntax: true, + minifyIdentifiers: true, + inject: [resolve(repoRoot, 'bin/bundling/cjs-shims.js')], + external: ['react-devtools-core'], + plugins: [ + ShopifyStacktraceyPlugin, + copy({ + resolveFrom: 'cwd', + assets: [{from: [wasmTomlPatchFile], to: ['./bundle/']}], + }), + ], +}) diff --git a/packages/flow-cli/bin/run.js b/packages/flow-cli/bin/run.js new file mode 100644 index 00000000000..c6ea53ccf87 --- /dev/null +++ b/packages/flow-cli/bin/run.js @@ -0,0 +1,11 @@ +#!/usr/bin/env node + +// eslint-disable-next-line n/no-unsupported-features/node-builtins +import {enableCompileCache} from 'node:module' + +if (enableCompileCache) enableCompileCache() + +process.removeAllListeners('warning') + +const {default: runCLI} = await import('../dist/bootstrap.js') +runCLI({development: false}) diff --git a/packages/flow-cli/package.json b/packages/flow-cli/package.json new file mode 100644 index 00000000000..96840ab601a --- /dev/null +++ b/packages/flow-cli/package.json @@ -0,0 +1,52 @@ +{ + "name": "@shopify/flow-cli", + "version": "0.0.1", + "description": "Standalone Shopify Flow CLI", + "private": true, + "type": "module", + "bin": { + "flow": "./dist/main.js" + }, + "exports": { + ".": "./dist/index.js" + }, + "files": [ + "dist" + ], + "oclif": { + "bin": "flow", + "commands": { + "strategy": "explicit", + "target": "./dist/index.js", + "identifier": "COMMANDS" + }, + "topics": { + "workflow": { + "description": "Manage Flow workflows" + } + } + }, + "dependencies": { + "@oclif/core": "4.8.3", + "@shopify/cli-kit": "4.3.0" + }, + "devDependencies": { + "@rollup/plugin-alias": "^6.0.0", + "@rollup/plugin-commonjs": "^29.0.3", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^16.0.3", + "esbuild": "0.28.1", + "esbuild-plugin-copy": "^2.1.1", + "fast-glob": "3.3.3", + "rollup": "^3.29.5", + "typescript": "5.8.2" + }, + "scripts": { + "build": "tsc -b", + "bundle": "rollup --config rollup.config.mjs", + "clean": "rm -rf dist bundle" + }, + "engines": { + "node": ">=18.20.0" + } +} diff --git a/packages/flow-cli/rollup.config.mjs b/packages/flow-cli/rollup.config.mjs new file mode 100644 index 00000000000..c4a91ec6fec --- /dev/null +++ b/packages/flow-cli/rollup.config.mjs @@ -0,0 +1,60 @@ +import {nodeResolve} from '@rollup/plugin-node-resolve' +import commonjs from '@rollup/plugin-commonjs' +import json from '@rollup/plugin-json' +import alias from '@rollup/plugin-alias' +import {fileURLToPath} from 'url' +import {dirname, resolve} from 'path' + +const __dirname = dirname(fileURLToPath(import.meta.url)) + +// Fix from the Shopify CLI bundler: StackTracey uses a variable called +// `nodeRequire` (assigned to the bundler's sentinel) to avoid tree-shaking. +// Rewrite it to module.require() so it calls the real Node.js require. +const stacktraceyPlugin = { + name: 'fix-stacktracey', + transform(code, id) { + if (id.includes('stacktracey')) { + return {code: code.replaceAll('nodeRequire (', 'require(')} + } + }, +} + +export default { + input: 'dist/main.js', + output: { + file: 'flow.mjs', + format: 'esm', + inlineDynamicImports: true, + // Fix from the Shopify CLI bundler: inject CJS shims so __dirname, + // __filename, and require are available to bundled CJS modules. + banner: [ + "import { createRequire as __cjsRequire } from 'module';", + "import { fileURLToPath as __fup } from 'url';", + "import { dirname as __dn } from 'path';", + 'const require = __cjsRequire(import.meta.url);', + 'const __filename = __fup(import.meta.url);', + 'const __dirname = __dn(__filename);', + ].join('\n'), + }, + external: [/^node:/, 'fs', 'fs/promises', 'path', 'os', 'url', 'module', 'stream', 'events', + 'crypto', 'http', 'https', 'net', 'tty', 'util', 'child_process', 'worker_threads', + 'readline', 'buffer', 'string_decoder', 'assert', 'perf_hooks', 'v8', 'vm', 'zlib', + 'timers', 'timers/promises'], + plugins: [ + alias({ + entries: [ + {find: 'react-devtools-core', replacement: resolve(__dirname, 'src/stubs/react-devtools-core.mjs')}, + {find: '@shopify/toml-patch', replacement: resolve(__dirname, 'src/stubs/toml-patch.cjs')}, + ], + }), + json(), + nodeResolve({preferBuiltins: true, exportConditions: ['node', 'import', 'require', 'default']}), + commonjs({transformMixedEsModules: true, ignoreDynamicRequires: true}), + stacktraceyPlugin, + ], + onwarn(warning, warn) { + if (warning.code === 'CIRCULAR_DEPENDENCY') return + if (warning.code === 'THIS_IS_UNDEFINED') return + warn(warning) + }, +} diff --git a/packages/flow-cli/src/bootstrap.ts b/packages/flow-cli/src/bootstrap.ts new file mode 100644 index 00000000000..7bedf4be46c --- /dev/null +++ b/packages/flow-cli/src/bootstrap.ts @@ -0,0 +1,36 @@ +import {runCLI} from '@shopify/cli-kit/node/cli' +import fs from 'fs' + +// eslint-disable-next-line @typescript-eslint/no-misused-promises +process.on('uncaughtException', async (err) => { + try { + const {FatalError} = await import('@shopify/cli-kit/node/error') + if (err instanceof FatalError) { + const {renderFatalError} = await import('@shopify/cli-kit/node/ui') + renderFatalError(err) + } else { + fs.writeSync(process.stderr.fd, `${err.stack ?? err.message ?? err}\n`) + } + // eslint-disable-next-line no-catch-all/no-catch-all + } catch { + fs.writeSync(process.stderr.fd, `${err.stack ?? err.message ?? err}\n`) + } + process.exit(1) +}) + +for (const signal of ['SIGINT', 'SIGTERM', 'SIGQUIT'] as const) { + process.on(signal, () => process.exit(1)) +} + +interface RunOptions { + development: boolean +} + +async function runFlowCLI({development}: RunOptions) { + await runCLI({ + moduleURL: import.meta.url, + development, + }) +} + +export default runFlowCLI diff --git a/packages/flow-cli/src/cli/commands/env/search.ts b/packages/flow-cli/src/cli/commands/env/search.ts new file mode 100644 index 00000000000..2523e3d8189 --- /dev/null +++ b/packages/flow-cli/src/cli/commands/env/search.ts @@ -0,0 +1,87 @@ +import {loadConfig} from '../../services/flow/project-config.js' +import {dispatchFlowTool, unwrapJsonResult} from '../../services/flow/dispatch.js' +import Command from '@shopify/cli-kit/node/base-command' +import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli' +import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn' +import {AbortError} from '@shopify/cli-kit/node/error' +import {outputResult} from '@shopify/cli-kit/node/output' +import {Args, Flags} from '@oclif/core' + +const DATA_TYPE_FILTERS = ['LIST', 'ALL'] as const +type DataTypeFilter = (typeof DATA_TYPE_FILTERS)[number] + +export default class FlowEnvSearch extends Command { + static summary = 'Discover Flow environment field paths.' + + static descriptionWithMarkdown = `Searches the Flow environment for field paths under a given Admin API root type. Use this before writing conditions, Liquid, or task config that reference \`{{ order.customer.email }}\`-style paths. + +Pass a single root_type and search term as positional args. Use \`--workflow-id\` to pin the search to a specific workflow's Admin API version.` + + static description = this.descriptionWithoutMarkdown() + + static examples = [ + '<%= config.bin %> <%= command.id %> Order "customer email"', + '<%= config.bin %> <%= command.id %> Product tags --data-type LIST', + ] + + static args = { + 'root-type': Args.string({ + description: "The Admin API type name to search within (e.g. 'Order', 'Product', 'Customer').", + required: true, + }), + 'search-term': Args.string({ + description: "The search term to look for (e.g. 'email', 'customer name of order').", + required: true, + }), + } + + static flags = { + ...globalFlags, + ...jsonFlag, + store: Flags.string({ + char: 's', + description: 'The myshopify.com domain. Falls back to flow.toml.', + env: 'SHOPIFY_FLAG_STORE', + parse: async (input) => normalizeStoreFqdn(input), + }), + 'data-type': Flags.string({ + description: "Filter by data type. 'LIST' for array paths only, 'ALL' for all scalar-ending types.", + options: [...DATA_TYPE_FILTERS], + default: 'ALL', + env: 'SHOPIFY_FLAG_FLOW_DATA_TYPE', + }), + 'workflow-id': Flags.string({ + description: "Pin the search to a workflow's Admin API version.", + env: 'SHOPIFY_FLAG_FLOW_WORKFLOW_ID', + }), + 'workflow-version': Flags.string({ + description: 'Workflow definition version (paired with --workflow-id).', + env: 'SHOPIFY_FLAG_FLOW_WORKFLOW_VERSION', + }), + } + + public async run(): Promise { + const {args, flags} = await this.parse(FlowEnvSearch) + + const store = flags.store ?? (await loadConfig())?.store + if (!store) { + throw new AbortError('No store. Pass --store, or run `flow init` to create a flow.toml.') + } + + const toolArgs: Record = { + searches: [{root_type: args['root-type'], search_term: args['search-term']}], + data_type_filter: flags['data-type'] as DataTypeFilter, + } + if (flags['workflow-id']) toolArgs.workflow_id = flags['workflow-id'] + if (flags['workflow-version']) toolArgs.workflow_version = flags['workflow-version'] + + const response = await dispatchFlowTool({ + name: 'flow_app_agent_environment_paths_search', + source: 'flow', + store, + args: toolArgs, + }) + + outputResult(JSON.stringify(unwrapJsonResult(response), null, 2)) + } +} diff --git a/packages/flow-cli/src/cli/commands/init.ts b/packages/flow-cli/src/cli/commands/init.ts new file mode 100644 index 00000000000..f8f6f9b84e6 --- /dev/null +++ b/packages/flow-cli/src/cli/commands/init.ts @@ -0,0 +1,67 @@ +import {writeConfig} from '../services/flow/project-config.js' +import Command from '@shopify/cli-kit/node/base-command' +import {globalFlags} from '@shopify/cli-kit/node/cli' +import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn' +import {mkdir} from '@shopify/cli-kit/node/fs' +import {outputResult} from '@shopify/cli-kit/node/output' +import {cwd, resolvePath} from '@shopify/cli-kit/node/path' +import {Args, Flags} from '@oclif/core' + +export default class FlowInit extends Command { + static summary = 'Initialize a Flow IaC project (creates the directory and writes flow.toml).' + + static descriptionWithMarkdown = `Writes a \`flow.toml\` with the store handle and workflow directory. Pass a path to create (or use) a project directory; defaults to the current directory. + +Lifecycle commands (\`workflow validate/push/pull/diff/activate/deactivate/status\`) read this file when their --store and --workflows-dir flags aren't passed. Refuses to overwrite an existing file unless --force is set.` + + static description = this.descriptionWithoutMarkdown() + + static examples = [ + '<%= config.bin %> <%= command.id %> --store shop.myshopify.com', + '<%= config.bin %> <%= command.id %> ./shop1 --store shop.myshopify.com', + '<%= config.bin %> <%= command.id %> ./shop1 --store shop.myshopify.com --workflows-dir flows --force', + ] + + static args = { + path: Args.string({ + description: 'Project directory to create (or reuse). Defaults to the current directory.', + required: false, + }), + } + + static flags = { + ...globalFlags, + store: Flags.string({ + char: 's', + description: 'The myshopify.com domain of the store this project tracks.', + env: 'SHOPIFY_FLAG_STORE', + parse: async (input) => normalizeStoreFqdn(input), + required: true, + }), + 'workflows-dir': Flags.string({ + description: 'Directory (relative to flow.toml) where workflow JSON files live. Defaults to `workflows`.', + env: 'SHOPIFY_FLAG_FLOW_WORKFLOWS_DIR', + }), + force: Flags.boolean({ + description: 'Overwrite an existing flow.toml.', + default: false, + env: 'SHOPIFY_FLAG_FLOW_FORCE', + }), + } + + public async run(): Promise { + const {args, flags} = await this.parse(FlowInit) + + const dir = args.path ? resolvePath(cwd(), args.path) : cwd() + if (args.path) await mkdir(dir) + + const path = await writeConfig({ + dir, + store: flags.store, + workflowsDir: flags['workflows-dir'], + force: flags.force, + }) + + outputResult(`Wrote ${path}.`) + } +} diff --git a/packages/flow-cli/src/cli/commands/resource/search.ts b/packages/flow-cli/src/cli/commands/resource/search.ts new file mode 100644 index 00000000000..b583e90d6e6 --- /dev/null +++ b/packages/flow-cli/src/cli/commands/resource/search.ts @@ -0,0 +1,98 @@ +import {loadConfig} from '../../services/flow/project-config.js' +import {dispatchFlowTool} from '../../services/flow/dispatch.js' +import Command from '@shopify/cli-kit/node/base-command' +import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli' +import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn' +import {AbortError} from '@shopify/cli-kit/node/error' +import {outputResult} from '@shopify/cli-kit/node/output' +import {Args, Flags} from '@oclif/core' + +const RESOURCE_TYPES = [ + 'PRODUCT', + 'PRODUCT_VARIANT', + 'CUSTOMER', + 'SEGMENT', + 'COLLECTION', + 'ORDER', + 'DRAFT_ORDER', + 'APP_INSTALLATION', + 'ARTICLE', + 'BLOG', + 'PAGE', + 'DISCOUNT', + 'LOCATION', + 'MARKETING_AUTOMATION', + 'METAOBJECT_DEFINITION', + 'METAOBJECT', + 'COMPANY', + 'COMPANY_LOCATION', + 'PRODUCT_NETWORK_ORDER', +] as const + +export default class FlowResourceSearch extends Command { + static summary = 'Search for a Shopify resource by query (Sidekick-routed).' + + static descriptionWithMarkdown = `Searches the Admin API for a specific resource type. Returns title, GID, and path. \`PRODUCT_VARIANT\` also returns inventory_item_id; \`LOCATION\` also returns inventory_group_id and inventory_group_name. + +Routes through Sidekick — uses the \`shop.admin.graphql\` Identity scope, not Flow's own scope.` + + static description = this.descriptionWithoutMarkdown() + + static examples = [ + '<%= config.bin %> <%= command.id %> PRODUCT shoes', + '<%= config.bin %> <%= command.id %> CUSTOMER "" --limit 5', + ] + + static args = { + type: Args.string({ + description: 'Resource type to search.', + required: true, + options: [...RESOURCE_TYPES], + }), + query: Args.string({ + description: 'Query string. Wildcards (* or ?) are not supported. Empty string returns up to --limit.', + required: true, + }), + } + + static flags = { + ...globalFlags, + ...jsonFlag, + store: Flags.string({ + char: 's', + description: 'The myshopify.com domain. Falls back to flow.toml.', + env: 'SHOPIFY_FLAG_STORE', + parse: async (input) => normalizeStoreFqdn(input), + }), + limit: Flags.integer({ + description: 'Number of resources to return (default 10, max 50).', + min: 1, + max: 50, + env: 'SHOPIFY_FLAG_FLOW_LIMIT', + }), + } + + public async run(): Promise { + const {args, flags} = await this.parse(FlowResourceSearch) + + const store = flags.store ?? (await loadConfig())?.store + if (!store) { + throw new AbortError('No store. Pass --store, or run `flow init` to create a flow.toml.') + } + + const toolArgs: Record = { + resource_type: args.type, + query: args.query, + } + if (flags.limit !== undefined) toolArgs.limit = flags.limit + + const result = await dispatchFlowTool({ + name: 'flow_app_agent_search_shop_resource', + source: 'sk', + store, + args: toolArgs, + }) + + outputResult(JSON.stringify(result, null, 2)) + } +} diff --git a/packages/flow-cli/src/cli/commands/shopifyql/columns.ts b/packages/flow-cli/src/cli/commands/shopifyql/columns.ts new file mode 100644 index 00000000000..35a58f54a3b --- /dev/null +++ b/packages/flow-cli/src/cli/commands/shopifyql/columns.ts @@ -0,0 +1,52 @@ +import {loadConfig} from '../../services/flow/project-config.js' +import {dispatchFlowTool, unwrapJsonResult} from '../../services/flow/dispatch.js' +import Command from '@shopify/cli-kit/node/base-command' +import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli' +import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn' +import {AbortError} from '@shopify/cli-kit/node/error' +import {outputResult} from '@shopify/cli-kit/node/output' +import {Args, Flags} from '@oclif/core' + +export default class FlowShopifyqlColumns extends Command { + static summary = 'Resolve the columns a ShopifyQL query produces.' + + static descriptionWithMarkdown = + 'Given a ShopifyQL query string, returns the column names + types the query will produce. These are the fields Flow would add to the environment when the query runs.' + + static description = this.descriptionWithoutMarkdown() + + static examples = ['<%= config.bin %> <%= command.id %> "FROM sales SHOW gross_sales BY product_title SINCE -7d"'] + + static args = { + query: Args.string({description: 'A ShopifyQL query string.', required: true}), + } + + static flags = { + ...globalFlags, + ...jsonFlag, + store: Flags.string({ + char: 's', + description: 'The myshopify.com domain. Falls back to flow.toml.', + env: 'SHOPIFY_FLAG_STORE', + parse: async (input) => normalizeStoreFqdn(input), + }), + } + + public async run(): Promise { + const {args, flags} = await this.parse(FlowShopifyqlColumns) + + const store = flags.store ?? (await loadConfig())?.store + if (!store) { + throw new AbortError('No store. Pass --store, or run `flow init` to create a flow.toml.') + } + + const response = await dispatchFlowTool({ + name: 'flow_app_agent_shopifyql_query_fields', + source: 'flow', + store, + args: {query: args.query}, + }) + + outputResult(JSON.stringify(unwrapJsonResult(response), null, 2)) + } +} diff --git a/packages/flow-cli/src/cli/commands/task/describe.ts b/packages/flow-cli/src/cli/commands/task/describe.ts new file mode 100644 index 00000000000..cb86942773c --- /dev/null +++ b/packages/flow-cli/src/cli/commands/task/describe.ts @@ -0,0 +1,160 @@ +import {loadConfig} from '../../services/flow/project-config.js' +import {dispatchFlowTool, unwrapJsonResult} from '../../services/flow/dispatch.js' +import Command from '@shopify/cli-kit/node/base-command' +import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli' +import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn' +import {AbortError} from '@shopify/cli-kit/node/error' +import {outputResult} from '@shopify/cli-kit/node/output' +import {Args, Flags} from '@oclif/core' + +interface TaskRef { + id: string + version: string +} + +interface TaskConfiguration { + id: string + version: string + task_type: string + input_port_id?: string + output_port_ids?: string[] + config_fields: unknown[] + return_fields: unknown[] + config_instructions?: Record + error?: string +} + +interface TaskConfigurationResponse { + tasks: TaskConfiguration[] +} + +function parseTaskRef(input: string): TaskRef { + const at = input.lastIndexOf('@') + if (at === -1) { + throw new AbortError( + `Invalid task reference "${input}".`, + 'Use the form `@`, e.g. `shopify::admin::order_created@0.1`.', + ) + } + const id = input.slice(0, at) + const version = input.slice(at + 1) + if (!id || !version) { + throw new AbortError(`Invalid task reference "${input}".`, 'Both id and version are required.') + } + return {id, version} +} + +interface ConfigField { + id?: string + label?: string + description?: string + default_value?: string + validations?: {id?: string; options?: Record; error_message?: string}[] + possible_object_types?: string[] + supports_liquid?: boolean +} + +function formatHuman(tasks: TaskConfiguration[]): string { + return tasks.map(formatTask).join('\n\n---\n\n') +} + +function formatTask(task: TaskConfiguration): string { + const lines: string[] = [] + lines.push(`${task.id}@${task.version} (${task.task_type})`) + if (task.error) { + lines.push(` error: ${task.error}`) + return lines.join('\n') + } + if (task.input_port_id) lines.push(` input port: ${task.input_port_id}`) + if (task.output_port_ids?.length) lines.push(` output ports: ${task.output_port_ids.join(', ')}`) + + if (task.config_fields.length === 0) { + lines.push(' config fields: (none)') + } else { + lines.push(' config fields:') + for (const field of task.config_fields as ConfigField[]) { + const wrapHint = needsValueWrapper(field) ? ' [wrap value: {"value": "...", "default_value": ""}]' : '' + const liquidHint = field.supports_liquid ? ' [supports Liquid]' : '' + lines.push(` - ${field.id ?? '?'}${field.label ? ` — ${field.label}` : ''}${wrapHint}${liquidHint}`) + if (field.description) lines.push(` ${field.description}`) + } + } + + if (task.return_fields.length === 0) { + lines.push(' return fields: (none)') + } else { + lines.push(` return fields: ${task.return_fields.length} (use --json for full schema)`) + } + + return lines.join('\n') +} + +function needsValueWrapper(field: ConfigField): boolean { + return Boolean( + field.validations?.some((validation) => (validation.options as {subfield?: string})?.subfield === 'value'), + ) +} + +export default class FlowTaskDescribe extends Command { + static summary = 'Describe one or more Flow tasks: configuration and return schemas.' + + static descriptionWithMarkdown = `Returns the full configuration schema and return-field definitions for one or more Flow tasks. + +Each task is identified as \`@\` (e.g. \`shopify::admin::order_created@0.1\`).` + + static description = this.descriptionWithoutMarkdown() + + static examples = [ + '<%= config.bin %> <%= command.id %> shopify::admin::order_created@0.1', + '<%= config.bin %> <%= command.id %> shopify::admin::order_created@0.1 shopify::admin::add_order_tags@0.1', + ] + + static args = { + task: Args.string({ + description: 'Task reference as `@` (one or more).', + required: true, + }), + } + + static strict = false + + static flags = { + ...globalFlags, + ...jsonFlag, + store: Flags.string({ + char: 's', + description: 'The myshopify.com domain. Falls back to flow.toml.', + env: 'SHOPIFY_FLAG_STORE', + parse: async (input) => normalizeStoreFqdn(input), + }), + } + + public async run(): Promise { + const {argv, flags} = await this.parse(FlowTaskDescribe) + const refs = (argv as string[]).map(parseTaskRef) + + if (refs.length === 0) throw new AbortError('At least one task reference is required.') + + const store = flags.store ?? (await loadConfig())?.store + if (!store) { + throw new AbortError('No store. Pass --store, or run `flow init` to create a flow.toml.') + } + + const response = await dispatchFlowTool({ + name: 'flow_app_agent_task_configuration', + source: 'flow', + store, + args: {tasks: refs}, + }) + + const raw = unwrapJsonResult(response) + const tasks = raw.tasks ?? [] + + if (flags.json) { + outputResult(JSON.stringify({tasks}, null, 2)) + return + } + + outputResult(formatHuman(tasks)) + } +} diff --git a/packages/flow-cli/src/cli/commands/task/search.ts b/packages/flow-cli/src/cli/commands/task/search.ts new file mode 100644 index 00000000000..aa374ad5f28 --- /dev/null +++ b/packages/flow-cli/src/cli/commands/task/search.ts @@ -0,0 +1,152 @@ +import {loadConfig} from '../../services/flow/project-config.js' +import {dispatchFlowTool, unwrapJsonResult} from '../../services/flow/dispatch.js' +import Command from '@shopify/cli-kit/node/base-command' +import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli' +import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn' +import {AbortError} from '@shopify/cli-kit/node/error' +import {outputResult} from '@shopify/cli-kit/node/output' +import {Args, Flags} from '@oclif/core' + +const TASK_TYPES = ['trigger', 'action', 'condition', 'foreach', 'wait'] as const +type TaskTypeFilter = (typeof TASK_TYPES)[number] + +interface RawTask { + id: string + version: string + label: string + description?: string + task_type: string + connector_id?: string + installed?: boolean + publisher?: string +} + +interface RawTaskSearchResult { + tasks: Record +} + +interface FlatTask extends RawTask { + matched_queries: string[] +} + +interface SearchResult { + store: string + total: number + tasks: FlatTask[] + by_query: Record +} + +export default class FlowTaskSearch extends Command { + static summary = 'Search Flow tasks (triggers, conditions, actions, etc.).' + + static descriptionWithMarkdown = `Finds Flow tasks (triggers, conditions, actions, foreach, wait) by natural-language query. Pass each goal as a separate positional argument. + +Returns task references (id + version) you can pass to \`flow task describe\` to get the full configuration schema.` + + static description = this.descriptionWithoutMarkdown() + + static examples = [ + '<%= config.bin %> <%= command.id %> "order created trigger"', + '<%= config.bin %> <%= command.id %> "add order tags" "send email"', + '<%= config.bin %> <%= command.id %> "tagging" --type action', + ] + + static args = { + query: Args.string({description: 'Search query (one or more, space-separated).', required: true}), + } + + static strict = false + + static flags = { + ...globalFlags, + ...jsonFlag, + store: Flags.string({ + char: 's', + description: 'The myshopify.com domain. Falls back to flow.toml.', + env: 'SHOPIFY_FLAG_STORE', + parse: async (input) => normalizeStoreFqdn(input), + }), + type: Flags.string({ + description: 'Filter by task type.', + options: [...TASK_TYPES], + env: 'SHOPIFY_FLAG_FLOW_TASK_TYPE', + }), + } + + public async run(): Promise { + const {argv, flags} = await this.parse(FlowTaskSearch) + const queries = argv as string[] + + if (queries.length === 0) throw new AbortError('At least one search query is required.') + + const store = flags.store ?? (await loadConfig())?.store + if (!store) { + throw new AbortError('No store. Pass --store, or run `flow init` to create a flow.toml.') + } + + const response = await dispatchFlowTool({ + name: 'flow_app_agent_task_search', + source: 'flow', + store, + args: {search_queries: queries}, + }) + + const raw = unwrapJsonResult(response) + const result = shapeResult(store, raw, flags.type as TaskTypeFilter | undefined) + + if (flags.json) { + outputResult(JSON.stringify(result, null, 2)) + return + } + + outputResult(formatHuman(result)) + } +} + +function shapeResult(store: string, raw: RawTaskSearchResult, typeFilter: TaskTypeFilter | undefined): SearchResult { + const byId = new Map() + const byQuery: Record = {} + + const wantedType = typeFilter?.toUpperCase() + + for (const [query, tasks] of Object.entries(raw.tasks ?? {})) { + const filtered = wantedType ? tasks.filter((task) => task.task_type === wantedType) : tasks + byQuery[query] = filtered + + for (const task of filtered) { + const key = `${task.id}@${task.version}` + const existing = byId.get(key) + if (existing) { + if (!existing.matched_queries.includes(query)) existing.matched_queries.push(query) + } else { + byId.set(key, {...task, matched_queries: [query]}) + } + } + } + + const tasks = [...byId.values()].sort((left, right) => left.id.localeCompare(right.id)) + return {store, total: tasks.length, tasks, by_query: byQuery} +} + +function formatHuman(result: SearchResult): string { + if (result.tasks.length === 0) return `No tasks matched on ${result.store}.` + + const idWidth = Math.max(2, ...result.tasks.map((task) => `${task.id}@${task.version}`.length)) + const typeWidth = Math.max(4, ...result.tasks.map((task) => task.task_type.length)) + const pubWidth = Math.max(3, ...result.tasks.map((task) => (task.publisher ?? '?').length)) + const labelWidth = Math.max(5, ...result.tasks.map((task) => task.label.length)) + + const lines = [ + `${'TYPE'.padEnd(typeWidth)} ${'PUB'.padEnd(pubWidth)} ${'ID@VERSION'.padEnd(idWidth)} ${'LABEL'.padEnd( + labelWidth, + )}`, + ...result.tasks.map( + (task) => + `${task.task_type.padEnd(typeWidth)} ${(task.publisher ?? '?').padEnd(pubWidth)} ${`${task.id}@${task.version}`.padEnd( + idWidth, + )} ${task.label.padEnd(labelWidth)}`, + ), + ] + + return lines.join('\n') +} diff --git a/packages/flow-cli/src/cli/commands/template/save.ts b/packages/flow-cli/src/cli/commands/template/save.ts new file mode 100644 index 00000000000..ff5c5ffa83f --- /dev/null +++ b/packages/flow-cli/src/cli/commands/template/save.ts @@ -0,0 +1,134 @@ +import {dispatchFlowTool} from '../../services/flow/dispatch.js' +import {loadConfig, workflowsDirAbsolute} from '../../services/flow/project-config.js' +import {writeWorkflowFile, type WorkflowJson} from '../../services/flow/workflow-lifecycle.js' +import Command from '@shopify/cli-kit/node/base-command' +import {globalFlags} from '@shopify/cli-kit/node/cli' +import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn' +import {AbortError} from '@shopify/cli-kit/node/error' +import {fileExists, mkdir} from '@shopify/cli-kit/node/fs' +import {outputResult} from '@shopify/cli-kit/node/output' +import {cwd, joinPath, resolvePath} from '@shopify/cli-kit/node/path' +import {Args, Flags} from '@oclif/core' + +interface TemplateLookupResponseJson { + template_id?: string + title?: string + workflow_json?: WorkflowJson + error?: string + error_code?: string +} + +function unwrapTemplateLookupResult(response: unknown): TemplateLookupResponseJson { + const typed = response as {ok: boolean; data?: TemplateLookupResponseJson} | undefined + if (!typed?.data) throw new AbortError('Unexpected response shape from template_lookup.') + return typed.data +} + +export default class FlowTemplateSave extends Command { + static summary = 'Save a Flow template into your IaC project as a new workflow.' + + static descriptionWithMarkdown = `Fetches the template by ID (always the leading version) and writes its \`workflow_json\` to \`//workflow.flow.json\`. Sets \`root.workflow_name\` to the template's title — rename it before push if you want a different name. + +No lockfile is written; the file is "new" until you \`push\` it. + +Get a template ID from \`flow template search\` — each result carries a stable \`template_id\` field.` + + static description = this.descriptionWithoutMarkdown() + + static examples = [ + '<%= config.bin %> <%= command.id %> 01HQK000000000000000000000 --as fraud-prevention', + '<%= config.bin %> <%= command.id %> 01HQK... --as high-value-orders --force', + ] + + static args = { + 'template-id': Args.string({ + description: 'Template identifier from a prior `flow template search` result.', + required: true, + }), + } + + static flags = { + ...globalFlags, + store: Flags.string({ + char: 's', + description: 'The myshopify.com domain. Falls back to flow.toml.', + env: 'SHOPIFY_FLAG_STORE', + parse: async (input) => normalizeStoreFqdn(input), + }), + as: Flags.string({ + char: 'a', + description: 'Slug for the new workflow folder. Creates workflows//workflow.flow.json.', + required: true, + env: 'SHOPIFY_FLAG_FLOW_AS', + }), + 'workflows-dir': Flags.string({ + description: 'Workflows directory (relative to cwd). Falls back to flow.toml.', + env: 'SHOPIFY_FLAG_FLOW_WORKFLOWS_DIR', + }), + force: Flags.boolean({ + description: 'Overwrite an existing workflow.flow.json at the target path.', + default: false, + env: 'SHOPIFY_FLAG_FLOW_FORCE', + }), + } + + public async run(): Promise { + const {args, flags} = await this.parse(FlowTemplateSave) + + const config = await loadConfig() + const store = flags.store ?? config?.store + if (!store) { + throw new AbortError('No store. Pass --store, or run `flow init` to create a flow.toml.') + } + + let dir: string | undefined + if (flags['workflows-dir']) { + dir = resolvePath(cwd(), flags['workflows-dir']) + } else if (config) { + dir = workflowsDirAbsolute(config) + } + if (!dir) { + throw new AbortError('No workflows directory. Pass --workflows-dir, or run `flow init` to create a flow.toml.') + } + + const targetDir = joinPath(dir, flags.as) + const filePath = joinPath(targetDir, 'workflow.flow.json') + + if ((await fileExists(filePath)) && !flags.force) { + throw new AbortError(`${filePath} already exists.`, 'Pass --force to overwrite.') + } + + const response = await dispatchFlowTool({ + name: 'flow_app_agent_template_lookup', + source: 'flow', + store, + args: {template_id: args['template-id']}, + }) + + const json = unwrapTemplateLookupResult(response) + if (json.error) { + throw new AbortError(`Template lookup failed: ${json.error}`, json.error_code) + } + if (!json.workflow_json) { + throw new AbortError(`Template ${args['template-id']} has no workflow_json.`) + } + + const payload: WorkflowJson = json.workflow_json + const root = payload.root as Record | undefined + if (root && !root.workflow_name) { + root.workflow_name = json.title ?? flags.as + } + + await mkdir(targetDir) + await writeWorkflowFile(filePath, payload) + + const lines = [ + `Saved "${json.title ?? args['template-id']}" to ${filePath}.`, + '', + 'Next:', + ` flow workflow validate ${filePath}`, + ` flow workflow push ${filePath} # creates lockfile`, + ] + outputResult(lines.join('\n')) + } +} diff --git a/packages/flow-cli/src/cli/commands/template/search.ts b/packages/flow-cli/src/cli/commands/template/search.ts new file mode 100644 index 00000000000..dcf8969978b --- /dev/null +++ b/packages/flow-cli/src/cli/commands/template/search.ts @@ -0,0 +1,61 @@ +import {loadConfig} from '../../services/flow/project-config.js' +import {dispatchFlowTool, unwrapJsonResult} from '../../services/flow/dispatch.js' +import Command from '@shopify/cli-kit/node/base-command' +import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli' +import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn' +import {AbortError} from '@shopify/cli-kit/node/error' +import {outputResult} from '@shopify/cli-kit/node/output' +import {Args, Flags} from '@oclif/core' + +export default class FlowTemplateSearch extends Command { + static summary = 'Search Flow templates by business goal.' + + static descriptionWithMarkdown = `Searches Flow's template catalog for workflows matching one or more business goals. Pass each goal as a separate positional argument. + +The result is a JSON object keyed by query, with templates and their workflow_json structures. Use a returned template's workflow_json as the starting point for a new workflow.` + + static description = this.descriptionWithoutMarkdown() + + static examples = [ + '<%= config.bin %> <%= command.id %> "fraud prevention"', + '<%= config.bin %> <%= command.id %> "fraud prevention" "high risk orders"', + ] + + static args = { + query: Args.string({description: 'Search query (one or more, space-separated).', required: true}), + } + + static strict = false + + static flags = { + ...globalFlags, + ...jsonFlag, + store: Flags.string({ + char: 's', + description: 'The myshopify.com domain. Falls back to flow.toml.', + env: 'SHOPIFY_FLAG_STORE', + parse: async (input) => normalizeStoreFqdn(input), + }), + } + + public async run(): Promise { + const {argv, flags} = await this.parse(FlowTemplateSearch) + const queries = argv as string[] + + if (queries.length === 0) throw new AbortError('At least one search query is required.') + + const store = flags.store ?? (await loadConfig())?.store + if (!store) { + throw new AbortError('No store. Pass --store, or run `flow init` to create a flow.toml.') + } + + const response = await dispatchFlowTool({ + name: 'flow_app_agent_template_search', + source: 'flow', + store, + args: {search_queries: queries}, + }) + + outputResult(JSON.stringify(unwrapJsonResult(response), null, 2)) + } +} diff --git a/packages/flow-cli/src/cli/commands/type/show.ts b/packages/flow-cli/src/cli/commands/type/show.ts new file mode 100644 index 00000000000..acea3357ec2 --- /dev/null +++ b/packages/flow-cli/src/cli/commands/type/show.ts @@ -0,0 +1,67 @@ +import {loadConfig} from '../../services/flow/project-config.js' +import {dispatchFlowTool, unwrapJsonResult} from '../../services/flow/dispatch.js' +import Command from '@shopify/cli-kit/node/base-command' +import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli' +import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn' +import {AbortError} from '@shopify/cli-kit/node/error' +import {outputResult} from '@shopify/cli-kit/node/output' +import {Args, Flags} from '@oclif/core' + +export default class FlowTypeShow extends Command { + static summary = "Show a GraphQL type's structure (fields, arguments, return types)." + + static descriptionWithMarkdown = + 'Prints the field structure, argument signatures, and return types for a single Admin API GraphQL type. Use to understand the shape of a type before referencing its fields in workflow JSON.' + + static description = this.descriptionWithoutMarkdown() + + static examples = [ + '<%= config.bin %> <%= command.id %> Product', + '<%= config.bin %> <%= command.id %> Order --workflow-id 01HQK...', + ] + + static args = { + type: Args.string({description: 'The GraphQL type name to inspect.', required: true}), + } + + static flags = { + ...globalFlags, + ...jsonFlag, + store: Flags.string({ + char: 's', + description: 'The myshopify.com domain. Falls back to flow.toml.', + env: 'SHOPIFY_FLAG_STORE', + parse: async (input) => normalizeStoreFqdn(input), + }), + 'workflow-id': Flags.string({ + description: "Pin the lookup to a workflow's Admin API version.", + env: 'SHOPIFY_FLAG_FLOW_WORKFLOW_ID', + }), + 'workflow-version': Flags.string({ + description: 'Workflow definition version (paired with --workflow-id).', + env: 'SHOPIFY_FLAG_FLOW_WORKFLOW_VERSION', + }), + } + + public async run(): Promise { + const {args, flags} = await this.parse(FlowTypeShow) + + const store = flags.store ?? (await loadConfig())?.store + if (!store) { + throw new AbortError('No store. Pass --store, or run `flow init` to create a flow.toml.') + } + + const toolArgs: Record = {type_ref_name: args.type} + if (flags['workflow-id']) toolArgs.workflow_id = flags['workflow-id'] + if (flags['workflow-version']) toolArgs.workflow_version = flags['workflow-version'] + + const response = await dispatchFlowTool({ + name: 'flow_app_agent_object_type_definition_search', + source: 'flow', + store, + args: toolArgs, + }) + + outputResult(JSON.stringify(unwrapJsonResult(response), null, 2)) + } +} diff --git a/packages/flow-cli/src/cli/commands/workflow/activate.ts b/packages/flow-cli/src/cli/commands/workflow/activate.ts new file mode 100644 index 00000000000..c8bcbb2105b --- /dev/null +++ b/packages/flow-cli/src/cli/commands/workflow/activate.ts @@ -0,0 +1,92 @@ +import {loadConfig} from '../../services/flow/project-config.js' +import {activateWorkflow} from '../../services/flow/workflow-lifecycle.js' +import Command from '@shopify/cli-kit/node/base-command' +import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli' +import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn' +import {AbortError} from '@shopify/cli-kit/node/error' +import {outputResult} from '@shopify/cli-kit/node/output' +import {resolvePath} from '@shopify/cli-kit/node/path' +import {Args, Flags} from '@oclif/core' + +export default class FlowWorkflowActivate extends Command { + static summary = 'Activate a workflow on a shop.' + + static descriptionWithMarkdown = `Activates a workflow definition. Three input modes: + +- positional file path: read lockfile sibling for workflow_id and workflow_definition_version +- \`--workflow-id X --workflow-version Y\`: activate that exact pair +- \`--workflow-id X --use-latest\`: resolve the current main version via lookup, then activate + +Hidden workflows must be unhidden first (push with hidden:false).` + + static description = this.descriptionWithoutMarkdown() + + static examples = [ + '<%= config.bin %> <%= command.id %> ./workflow.flow.json --store shop.myshopify.com', + '<%= config.bin %> <%= command.id %> --workflow-id 01HQK... --workflow-version 01HQL... --store shop.myshopify.com', + '<%= config.bin %> <%= command.id %> --workflow-id 01HQK... --use-latest --store shop.myshopify.com', + ] + + static args = { + file: Args.string({ + description: 'Optional path to the workflow JSON file (lockfile drives activation).', + required: false, + parse: async (input) => resolvePath(input), + }), + } + + static flags = { + ...globalFlags, + ...jsonFlag, + store: Flags.string({ + char: 's', + description: 'The myshopify.com domain. Falls back to the `store` field in flow.toml.', + env: 'SHOPIFY_FLAG_STORE', + parse: async (input) => normalizeStoreFqdn(input), + }), + 'workflow-id': Flags.string({ + description: 'Workflow ID (escape hatch when no lockfile).', + env: 'SHOPIFY_FLAG_FLOW_WORKFLOW_ID', + }), + 'workflow-version': Flags.string({ + description: 'Workflow definition version. Required with --workflow-id unless --use-latest.', + env: 'SHOPIFY_FLAG_FLOW_WORKFLOW_VERSION', + }), + 'use-latest': Flags.boolean({ + description: 'Resolve the latest main version via workflow_lookup.', + default: false, + env: 'SHOPIFY_FLAG_FLOW_USE_LATEST', + }), + } + + public async run(): Promise { + const {args, flags} = await this.parse(FlowWorkflowActivate) + + if (flags['workflow-id'] && !flags['workflow-version'] && !flags['use-latest']) { + throw new AbortError('--workflow-id requires --workflow-version or --use-latest.') + } + + const store = flags.store ?? (await loadConfig())?.store + if (!store) { + throw new AbortError('No store. Pass --store, or run `flow init` to create a flow.toml.') + } + + const result = await activateWorkflow({ + filePath: args.file, + store, + workflowId: flags['workflow-id'], + workflowVersion: flags['workflow-version'], + useLatest: flags['use-latest'], + }) + + if (flags.json) { + outputResult(JSON.stringify(result, null, 2)) + } else if (result.error) { + outputResult(`Error: ${result.error}${result.error_code ? ` (${result.error_code})` : ''}`) + throw new AbortError('Workflow activation failed.') + } else { + const version = result.workflow_definition_version ?? result.workflow_version + outputResult(`Activated workflow ${result.workflow_id} (version ${version}).`) + } + } +} diff --git a/packages/flow-cli/src/cli/commands/workflow/deactivate.ts b/packages/flow-cli/src/cli/commands/workflow/deactivate.ts new file mode 100644 index 00000000000..1c7216113a4 --- /dev/null +++ b/packages/flow-cli/src/cli/commands/workflow/deactivate.ts @@ -0,0 +1,86 @@ +import {loadConfig} from '../../services/flow/project-config.js' +import {deactivateWorkflow} from '../../services/flow/workflow-lifecycle.js' +import Command from '@shopify/cli-kit/node/base-command' +import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli' +import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn' +import {AbortError} from '@shopify/cli-kit/node/error' +import {outputResult} from '@shopify/cli-kit/node/output' +import {resolvePath} from '@shopify/cli-kit/node/path' +import {Args, Flags} from '@oclif/core' + +export default class FlowWorkflowDeactivate extends Command { + static summary = 'Deactivate a workflow on a shop.' + + static descriptionWithMarkdown = + 'Deactivates a workflow definition. Same input modes as `activate`: positional file (lockfile), `--workflow-id` + `--workflow-version`, or `--workflow-id` + `--use-latest`.' + + static description = this.descriptionWithoutMarkdown() + + static examples = [ + '<%= config.bin %> <%= command.id %> ./workflow.flow.json --store shop.myshopify.com', + '<%= config.bin %> <%= command.id %> --workflow-id 01HQK... --workflow-version 01HQL... --store shop.myshopify.com', + ] + + static args = { + file: Args.string({ + description: 'Optional path to the workflow JSON file (lockfile drives deactivation).', + required: false, + parse: async (input) => resolvePath(input), + }), + } + + static flags = { + ...globalFlags, + ...jsonFlag, + store: Flags.string({ + char: 's', + description: 'The myshopify.com domain. Falls back to the `store` field in flow.toml.', + env: 'SHOPIFY_FLAG_STORE', + parse: async (input) => normalizeStoreFqdn(input), + }), + 'workflow-id': Flags.string({ + description: 'Workflow ID (escape hatch when no lockfile).', + env: 'SHOPIFY_FLAG_FLOW_WORKFLOW_ID', + }), + 'workflow-version': Flags.string({ + description: 'Workflow definition version. Required with --workflow-id unless --use-latest.', + env: 'SHOPIFY_FLAG_FLOW_WORKFLOW_VERSION', + }), + 'use-latest': Flags.boolean({ + description: 'Resolve the latest main version via workflow_lookup.', + default: false, + env: 'SHOPIFY_FLAG_FLOW_USE_LATEST', + }), + } + + public async run(): Promise { + const {args, flags} = await this.parse(FlowWorkflowDeactivate) + + if (flags['workflow-id'] && !flags['workflow-version'] && !flags['use-latest']) { + throw new AbortError('--workflow-id requires --workflow-version or --use-latest.') + } + + const store = flags.store ?? (await loadConfig())?.store + if (!store) { + throw new AbortError('No store. Pass --store, or run `flow init` to create a flow.toml.') + } + + const result = await deactivateWorkflow({ + filePath: args.file, + store, + workflowId: flags['workflow-id'], + workflowVersion: flags['workflow-version'], + useLatest: flags['use-latest'], + }) + + if (flags.json) { + outputResult(JSON.stringify(result, null, 2)) + } else if (result.error) { + outputResult(`Error: ${result.error}${result.error_code ? ` (${result.error_code})` : ''}`) + throw new AbortError('Workflow deactivation failed.') + } else { + const version = result.workflow_definition_version ?? result.workflow_version + outputResult(`Deactivated workflow ${result.workflow_id} (version ${version}).`) + } + } +} diff --git a/packages/flow-cli/src/cli/commands/workflow/diff.ts b/packages/flow-cli/src/cli/commands/workflow/diff.ts new file mode 100644 index 00000000000..73c319a752f --- /dev/null +++ b/packages/flow-cli/src/cli/commands/workflow/diff.ts @@ -0,0 +1,105 @@ +import {loadConfig} from '../../services/flow/project-config.js' +import { + diffWorkflow, + normalizeWorkflowJson, + readWorkflowFile, + unifiedDiff, +} from '../../services/flow/workflow-lifecycle.js' +import Command from '@shopify/cli-kit/node/base-command' +import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli' +import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn' +import {AbortError} from '@shopify/cli-kit/node/error' +import {outputResult} from '@shopify/cli-kit/node/output' +import {resolvePath} from '@shopify/cli-kit/node/path' +import {Args, Flags} from '@oclif/core' + +export default class FlowWorkflowDiff extends Command { + static summary = 'Show the diff between a local workflow file and the remote workflow.' + + static descriptionWithMarkdown = `Compares a local Flow workflow file against the remote workflow on the shop. Both sides are normalized (sorted keys, stable indent) so the diff reflects real changes, not formatting noise. + +Default direction is \`--- remote\` / \`+++ local\` so added lines = what \`push\` would change. + +Exit code: 0 if no differences, 1 if differences found. + +To inspect a remote workflow without a local file, use \`flow workflow show \`.` + + static description = this.descriptionWithoutMarkdown() + + static examples = [ + '<%= config.bin %> <%= command.id %> ./workflow.flow.json --store shop.myshopify.com', + '<%= config.bin %> <%= command.id %> ./workflow.flow.json --json', + ] + + static args = { + file: Args.string({ + description: 'Path to the workflow JSON file.', + required: true, + parse: async (input) => resolvePath(input), + }), + } + + static flags = { + ...globalFlags, + ...jsonFlag, + store: Flags.string({ + char: 's', + description: 'The myshopify.com domain. Falls back to the `store` field in flow.toml.', + env: 'SHOPIFY_FLAG_STORE', + parse: async (input) => normalizeStoreFqdn(input), + }), + 'workflow-id': Flags.string({ + description: 'Compare against a specific workflow ID instead of the lockfile.', + env: 'SHOPIFY_FLAG_FLOW_WORKFLOW_ID', + }), + 'workflow-version': Flags.string({ + description: 'Optional definition version override.', + env: 'SHOPIFY_FLAG_FLOW_WORKFLOW_VERSION', + }), + 'local-only': Flags.boolean({ + description: 'Print the normalized local workflow only (no diff).', + default: false, + env: 'SHOPIFY_FLAG_FLOW_LOCAL_ONLY', + }), + } + + public async run(): Promise { + const {args, flags} = await this.parse(FlowWorkflowDiff) + + if (flags['local-only']) { + const local = await readWorkflowFile(args.file) + outputResult(normalizeWorkflowJson(local)) + return + } + + const store = flags.store ?? (await loadConfig())?.store + if (!store) { + throw new AbortError('No store. Pass --store, or run `flow init` to create a flow.toml.') + } + + const result = await diffWorkflow({ + filePath: args.file, + store, + workflowId: flags['workflow-id'], + workflowVersion: flags['workflow-version'], + }) + + if (flags.json) { + outputResult( + JSON.stringify( + {changed: result.changed, local: result.localNormalized, remote: result.remoteNormalized}, + null, + 2, + ), + ) + } else if (result.changed) { + const remoteLabel = `remote: ${store}` + const localLabel = `local: ${args.file}` + outputResult(unifiedDiff(remoteLabel, result.remoteNormalized, localLabel, result.localNormalized)) + } + + if (result.changed) { + process.exitCode = 1 + } + } +} diff --git a/packages/flow-cli/src/cli/commands/workflow/list.ts b/packages/flow-cli/src/cli/commands/workflow/list.ts new file mode 100644 index 00000000000..e909b940b0a --- /dev/null +++ b/packages/flow-cli/src/cli/commands/workflow/list.ts @@ -0,0 +1,74 @@ +import {loadConfig} from '../../services/flow/project-config.js' +import {listAllWorkflows} from '../../services/flow/workflow-lifecycle.js' +import Command from '@shopify/cli-kit/node/base-command' +import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli' +import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn' +import {AbortError} from '@shopify/cli-kit/node/error' +import {outputResult} from '@shopify/cli-kit/node/output' +import {Flags} from '@oclif/core' + +export default class FlowWorkflowList extends Command { + static summary = 'List remote workflows on a shop.' + + static descriptionWithMarkdown = `Prints every workflow on the shop with id, name, hidden flag, and last-updated timestamp. Doesn't read or modify any local files. + +Use this to discover workflow IDs for \`flow workflow show \` or \`flow workflow pull --workflow-id \`. For a project-scoped view that also classifies local files, use \`flow workflow status\`.` + + static description = this.descriptionWithoutMarkdown() + + static examples = [ + '<%= config.bin %> <%= command.id %>', + '<%= config.bin %> <%= command.id %> --include-hidden --json', + ] + + static flags = { + ...globalFlags, + ...jsonFlag, + store: Flags.string({ + char: 's', + description: 'The myshopify.com domain. Falls back to flow.toml.', + env: 'SHOPIFY_FLAG_STORE', + parse: async (input) => normalizeStoreFqdn(input), + }), + 'include-hidden': Flags.boolean({ + description: 'Include hidden workflows.', + default: false, + env: 'SHOPIFY_FLAG_FLOW_INCLUDE_HIDDEN', + }), + } + + public async run(): Promise { + const {flags} = await this.parse(FlowWorkflowList) + + const store = flags.store ?? (await loadConfig())?.store + if (!store) { + throw new AbortError('No store. Pass --store, or run `flow init` to create a flow.toml.') + } + + const workflows = await listAllWorkflows({store, includeHidden: flags['include-hidden']}) + + if (flags.json) { + outputResult(JSON.stringify({store, total: workflows.length, workflows}, null, 2)) + return + } + + if (workflows.length === 0) { + outputResult(`No workflows on ${store}.`) + return + } + + const nameWidth = Math.max(4, ...workflows.map((workflow) => workflow.name.length)) + const idWidth = Math.max(2, ...workflows.map((workflow) => workflow.workflow_id.length)) + + const lines = [ + `${'NAME'.padEnd(nameWidth)} ${'ID'.padEnd(idWidth)} HIDDEN UPDATED`, + ...workflows.map( + (workflow) => + `${workflow.name.padEnd(nameWidth)} ${workflow.workflow_id.padEnd(idWidth)} ${ + workflow.hidden ? 'yes ' : 'no ' + } ${workflow.last_updated ?? '—'}`, + ), + ] + outputResult(lines.join('\n')) + } +} diff --git a/packages/flow-cli/src/cli/commands/workflow/preview.ts b/packages/flow-cli/src/cli/commands/workflow/preview.ts new file mode 100644 index 00000000000..8bd11680f84 --- /dev/null +++ b/packages/flow-cli/src/cli/commands/workflow/preview.ts @@ -0,0 +1,84 @@ +import {loadConfig} from '../../services/flow/project-config.js' +import {readWorkflowFile} from '../../services/flow/workflow-lifecycle.js' +import {authenticateFlowStore, throwFlowAuthExpired} from '../../services/flow/auth.js' +import Command from '@shopify/cli-kit/node/base-command' +import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli' +import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn' +import {AbortError} from '@shopify/cli-kit/node/error' +import {shopifyFetch} from '@shopify/cli-kit/node/http' +import {outputResult} from '@shopify/cli-kit/node/output' +import {resolvePath} from '@shopify/cli-kit/node/path' +import {Args, Flags} from '@oclif/core' + +const FLOW_PREVIEW_URL_PRODUCTION = 'https://flow.shopifycloud.com/flow-core/mcp_preview/transform' +const FLOW_PREVIEW_URL_LOCAL = 'https://flow.shop.dev/flow-core/mcp_preview/transform' + +function previewEndpoint(): string { + return process.env.SHOPIFY_SERVICE_ENV === 'local' ? FLOW_PREVIEW_URL_LOCAL : FLOW_PREVIEW_URL_PRODUCTION +} + +export default class FlowWorkflowPreview extends Command { + static summary = 'Transform a local workflow JSON file into the preview payload used by the MCP preview iframe.' + + static descriptionWithMarkdown = `Reads a local \`.flow.json\` file, calls the Flow MCP preview transform endpoint, and prints the resulting JSON. The output is the payload the \`/public/mcp-preview\` iframe expects via \`postMessage({type: "mcp-workflow-preview", workflow: })\`. + +This is the data path used by the Flow VSCode extension. The extension shells out to this command transparently when the user opens or saves a \`.flow.json\` file — no manual invocation required.` + + static description = this.descriptionWithoutMarkdown() + + static examples = ['<%= config.bin %> <%= command.id %> ./workflow.flow.json --store shop.myshopify.com'] + + static args = { + file: Args.string({ + description: 'Path to the workflow JSON file.', + required: true, + parse: async (input) => resolvePath(input), + }), + } + + static flags = { + ...globalFlags, + ...jsonFlag, + store: Flags.string({ + char: 's', + description: 'The myshopify.com domain. Falls back to the `store` field in flow.toml.', + env: 'SHOPIFY_FLAG_STORE', + parse: async (input) => normalizeStoreFqdn(input), + }), + } + + public async run(): Promise { + const {args, flags} = await this.parse(FlowWorkflowPreview) + + const store = flags.store ?? (await loadConfig())?.store + if (!store) { + throw new AbortError('No store. Pass --store, or run `flow init` to create a flow.toml.') + } + + const payload = await readWorkflowFile(args.file) + const session = await authenticateFlowStore(store) + + const response = await shopifyFetch( + previewEndpoint(), + { + method: 'POST', + headers: { + Authorization: `Bearer ${session.accessToken}`, + 'Content-Type': 'application/json', + 'X-Shopify-Shop-Domain': store, + }, + body: JSON.stringify(payload), + }, + 'slow-request', + ) + + if (response.status === 401) throwFlowAuthExpired(session) + + const text = await response.text() + if (!response.ok) { + throw new AbortError(`Preview transform failed (HTTP ${response.status}).`, text) + } + + outputResult(text) + } +} diff --git a/packages/flow-cli/src/cli/commands/workflow/pull.ts b/packages/flow-cli/src/cli/commands/workflow/pull.ts new file mode 100644 index 00000000000..9b4fd21d12c --- /dev/null +++ b/packages/flow-cli/src/cli/commands/workflow/pull.ts @@ -0,0 +1,151 @@ +import {loadConfig, workflowsDirAbsolute} from '../../services/flow/project-config.js' +import {pullAllWorkflows, pullWorkflow} from '../../services/flow/workflow-lifecycle.js' +import Command from '@shopify/cli-kit/node/base-command' +import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli' +import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn' +import {AbortError} from '@shopify/cli-kit/node/error' +import {outputResult} from '@shopify/cli-kit/node/output' +import {cwd, resolvePath} from '@shopify/cli-kit/node/path' +import {Flags} from '@oclif/core' + +export default class FlowWorkflowPull extends Command { + static summary = 'Pull a Flow workflow definition (or all of them) into local files with lockfiles.' + + static descriptionWithMarkdown = `Without \`--all\`: fetches a single workflow + leading definition from the shop and writes it to \`--out\` (normalized JSON). Sibling \`.flow.lock.json\` is also written. + +With \`--all\`: lists every workflow on the shop and writes one file per workflow into the project's workflows directory. Skips files that already exist unless \`--force\` is set. Requires \`flow.toml\` (run \`flow init\` first), or pass \`--workflows-dir\`.` + + static description = this.descriptionWithoutMarkdown() + + static examples = [ + '<%= config.bin %> <%= command.id %> --workflow-id 01HQK... --out ./workflow.flow.json --store shop.myshopify.com', + '<%= config.bin %> <%= command.id %> --all', + '<%= config.bin %> <%= command.id %> --all --include-hidden --force', + ] + + static flags = { + ...globalFlags, + ...jsonFlag, + store: Flags.string({ + char: 's', + description: 'The myshopify.com domain. Falls back to the `store` field in flow.toml.', + env: 'SHOPIFY_FLAG_STORE', + parse: async (input) => normalizeStoreFqdn(input), + }), + 'workflow-id': Flags.string({ + description: 'ID of a single workflow to pull. Mutually exclusive with --all.', + env: 'SHOPIFY_FLAG_FLOW_WORKFLOW_ID', + exclusive: ['all'], + }), + 'workflow-version': Flags.string({ + description: 'Optional definition version when pulling a single workflow. Defaults to leading.', + env: 'SHOPIFY_FLAG_FLOW_WORKFLOW_VERSION', + }), + out: Flags.string({ + description: 'Path to write a single workflow JSON. Required without --all.', + env: 'SHOPIFY_FLAG_FLOW_OUT', + parse: async (input) => resolvePath(input), + }), + all: Flags.boolean({ + description: 'Pull every workflow on the shop into the project workflows directory.', + default: false, + exclusive: ['workflow-id'], + env: 'SHOPIFY_FLAG_FLOW_ALL', + }), + 'workflows-dir': Flags.string({ + description: 'Directory to write workflows into when using --all. Falls back to flow.toml.', + env: 'SHOPIFY_FLAG_FLOW_WORKFLOWS_DIR', + }), + 'include-hidden': Flags.boolean({ + description: 'Include hidden workflows when pulling --all.', + default: false, + env: 'SHOPIFY_FLAG_FLOW_INCLUDE_HIDDEN', + }), + force: Flags.boolean({ + description: 'Overwrite existing files when pulling --all. (Single-pull always overwrites the --out path.)', + default: false, + env: 'SHOPIFY_FLAG_FLOW_FORCE', + }), + } + + public async run(): Promise { + const {flags} = await this.parse(FlowWorkflowPull) + + if (flags.all) { + await this.runPullAll(flags) + return + } + + if (!flags['workflow-id'] || !flags.out) { + throw new AbortError('Pull a single workflow with --workflow-id and --out, or pull every workflow with --all.') + } + + const store = flags.store ?? (await loadConfig())?.store + if (!store) { + throw new AbortError('No store. Pass --store, or run `flow init` to write a flow.toml.') + } + + const result = await pullWorkflow({ + workflowId: flags['workflow-id'], + workflowVersion: flags['workflow-version'], + outPath: flags.out, + store, + }) + + if (flags.json) { + outputResult(JSON.stringify(result, null, 2)) + } else { + outputResult( + `Pulled workflow ${result.workflowId} (version ${result.workflowDefinitionVersion}) to ${flags.out}.`, + ) + } + } + + private async runPullAll(flags: { + store?: string + 'workflows-dir'?: string + 'include-hidden': boolean + force: boolean + json?: boolean + }): Promise { + const config = await loadConfig() + const store = flags.store ?? config?.store + if (!store) { + throw new AbortError('--all requires a store. Pass --store or run `flow init` to create a flow.toml.') + } + + let dir: string | undefined + if (flags['workflows-dir']) { + dir = resolvePath(cwd(), flags['workflows-dir']) + } else if (config) { + dir = workflowsDirAbsolute(config) + } + if (!dir) { + throw new AbortError('--all requires a workflows directory. Pass --workflows-dir or run `flow init`.') + } + + const result = await pullAllWorkflows({ + store, + outDir: dir, + includeHidden: flags['include-hidden'], + force: flags.force, + }) + + if (flags.json) { + outputResult(JSON.stringify(result, null, 2)) + return + } + + const lines = [ + `Pulled ${result.pulled} of ${result.total} workflow(s) into ${dir}.`, + result.skipped > 0 ? `Skipped ${result.skipped} (file already exists; pass --force to overwrite).` : '', + '', + ...result.items.map((item) => + item.status === 'pulled' + ? ` + ${item.filePath} (${item.workflowId})` + : ` - ${item.filePath} (${item.workflowId}) — ${item.reason ?? 'skipped'}`, + ), + ].filter(Boolean) + outputResult(lines.join('\n')) + } +} diff --git a/packages/flow-cli/src/cli/commands/workflow/push.ts b/packages/flow-cli/src/cli/commands/workflow/push.ts new file mode 100644 index 00000000000..456e8fd0152 --- /dev/null +++ b/packages/flow-cli/src/cli/commands/workflow/push.ts @@ -0,0 +1,61 @@ +import {loadConfig} from '../../services/flow/project-config.js' +import {pushWorkflow} from '../../services/flow/workflow-lifecycle.js' +import Command from '@shopify/cli-kit/node/base-command' +import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli' +import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn' +import {AbortError} from '@shopify/cli-kit/node/error' +import {outputResult} from '@shopify/cli-kit/node/output' +import {resolvePath} from '@shopify/cli-kit/node/path' +import {Args, Flags} from '@oclif/core' + +export default class FlowWorkflowPush extends Command { + static summary = 'Push a Flow workflow JSON file to a shop and write a lockfile.' + + static descriptionWithMarkdown = + 'Creates or updates a workflow on the shop. Writes a sibling `.flow.lock.json` with the returned workflow_id, version, and payload SHA. Workflows are pushed `hidden: false` so they can be activated.' + + static description = this.descriptionWithoutMarkdown() + + static examples = ['<%= config.bin %> <%= command.id %> ./workflow.flow.json --store shop.myshopify.com'] + + static args = { + file: Args.string({ + description: 'Path to the workflow JSON file.', + required: true, + parse: async (input) => resolvePath(input), + }), + } + + static flags = { + ...globalFlags, + ...jsonFlag, + store: Flags.string({ + char: 's', + description: 'The myshopify.com domain. Falls back to the `store` field in flow.toml.', + env: 'SHOPIFY_FLAG_STORE', + parse: async (input) => normalizeStoreFqdn(input), + }), + } + + public async run(): Promise { + const {args, flags} = await this.parse(FlowWorkflowPush) + + const store = flags.store ?? (await loadConfig())?.store + if (!store) { + throw new AbortError('No store. Pass --store, or run `flow init` to create a flow.toml.') + } + + const result = await pushWorkflow({ + filePath: args.file, + store, + }) + + if (flags.json) { + outputResult(JSON.stringify(result, null, 2)) + } else { + outputResult( + `Pushed workflow ${result.workflowId} (version ${result.workflowDefinitionVersion}). Lockfile updated.`, + ) + } + } +} diff --git a/packages/flow-cli/src/cli/commands/workflow/show.ts b/packages/flow-cli/src/cli/commands/workflow/show.ts new file mode 100644 index 00000000000..528ed52b699 --- /dev/null +++ b/packages/flow-cli/src/cli/commands/workflow/show.ts @@ -0,0 +1,62 @@ +import {loadConfig} from '../../services/flow/project-config.js' +import {fetchRemoteWorkflow, normalizeWorkflowJson} from '../../services/flow/workflow-lifecycle.js' +import Command from '@shopify/cli-kit/node/base-command' +import {globalFlags} from '@shopify/cli-kit/node/cli' +import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn' +import {AbortError} from '@shopify/cli-kit/node/error' +import {outputResult} from '@shopify/cli-kit/node/output' +import {Args, Flags} from '@oclif/core' + +export default class FlowWorkflowShow extends Command { + static summary = 'Print a remote workflow definition as normalized JSON.' + + static descriptionWithMarkdown = `Fetches a workflow from the shop and prints its full definition (sorted keys, 2-space indent) to stdout. Doesn't write any files. Use this to inspect a remote workflow without committing to a local file. + +To save the result, redirect to a file. To pull and create a tracked local copy with a lockfile, use \`flow workflow pull\`.` + + static description = this.descriptionWithoutMarkdown() + + static examples = [ + '<%= config.bin %> <%= command.id %> 01HQK000000000000000000000', + '<%= config.bin %> <%= command.id %> 01HQK... --workflow-version 01HQL... --store shop.myshopify.com', + '<%= config.bin %> <%= command.id %> 01HQK... > /tmp/workflow.json', + ] + + static args = { + 'workflow-id': Args.string({ + description: 'ID of the workflow to show.', + required: true, + }), + } + + static flags = { + ...globalFlags, + store: Flags.string({ + char: 's', + description: 'The myshopify.com domain. Falls back to the `store` field in flow.toml.', + env: 'SHOPIFY_FLAG_STORE', + parse: async (input) => normalizeStoreFqdn(input), + }), + 'workflow-version': Flags.string({ + description: 'Optional definition version. Defaults to leading.', + env: 'SHOPIFY_FLAG_FLOW_WORKFLOW_VERSION', + }), + } + + public async run(): Promise { + const {args, flags} = await this.parse(FlowWorkflowShow) + + const store = flags.store ?? (await loadConfig())?.store + if (!store) { + throw new AbortError('No store. Pass --store, or run `flow init` to create a flow.toml.') + } + + const remote = await fetchRemoteWorkflow({ + workflowId: args['workflow-id'], + workflowVersion: flags['workflow-version'], + store, + }) + + outputResult(normalizeWorkflowJson(remote)) + } +} diff --git a/packages/flow-cli/src/cli/commands/workflow/status.ts b/packages/flow-cli/src/cli/commands/workflow/status.ts new file mode 100644 index 00000000000..e9f88c1b36f --- /dev/null +++ b/packages/flow-cli/src/cli/commands/workflow/status.ts @@ -0,0 +1,122 @@ +import {loadConfig, workflowsDirAbsolute} from '../../services/flow/project-config.js' +import {statusProject, type StatusItem, type WorkflowStatus} from '../../services/flow/workflow-lifecycle.js' +import Command from '@shopify/cli-kit/node/base-command' +import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli' +import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn' +import {AbortError} from '@shopify/cli-kit/node/error' +import {outputResult} from '@shopify/cli-kit/node/output' +import {cwd, resolvePath} from '@shopify/cli-kit/node/path' +import {Flags} from '@oclif/core' + +const STATUS_LABEL: Record = { + clean: 'clean', + drifted: 'drifted', + new: 'new', + orphaned: 'orphaned', + unknown: 'unknown', +} + +export default class FlowWorkflowStatus extends Command { + static summary = 'Show drift between local workflow files and the shop.' + + static descriptionWithMarkdown = `Walks the project's workflows directory and classifies every workflow: + +- \`clean\` — local file matches the remote workflow +- \`drifted\` — local file and remote differ; \`push\` would update remote, \`pull\` would update local +- \`new\` — local file with no lockfile; never pushed +- \`orphaned\` — lockfile points to a workflow that no longer exists on the shop +- \`unknown\` — workflow exists on the shop but isn't tracked locally; \`pull --workflow-id \` to bring it in + +Exits 1 if anything other than \`clean\` is present.` + + static description = this.descriptionWithoutMarkdown() + + static examples = ['<%= config.bin %> <%= command.id %>', '<%= config.bin %> <%= command.id %> --json'] + + static flags = { + ...globalFlags, + ...jsonFlag, + store: Flags.string({ + char: 's', + description: 'The myshopify.com domain. Falls back to the `store` field in flow.toml.', + env: 'SHOPIFY_FLAG_STORE', + parse: async (input) => normalizeStoreFqdn(input), + }), + 'workflows-dir': Flags.string({ + description: 'Directory containing workflow JSON files. Falls back to flow.toml.', + env: 'SHOPIFY_FLAG_FLOW_WORKFLOWS_DIR', + }), + 'include-hidden': Flags.boolean({ + description: 'Include hidden remote workflows in the unknown-detection list.', + default: false, + env: 'SHOPIFY_FLAG_FLOW_INCLUDE_HIDDEN', + }), + } + + public async run(): Promise { + const {flags} = await this.parse(FlowWorkflowStatus) + + const config = await loadConfig() + const store = flags.store ?? config?.store + if (!store) { + throw new AbortError('No store. Pass --store, or run `flow init` to create a flow.toml.') + } + let dir: string | undefined + if (flags['workflows-dir']) { + dir = resolvePath(cwd(), flags['workflows-dir']) + } else if (config) { + dir = workflowsDirAbsolute(config) + } + if (!dir) { + throw new AbortError('No workflows directory. Pass --workflows-dir, or run `flow init`.') + } + + const result = await statusProject({store, workflowsDir: dir, includeHidden: flags['include-hidden']}) + + if (flags.json) { + outputResult(JSON.stringify(result, null, 2)) + } else { + outputResult(formatHuman(result.items, result.counts, store, dir)) + } + + const dirty = result.counts.drifted + result.counts.new + result.counts.orphaned + result.counts.unknown + if (dirty > 0) { + if (!flags.json) { + outputResult(`\nProject not clean: ${formatDirtySummary(result.counts)}. Exiting 1.`) + } + process.exitCode = 1 + } + } +} + +function formatDirtySummary(counts: Record): string { + const parts: string[] = [] + if (counts.drifted > 0) parts.push(`${counts.drifted} drifted`) + if (counts.new > 0) parts.push(`${counts.new} new`) + if (counts.orphaned > 0) parts.push(`${counts.orphaned} orphaned`) + if (counts.unknown > 0) parts.push(`${counts.unknown} unknown`) + return parts.join(', ') +} + +function formatHuman(items: StatusItem[], counts: Record, store: string, dir: string): string { + const header = [ + `Store: ${store}`, + `Workflows dir: ${dir}`, + '', + `clean: ${counts.clean}`, + `drifted: ${counts.drifted}`, + `new: ${counts.new}`, + `orphaned: ${counts.orphaned}`, + `unknown: ${counts.unknown}`, + '', + ] + + const rows = items.map((item) => { + const label = STATUS_LABEL[item.status].padEnd(9) + const ref = item.filePath ?? `(remote: ${item.workflowId ?? '?'}${item.name ? ` "${item.name}"` : ''})` + const tail = item.message ? ` — ${item.message}` : '' + return `${label} ${ref}${tail}` + }) + + return [...header, ...rows].join('\n') +} diff --git a/packages/flow-cli/src/cli/commands/workflow/validate.ts b/packages/flow-cli/src/cli/commands/workflow/validate.ts new file mode 100644 index 00000000000..56aafa89a73 --- /dev/null +++ b/packages/flow-cli/src/cli/commands/workflow/validate.ts @@ -0,0 +1,67 @@ +import {loadConfig} from '../../services/flow/project-config.js' +import {validateWorkflow} from '../../services/flow/workflow-lifecycle.js' +import Command from '@shopify/cli-kit/node/base-command' +import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli' +import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn' +import {AbortError} from '@shopify/cli-kit/node/error' +import {outputResult} from '@shopify/cli-kit/node/output' +import {resolvePath} from '@shopify/cli-kit/node/path' +import {Args, Flags} from '@oclif/core' + +export default class FlowWorkflowValidate extends Command { + static summary = 'Validate a Flow workflow JSON file against a shop without persisting.' + + static descriptionWithMarkdown = + 'Runs the same validation as push but in dry-run mode (`is_eval`). No DB writes; returns validation errors against the shop scope.' + + static description = this.descriptionWithoutMarkdown() + + static examples = ['<%= config.bin %> <%= command.id %> ./workflow.flow.json --store shop.myshopify.com'] + + static args = { + file: Args.string({ + description: 'Path to the workflow JSON file.', + required: true, + parse: async (input) => resolvePath(input), + }), + } + + static flags = { + ...globalFlags, + ...jsonFlag, + store: Flags.string({ + char: 's', + description: 'The myshopify.com domain. Falls back to the `store` field in flow.toml.', + env: 'SHOPIFY_FLAG_STORE', + parse: async (input) => normalizeStoreFqdn(input), + }), + } + + public async run(): Promise { + const {args, flags} = await this.parse(FlowWorkflowValidate) + + const store = flags.store ?? (await loadConfig())?.store + if (!store) { + throw new AbortError('No store. Pass --store, or run `flow init` to create a flow.toml.') + } + + const result = await validateWorkflow({ + filePath: args.file, + store, + }) + + if (flags.json) { + outputResult(JSON.stringify(result, null, 2)) + } else if (result.validation_errors && result.validation_errors.length > 0) { + outputResult(`Validation errors:\n${JSON.stringify(result.validation_errors, null, 2)}`) + } else if (result.error) { + outputResult(`Error: ${result.error}`) + } else { + outputResult('Workflow is valid.') + } + + if (result.error || (result.validation_errors && result.validation_errors.length > 0)) { + throw new AbortError('Workflow validation failed.') + } + } +} diff --git a/packages/flow-cli/src/cli/services/flow/auth.ts b/packages/flow-cli/src/cli/services/flow/auth.ts new file mode 100644 index 00000000000..0faa00758ec --- /dev/null +++ b/packages/flow-cli/src/cli/services/flow/auth.ts @@ -0,0 +1,35 @@ +/* + * Caller authentication for Flow CLI requests. + * + * Flow authenticates the merchant with the credentials minted by + * `shopify store auth` (a pure-PKCE online Admin API access token), the same way + * `shopify store execute` does — no Shopify Identity login. The merchant token is + * used only to authenticate the caller: the Flow backend validates it against the + * shop's Admin API and performs the actual work with its own service auth. + */ +import {loadStoredStoreSession} from '../store-auth/session-lifecycle.js' +import {clearStoredStoreAppSession} from '../store-auth/session-store.js' +import {throwReauthenticateStoreAuthError} from '../store-auth/recovery.js' +import type {StoredStoreAppSession} from '../store-auth/session-store.js' + +/** + * Loads the merchant's stored `shopify store auth` session for `store`, + * transparently refreshing an expired access token. Throws a user-facing + * re-authentication error if no session is stored or the refresh fails. + */ +export async function authenticateFlowStore(store: string): Promise { + return loadStoredStoreSession(store) +} + +/** + * Handles a 401 from a Flow gateway endpoint: the stored token was rejected, so + * clear it and prompt the merchant to re-run `shopify store auth`. + */ +export function throwFlowAuthExpired(session: StoredStoreAppSession): never { + clearStoredStoreAppSession(session.store, session.userId) + throwReauthenticateStoreAuthError( + `Stored store authentication for ${session.store} is no longer valid.`, + session.store, + session.scopes.join(','), + ) +} diff --git a/packages/flow-cli/src/cli/services/flow/dispatch.ts b/packages/flow-cli/src/cli/services/flow/dispatch.ts new file mode 100644 index 00000000000..f63c95e4979 --- /dev/null +++ b/packages/flow-cli/src/cli/services/flow/dispatch.ts @@ -0,0 +1,117 @@ +/* + * Transport primitive for Flow + SK tool calls. + * + * Each first-class CLI command (e.g. `flow template search`, + * `flow workflow push`) constructs the structured args it needs and calls + * `dispatchFlowTool` to send the request. This file owns: + * - Routing (Flow vs SK endpoint, prod vs local) + * - Auth (merchant `shopify store auth` token; see ./auth.ts) + * - Headers (shop domain, eval flag) + * - Response parsing + error envelope + * + * No tool catalog, no name lookup, no zod here. Callers know what they want + * to call; this just dispatches it. + */ +import {authenticateFlowStore, throwFlowAuthExpired} from './auth.js' +import {AbortError} from '@shopify/cli-kit/node/error' +import {shopifyFetch, type Response} from '@shopify/cli-kit/node/http' + +const FLOW_TOOL_CALL_BASE_PRODUCTION = 'https://flow.shopifycloud.com/flow-core/tool_call' +const FLOW_TOOL_CALL_BASE_LOCAL = 'https://flow.shop.dev/flow-core/tool_call' +const SK_TOOL_CALL_BASE_PRODUCTION = 'https://sidekick.shopify.ai/tools/call' +const SK_TOOL_CALL_BASE_LOCAL = 'https://agent-server.shop.dev/tools/call' + +export type ToolSource = 'flow' | 'sk' + +export interface DispatchInput { + name: string + source: ToolSource + store: string + args: Record + isEval?: boolean +} + +function isLocalEnvironment(): boolean { + return process.env.SHOPIFY_SERVICE_ENV === 'local' +} + +export function endpointFor(source: ToolSource): string { + const local = isLocalEnvironment() + if (source === 'flow') return local ? FLOW_TOOL_CALL_BASE_LOCAL : FLOW_TOOL_CALL_BASE_PRODUCTION + return local ? SK_TOOL_CALL_BASE_LOCAL : SK_TOOL_CALL_BASE_PRODUCTION +} + +async function parseResponse(response: Response): Promise { + const text = await response.text() + if (!text.trim()) return {} + try { + return JSON.parse(text) + } catch (error) { + if (!(error instanceof SyntaxError)) throw error + return {raw: text} + } +} + +export async function dispatchFlowTool(input: DispatchInput): Promise { + const session = await authenticateFlowStore(input.store) + const endpoint = endpointFor(input.source) + + const headers: Record = { + Authorization: `Bearer ${session.accessToken}`, + 'Content-Type': 'application/json', + 'X-Shopify-Shop-Domain': input.store, + } + if (input.isEval) headers['X-Shopify-Is-Eval'] = 'true' + + const response = await shopifyFetch( + endpoint, + { + method: 'POST', + headers, + body: JSON.stringify({ + tool: input.name, + arguments: input.args, + }), + }, + 'slow-request', + ) + + if (response.status === 401) throwFlowAuthExpired(session) + + const body = await parseResponse(response) + + if (!response.ok) { + const verbose = process.env.DEBUG && process.env.DEBUG !== '' + const display = verbose ? body : stripBacktraces(body) + const hint = verbose ? undefined : 'Re-run with --verbose to see full backtraces.' + throw new AbortError( + `Flow tool gateway request failed with HTTP ${response.status}.`, + [JSON.stringify(display, null, 2), hint].filter(Boolean).join('\n\n'), + ) + } + + return body +} + +function stripBacktraces(value: unknown): unknown { + if (Array.isArray(value)) return value.map(stripBacktraces) + if (value !== null && typeof value === 'object') { + const entries = Object.entries(value as Record) + .filter(([key]) => key !== 'error_backtrace') + .map(([key, val]) => [key, stripBacktraces(val)] as const) + return Object.fromEntries(entries) + } + return value +} + +export interface ToolJsonResponse { + ok: boolean + tool?: string + data?: T +} + +export function unwrapJsonResult(response: unknown): T { + const typed = response as ToolJsonResponse | undefined + if (!typed?.data) throw new AbortError('Flow tool returned an unexpected response shape.') + return typed.data +} diff --git a/packages/flow-cli/src/cli/services/flow/project-config.ts b/packages/flow-cli/src/cli/services/flow/project-config.ts new file mode 100644 index 00000000000..0ef79d64046 --- /dev/null +++ b/packages/flow-cli/src/cli/services/flow/project-config.ts @@ -0,0 +1,116 @@ +/* + * Reader/writer for `flow.toml` — the per-project IaC config file. + * + * Shape: + * store = "shop.myshopify.com" + * + * [workflows] + * dir = "workflows" + * + * Lifecycle commands fall back to this file when --store and --workflows-dir + * aren't passed. `flow init` writes the initial file. + */ +import {formatZodErrors} from './zod-errors.js' +import {AbortError} from '@shopify/cli-kit/node/error' +import {fileExists} from '@shopify/cli-kit/node/fs' +import {cwd, dirname, joinPath, resolvePath} from '@shopify/cli-kit/node/path' +import {zod} from '@shopify/cli-kit/node/schema' +import {TomlFile, TomlFileError} from '@shopify/cli-kit/node/toml/toml-file' + +export const FLOW_CONFIG_FILENAME = 'flow.toml' +const DEFAULT_WORKFLOWS_DIR = 'workflows' + +const FlowTomlSchema = zod + .object({ + store: zod.string().min(1, 'store is required'), + workflows: zod + .object({ + dir: zod.string().min(1).optional(), + }) + .strict() + .optional(), + }) + .strict() + +export interface FlowProjectConfig { + store: string + workflowsDir: string + configPath: string + rootDir: string +} + +export async function findConfigFile(startDir = cwd()): Promise { + let dir = resolvePath(startDir) + while (true) { + const candidate = joinPath(dir, FLOW_CONFIG_FILENAME) + // eslint-disable-next-line no-await-in-loop + if (await fileExists(candidate)) return candidate + + const parent = dirname(dir) + if (parent === dir) return undefined + dir = parent + } +} + +export async function loadConfig(startDir = cwd()): Promise { + const path = await findConfigFile(startDir) + if (!path) return undefined + + let file: TomlFile + try { + file = await TomlFile.read(path) + } catch (error) { + if (error instanceof TomlFileError) throw new AbortError(`Failed to parse ${path}: ${error.message}`) + throw error + } + + const parsed = FlowTomlSchema.safeParse(file.content) + if (!parsed.success) { + throw new AbortError(`${path} is malformed.`, formatZodErrors(parsed.error)) + } + + return { + store: parsed.data.store, + workflowsDir: parsed.data.workflows?.dir ?? DEFAULT_WORKFLOWS_DIR, + configPath: path, + rootDir: dirname(path), + } +} + +export async function requireConfig(startDir = cwd()): Promise { + const config = await loadConfig(startDir) + if (!config) { + throw new AbortError( + `No ${FLOW_CONFIG_FILENAME} found in this directory or any ancestor.`, + 'Run `flow init` to create one.', + ) + } + return config +} + +export interface WriteConfigInput { + dir: string + store: string + workflowsDir?: string + force?: boolean +} + +export async function writeConfig(input: WriteConfigInput): Promise { + const path = joinPath(input.dir, FLOW_CONFIG_FILENAME) + if ((await fileExists(path)) && !input.force) { + throw new AbortError(`${path} already exists.`, 'Pass --force to overwrite, or edit the file directly.') + } + + const file = new TomlFile(path, {}) + await file.replace({ + store: input.store, + workflows: { + dir: input.workflowsDir ?? DEFAULT_WORKFLOWS_DIR, + }, + }) + return path +} + +export function workflowsDirAbsolute(config: FlowProjectConfig): string { + return resolvePath(config.rootDir, config.workflowsDir) +} diff --git a/packages/flow-cli/src/cli/services/flow/workflow-lifecycle.ts b/packages/flow-cli/src/cli/services/flow/workflow-lifecycle.ts new file mode 100644 index 00000000000..524302f7276 --- /dev/null +++ b/packages/flow-cli/src/cli/services/flow/workflow-lifecycle.ts @@ -0,0 +1,682 @@ +import {dispatchFlowTool} from './dispatch.js' +import {formatZodErrors} from './zod-errors.js' +import {AbortError} from '@shopify/cli-kit/node/error' +import {fileExists, glob, mkdir, readFile, writeFile} from '@shopify/cli-kit/node/fs' +import {joinPath} from '@shopify/cli-kit/node/path' +import {zod} from '@shopify/cli-kit/node/schema' +import {createHash} from 'crypto' + +const TOOL_CREATE_OR_UPDATE = 'flow_app_agent_create_or_update_workflow_from_json' +const TOOL_WORKFLOW_LOOKUP = 'flow_app_agent_workflow_lookup' +const TOOL_LIST_WORKFLOWS = 'flow_app_agent_list_workflows' +const TOOL_ACTIVATE = 'flow_app_agent_activate_workflow' +const TOOL_DEACTIVATE = 'flow_app_agent_deactivate_workflow' + +const LockfileSchema = zod + .object({ + workflow_id: zod.string().min(1), + workflow_definition_version: zod.string().min(1), + payload_sha256: zod.string().regex(/^[a-f0-9]{64}$/, 'must be a 64-char lowercase hex sha256'), + store: zod.string().min(1), + pushed_at: zod.string().datetime(), + }) + .strict() + +export type Lockfile = zod.infer + +export interface UpsertResponse { + ok: boolean + tool?: string + data?: UpsertResponseJson +} + +export interface UpsertResponseJson { + message?: string + workflow_id?: string + workflow_version?: string + workflow_definition_version?: string + validation_errors?: unknown[] + workflow_definition?: WorkflowJson + error?: string + error_code?: string +} + +export interface LookupResponseJson { + workflow_id?: string + workflow_version?: string + workflow_definition?: WorkflowJson + error?: string + error_code?: string +} + +export type WorkflowJson = Record + +export function normalizeWorkflowJson(payload: WorkflowJson): string { + return `${JSON.stringify(sortKeys(payload), null, 2)}\n` +} + +function sortKeys(value: unknown): unknown { + if (Array.isArray(value)) return value.map(sortKeys) + if (value !== null && typeof value === 'object') { + const entries = Object.entries(value as Record) + .sort(([left], [right]) => left.localeCompare(right)) + .map(([key, val]) => [key, sortKeys(val)] as const) + return Object.fromEntries(entries) + } + return value +} + +export function sha256(content: string): string { + return createHash('sha256').update(content).digest('hex') +} + +export function lockfilePath(workflowFilePath: string): string { + return workflowFilePath.replace(/\.flow\.json$/, '.flow.lock.json') +} + +export async function readWorkflowFile(filePath: string): Promise { + if (!(await fileExists(filePath))) { + throw new AbortError(`Workflow file not found at ${filePath}.`) + } + const raw = await readFile(filePath, {encoding: 'utf8'}) + try { + return JSON.parse(raw) as WorkflowJson + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error' + throw new AbortError(`Workflow file ${filePath} is not valid JSON: ${message}`) + } +} + +export async function readLockfileIfExists(workflowFilePath: string): Promise { + const path = lockfilePath(workflowFilePath) + if (!(await fileExists(path))) return undefined + + const raw = await readFile(path, {encoding: 'utf8'}) + + let parsed: unknown + try { + parsed = JSON.parse(raw) + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error' + throw new AbortError( + `Lockfile at ${path} is not valid JSON: ${message}`, + 'Repair the file by hand, restore from git, or delete and re-run `pull`/`push` to regenerate.', + ) + } + + const result = LockfileSchema.safeParse(parsed) + if (!result.success) { + // Throw on corrupt rather than silently treating as missing — silently + // treating a bad lockfile as "no lockfile" makes `push` create a brand- + // new workflow on the shop, duplicating whatever this lockfile pointed at. + throw new AbortError( + `Lockfile at ${path} has unexpected shape.`, + `${formatZodErrors(result.error)}\n\nRepair the file, restore from git, or delete and re-pull to regenerate.`, + ) + } + return result.data +} + +export async function writeLockfile(workflowFilePath: string, lockfile: Lockfile): Promise { + const path = lockfilePath(workflowFilePath) + await writeFile(path, `${JSON.stringify(lockfile, null, 2)}\n`) +} + +export async function writeWorkflowFile(filePath: string, payload: WorkflowJson): Promise { + await writeFile(filePath, normalizeWorkflowJson(payload)) +} + +function unwrapJsonResult(response: unknown): UpsertResponseJson { + const typed = response as {ok: boolean; data?: UpsertResponseJson} | undefined + if (!typed?.data) throw new AbortError('Flow tool returned an unexpected response shape.') + return typed.data +} + +function unwrapLookupResult(response: unknown): LookupResponseJson { + const typed = response as {ok: boolean; data?: LookupResponseJson} | undefined + if (!typed?.data) throw new AbortError('Flow workflow lookup returned an unexpected response shape.') + return typed.data +} + +export interface ValidateInput { + filePath: string + store: string +} + +export async function validateWorkflow(input: ValidateInput): Promise { + const payload = await readWorkflowFile(input.filePath) + const lockfile = await readLockfileIfExists(input.filePath) + + const args: Record = {workflow_json: payload} + if (lockfile) { + args.workflow_id = lockfile.workflow_id + args.workflow_version = lockfile.workflow_definition_version + } + + const response = await dispatchFlowTool({ + name: TOOL_CREATE_OR_UPDATE, + source: 'flow', + store: input.store, + args, + isEval: true, + }) + + return unwrapJsonResult(response) +} + +export interface PushInput { + filePath: string + store: string +} + +export interface PushResult { + workflowId: string + workflowDefinitionVersion: string + validationErrors: unknown[] + lockfile: Lockfile +} + +export async function pushWorkflow(input: PushInput): Promise { + const payload = await readWorkflowFile(input.filePath) + const lockfile = await readLockfileIfExists(input.filePath) + + const args: Record = { + workflow_json: payload, + hidden: false, + } + if (lockfile) { + args.workflow_id = lockfile.workflow_id + args.workflow_version = lockfile.workflow_definition_version + } + + const response = await dispatchFlowTool({ + name: TOOL_CREATE_OR_UPDATE, + source: 'flow', + store: input.store, + args, + }) + + const json = unwrapJsonResult(response) + if (!json.workflow_id || !json.workflow_version) { + throw new AbortError(`Push failed: ${json.error ?? 'no workflow_id returned'}`) + } + + const canonicalPayload = json.workflow_definition ?? payload + const canonicalNormalized = normalizeWorkflowJson(canonicalPayload) + if (json.workflow_definition) { + await writeWorkflowFile(input.filePath, canonicalPayload) + } + + const newLockfile: Lockfile = { + workflow_id: json.workflow_id, + workflow_definition_version: json.workflow_version, + payload_sha256: sha256(canonicalNormalized), + store: input.store, + pushed_at: new Date().toISOString(), + } + await writeLockfile(input.filePath, newLockfile) + + return { + workflowId: json.workflow_id, + workflowDefinitionVersion: json.workflow_version, + validationErrors: json.validation_errors ?? [], + lockfile: newLockfile, + } +} + +export interface PullInput { + workflowId: string + workflowVersion?: string + outPath: string + store: string +} + +export interface PullResult { + workflowId: string + workflowDefinitionVersion: string + payload: WorkflowJson + lockfile: Lockfile +} + +export async function pullWorkflow(input: PullInput): Promise { + const args: Record = {workflow_id: input.workflowId} + if (input.workflowVersion) args.version = input.workflowVersion + + const response = await dispatchFlowTool({ + name: TOOL_WORKFLOW_LOOKUP, + source: 'flow', + store: input.store, + args, + }) + + const json = unwrapLookupResult(response) + if (!json.workflow_definition || !json.workflow_id || !json.workflow_version) { + throw new AbortError(`Pull failed: ${json.error ?? 'workflow_definition missing from response'}`) + } + + await writeWorkflowFile(input.outPath, json.workflow_definition) + + const lockfile: Lockfile = { + workflow_id: json.workflow_id, + workflow_definition_version: json.workflow_version, + payload_sha256: sha256(normalizeWorkflowJson(json.workflow_definition)), + store: input.store, + pushed_at: new Date().toISOString(), + } + await writeLockfile(input.outPath, lockfile) + + return { + workflowId: json.workflow_id, + workflowDefinitionVersion: json.workflow_version, + payload: json.workflow_definition, + lockfile, + } +} + +export interface DiffInput { + filePath: string + store: string + workflowId?: string + workflowVersion?: string +} + +export interface DiffResult { + changed: boolean + localNormalized: string + remoteNormalized: string +} + +export async function fetchRemoteWorkflow(input: { + workflowId: string + workflowVersion?: string + store: string +}): Promise { + const args: Record = {workflow_id: input.workflowId} + if (input.workflowVersion) args.version = input.workflowVersion + + const response = await dispatchFlowTool({ + name: TOOL_WORKFLOW_LOOKUP, + source: 'flow', + store: input.store, + args, + }) + + const json = unwrapLookupResult(response) + if (!json.workflow_definition) { + throw new AbortError(`Remote workflow lookup failed: ${json.error ?? 'no workflow_definition'}`) + } + return json.workflow_definition +} + +export async function diffWorkflow(input: DiffInput): Promise { + const localPayload = await readWorkflowFile(input.filePath) + const localNormalized = normalizeWorkflowJson(localPayload) + + const workflowId = input.workflowId ?? (await readLockfileIfExists(input.filePath))?.workflow_id + if (!workflowId) { + throw new AbortError( + `No lockfile found for ${input.filePath} and no --workflow-id was provided.`, + 'Run `flow workflow push` first, or pass --workflow-id.', + ) + } + + const remotePayload = await fetchRemoteWorkflow({ + workflowId, + workflowVersion: input.workflowVersion, + store: input.store, + }) + const remoteNormalized = normalizeWorkflowJson(remotePayload) + + return { + changed: localNormalized !== remoteNormalized, + localNormalized, + remoteNormalized, + } +} + +export function unifiedDiff(remoteLabel: string, remoteText: string, localLabel: string, localText: string): string { + const remoteLines = remoteText.split('\n') + const localLines = localText.split('\n') + const lines: string[] = [`--- ${remoteLabel}`, `+++ ${localLabel}`] + + const lcs = longestCommonSubsequence(remoteLines, localLines) + let i = 0 + let j = 0 + let lcsIdx = 0 + while (i < remoteLines.length || j < localLines.length) { + if (lcsIdx < lcs.length && remoteLines[i] === lcs[lcsIdx] && localLines[j] === lcs[lcsIdx]) { + lines.push(` ${remoteLines[i]}`) + i++ + j++ + lcsIdx++ + } else if (j < localLines.length && (lcsIdx >= lcs.length || localLines[j] !== lcs[lcsIdx])) { + lines.push(`+${localLines[j]}`) + j++ + } else if (i < remoteLines.length && (lcsIdx >= lcs.length || remoteLines[i] !== lcs[lcsIdx])) { + lines.push(`-${remoteLines[i]}`) + i++ + } + } + return lines.join('\n') +} + +function longestCommonSubsequence(seqA: string[], seqB: string[]): string[] { + const dp: number[][] = Array.from({length: seqA.length + 1}, () => new Array(seqB.length + 1).fill(0)) + for (let i = 1; i <= seqA.length; i++) { + for (let j = 1; j <= seqB.length; j++) { + if (seqA[i - 1] === seqB[j - 1]) { + dp[i]![j] = dp[i - 1]![j - 1]! + 1 + } else { + dp[i]![j] = Math.max(dp[i - 1]![j]!, dp[i]![j - 1]!) + } + } + } + const result: string[] = [] + let i = seqA.length + let j = seqB.length + while (i > 0 && j > 0) { + if (seqA[i - 1] === seqB[j - 1]) { + result.unshift(seqA[i - 1]!) + i-- + j-- + } else if (dp[i - 1]![j]! >= dp[i]![j - 1]!) { + i-- + } else { + j-- + } + } + return result +} + +export interface ActivationInput { + filePath?: string + store: string + workflowId?: string + workflowVersion?: string + useLatest?: boolean +} + +async function resolveActivationTarget(input: ActivationInput): Promise<{workflowId: string; workflowVersion: string}> { + if (input.workflowId && input.workflowVersion) { + return {workflowId: input.workflowId, workflowVersion: input.workflowVersion} + } + + if (input.workflowId && input.useLatest) { + const remote = await fetchRemoteWorkflow({ + workflowId: input.workflowId, + store: input.store, + }) + const version = (remote as {root?: {workflow_version?: string}})?.root?.workflow_version + if (!version) { + throw new AbortError('Could not resolve latest workflow version from workflow lookup response.') + } + return {workflowId: input.workflowId, workflowVersion: version} + } + + if (input.filePath) { + const lockfile = await readLockfileIfExists(input.filePath) + if (lockfile) { + return { + workflowId: lockfile.workflow_id, + workflowVersion: lockfile.workflow_definition_version, + } + } + } + + throw new AbortError( + "Can't resolve workflow target.", + 'Pass a workflow file with a lockfile, or --workflow-id together with --workflow-version or --use-latest.', + ) +} + +export async function activateWorkflow(input: ActivationInput): Promise { + const {workflowId, workflowVersion} = await resolveActivationTarget(input) + const response = await dispatchFlowTool({ + name: TOOL_ACTIVATE, + source: 'flow', + store: input.store, + args: {workflow_id: workflowId, workflow_definition_version: workflowVersion}, + }) + return unwrapJsonResult(response) +} + +export async function deactivateWorkflow(input: ActivationInput): Promise { + const {workflowId, workflowVersion} = await resolveActivationTarget(input) + const response = await dispatchFlowTool({ + name: TOOL_DEACTIVATE, + source: 'flow', + store: input.store, + args: {workflow_id: workflowId, workflow_definition_version: workflowVersion}, + }) + return unwrapJsonResult(response) +} + +export interface RemoteWorkflowSummary { + workflow_id: string + name: string + hidden: boolean + last_updated?: string +} + +interface ListWorkflowsResponseJson { + workflows?: RemoteWorkflowSummary[] + pagination?: { + has_next_page?: boolean + next_cursor?: string + total_count?: number + } + error?: string + error_code?: string +} + +export async function listAllWorkflows(input: { + store: string + includeHidden?: boolean +}): Promise { + const all: RemoteWorkflowSummary[] = [] + let cursor: string | undefined + + while (true) { + const args: Record = {page_size: 100} + if (cursor) args.cursor = cursor + if (input.includeHidden) args.include_hidden = true + + // eslint-disable-next-line no-await-in-loop + const response = await dispatchFlowTool({ + name: TOOL_LIST_WORKFLOWS, + source: 'flow', + store: input.store, + args, + }) + + const json = (response as {ok: boolean; data?: ListWorkflowsResponseJson} | undefined)?.data + if (!json) throw new AbortError('Flow list_workflows returned an unexpected response shape.') + if (json.error) throw new AbortError(`list_workflows failed: ${json.error}`) + + if (json.workflows) all.push(...json.workflows) + + if (!json.pagination?.has_next_page || !json.pagination.next_cursor) break + cursor = json.pagination.next_cursor + } + + return all +} + +export const WORKFLOW_FILENAME = 'workflow.flow.json' + +export function workflowSlugFor(name: string): string { + const slug = name + .toLowerCase() + .normalize('NFKD') + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, '') + return slug || 'workflow' +} + +export function workflowDirAndFilename(name: string): {dir: string; filename: string} { + return {dir: workflowSlugFor(name), filename: WORKFLOW_FILENAME} +} + +export interface PullAllInput { + store: string + outDir: string + includeHidden?: boolean + force?: boolean +} + +export interface PullAllItem { + workflowId: string + name: string + filePath: string + status: 'pulled' | 'skipped' + reason?: string +} + +export interface PullAllResult { + total: number + pulled: number + skipped: number + items: PullAllItem[] +} + +export async function pullAllWorkflows(input: PullAllInput): Promise { + const remote = await listAllWorkflows({store: input.store, includeHidden: input.includeHidden}) + await mkdir(input.outDir) + + const usedSlugs = new Set() + const items: PullAllItem[] = [] + + for (const workflow of remote) { + const baseSlug = workflowSlugFor(workflow.name) + let slug = baseSlug + let suffix = 2 + while (usedSlugs.has(slug)) { + slug = `${baseSlug}-${suffix}` + suffix += 1 + } + usedSlugs.add(slug) + + const workflowDir = joinPath(input.outDir, slug) + const filePath = joinPath(workflowDir, WORKFLOW_FILENAME) + + // eslint-disable-next-line no-await-in-loop + if ((await fileExists(filePath)) && !input.force) { + items.push({ + workflowId: workflow.workflow_id, + name: workflow.name, + filePath, + status: 'skipped', + reason: 'file exists (pass --force to overwrite)', + }) + continue + } + + // eslint-disable-next-line no-await-in-loop + await mkdir(workflowDir) + // eslint-disable-next-line no-await-in-loop + await pullWorkflow({ + workflowId: workflow.workflow_id, + outPath: filePath, + store: input.store, + }) + + items.push({ + workflowId: workflow.workflow_id, + name: workflow.name, + filePath, + status: 'pulled', + }) + } + + return { + total: items.length, + pulled: items.filter((item) => item.status === 'pulled').length, + skipped: items.filter((item) => item.status === 'skipped').length, + items, + } +} + +export type WorkflowStatus = 'clean' | 'drifted' | 'new' | 'orphaned' | 'unknown' + +export interface StatusItem { + status: WorkflowStatus + filePath?: string + workflowId?: string + name?: string + message?: string +} + +export interface StatusInput { + store: string + workflowsDir: string + includeHidden?: boolean +} + +export interface StatusResult { + store: string + items: StatusItem[] + counts: Record +} + +async function classifyLocalFile(filePath: string, store: string): Promise { + const lockfile = await readLockfileIfExists(filePath) + if (!lockfile) { + return {status: 'new', filePath} + } + + try { + const localPayload = await readWorkflowFile(filePath) + const localNormalized = normalizeWorkflowJson(localPayload) + const remotePayload = await fetchRemoteWorkflow({ + workflowId: lockfile.workflow_id, + store, + }) + const remoteNormalized = normalizeWorkflowJson(remotePayload) + + return { + status: localNormalized === remoteNormalized ? 'clean' : 'drifted', + filePath, + workflowId: lockfile.workflow_id, + } + } catch (error) { + if (!(error instanceof AbortError)) throw error + return { + status: 'orphaned', + filePath, + workflowId: lockfile.workflow_id, + message: error.message, + } + } +} + +export async function statusProject(input: StatusInput): Promise { + const localFiles = await glob('**/*.flow.json', { + cwd: input.workflowsDir, + absolute: true, + }) + + const localItems = await Promise.all(localFiles.map((filePath) => classifyLocalFile(filePath, input.store))) + + const trackedIds = new Set(localItems.map((item) => item.workflowId).filter((id): id is string => Boolean(id))) + + const remote = await listAllWorkflows({store: input.store, includeHidden: input.includeHidden}) + const unknownItems: StatusItem[] = remote + .filter((workflow) => !trackedIds.has(workflow.workflow_id)) + .map((workflow) => ({ + status: 'unknown' as WorkflowStatus, + workflowId: workflow.workflow_id, + name: workflow.name, + })) + + const items = [...localItems, ...unknownItems] + const counts: Record = { + clean: 0, + drifted: 0, + new: 0, + orphaned: 0, + unknown: 0, + } + for (const item of items) counts[item.status] += 1 + + return {store: input.store, items, counts} +} diff --git a/packages/flow-cli/src/cli/services/flow/zod-errors.ts b/packages/flow-cli/src/cli/services/flow/zod-errors.ts new file mode 100644 index 00000000000..9796937bcfb --- /dev/null +++ b/packages/flow-cli/src/cli/services/flow/zod-errors.ts @@ -0,0 +1,16 @@ +import type {zod} from '@shopify/cli-kit/node/schema' + +/** + * Format a ZodError into a single human-readable string suitable for an + * AbortError detail line. Each issue is rendered as `: `, + * joined with `; `. Paths are dotted (`workflows.dir`) or `(root)` for + * top-level errors. + */ +export function formatZodErrors(error: zod.ZodError): string { + return error.errors + .map((issue) => { + const path = issue.path.length > 0 ? issue.path.join('.') : '(root)' + return `${path}: ${issue.message}` + }) + .join('; ') +} diff --git a/packages/flow-cli/src/cli/services/store-auth/config.ts b/packages/flow-cli/src/cli/services/store-auth/config.ts new file mode 100644 index 00000000000..46b72e6a7da --- /dev/null +++ b/packages/flow-cli/src/cli/services/store-auth/config.ts @@ -0,0 +1,20 @@ +export const STORE_AUTH_APP_CLIENT_ID = '7e9cb568cfd431c538f36d1ad3f2b4f6' +export const DEFAULT_STORE_AUTH_PORT = 13387 +export const STORE_AUTH_CALLBACK_PATH = '/auth/callback' + +export function storeAuthRedirectUri(port: number): string { + return `http://127.0.0.1:${port}${STORE_AUTH_CALLBACK_PATH}` +} + +export function storeAuthSessionKey(store: string): string { + return `${STORE_AUTH_APP_CLIENT_ID}::${escapeStoreAuthSessionKeySegment(store)}` +} + +function escapeStoreAuthSessionKeySegment(value: string): string { + return value.replace(/\./g, '\\.') +} + +export function maskToken(token: string): string { + if (token.length <= 10) return '***' + return `${token.slice(0, 10)}***` +} diff --git a/packages/flow-cli/src/cli/services/store-auth/recovery.ts b/packages/flow-cli/src/cli/services/store-auth/recovery.ts new file mode 100644 index 00000000000..8b328701eb0 --- /dev/null +++ b/packages/flow-cli/src/cli/services/store-auth/recovery.ts @@ -0,0 +1,30 @@ +import {AbortError} from '@shopify/cli-kit/node/error' + +function storeAuthCommand(store: string, scopes: string): {command: string} { + return {command: `shopify store auth --store ${store} --scopes ${scopes}`} +} + +function storeAuthCommandNextSteps(store: string, scopes: string) { + return [[storeAuthCommand(store, scopes)]] +} + +export function throwStoredStoreAuthError(store: string): never { + throw new AbortError( + `No stored app authentication found for ${store}.`, + 'To create stored auth for this store, run:', + storeAuthCommandNextSteps(store, ''), + ) +} + +export function throwReauthenticateStoreAuthError(message: string, store: string, scopes: string): never { + throw new AbortError(message, 'To re-authenticate, run:', storeAuthCommandNextSteps(store, scopes)) +} + +export function retryStoreAuthWithPermanentDomainError(returnedStore: string): AbortError { + // eslint-disable-next-line @shopify/cli/no-error-factory-functions + return new AbortError( + 'OAuth callback store does not match the requested store.', + `Shopify returned ${returnedStore} during authentication. Re-run using the permanent store domain:`, + storeAuthCommandNextSteps(returnedStore, ''), + ) +} diff --git a/packages/flow-cli/src/cli/services/store-auth/session-lifecycle.ts b/packages/flow-cli/src/cli/services/store-auth/session-lifecycle.ts new file mode 100644 index 00000000000..bdefbfd54e4 --- /dev/null +++ b/packages/flow-cli/src/cli/services/store-auth/session-lifecycle.ts @@ -0,0 +1,108 @@ +import {maskToken} from './config.js' +import {throwStoredStoreAuthError, throwReauthenticateStoreAuthError} from './recovery.js' +import {clearStoredStoreAppSession, getCurrentStoredStoreAppSession, setStoredStoreAppSession} from './session-store.js' +import {refreshStoreAccessToken} from './token-client.js' +import {outputContent, outputDebug, outputToken} from '@shopify/cli-kit/node/output' +import {AbortError} from '@shopify/cli-kit/node/error' +import type {StoredStoreAppSession} from './session-store.js' + +const EXPIRY_MARGIN_MS = 4 * 60 * 1000 + +export function isSessionExpired(session: StoredStoreAppSession): boolean { + if (!session.expiresAt) return false + + const expiresAtMs = new Date(session.expiresAt).getTime() + if (Number.isNaN(expiresAtMs)) return true + + return expiresAtMs - EXPIRY_MARGIN_MS < Date.now() +} + +function buildRefreshedStoredSession( + session: StoredStoreAppSession, + refresh: { + accessToken: string + refreshToken?: string + expiresIn?: number + refreshTokenExpiresIn?: number + }, +): StoredStoreAppSession { + const now = Date.now() + const expiresAt = refresh.expiresIn ? new Date(now + refresh.expiresIn * 1000).toISOString() : session.expiresAt + + return { + ...session, + accessToken: refresh.accessToken, + refreshToken: refresh.refreshToken ?? session.refreshToken, + expiresAt, + refreshTokenExpiresAt: refresh.refreshTokenExpiresIn + ? new Date(now + refresh.refreshTokenExpiresIn * 1000).toISOString() + : session.refreshTokenExpiresAt, + acquiredAt: new Date(now).toISOString(), + } +} + +export async function loadStoredStoreSession(store: string): Promise { + let session = getCurrentStoredStoreAppSession(store) + + if (!session) { + throwStoredStoreAuthError(store) + } + + outputDebug( + outputContent`Loaded stored session for ${outputToken.raw(store)}: token=${outputToken.raw(maskToken(session.accessToken))}, expires=${outputToken.raw(session.expiresAt ?? 'unknown')}`, + ) + + if (!isSessionExpired(session)) { + return session + } + + if (!session.refreshToken) { + throwReauthenticateStoreAuthError( + `No refresh token stored for ${session.store}.`, + session.store, + session.scopes.join(','), + ) + } + + outputDebug( + outputContent`Refreshing expired token for ${outputToken.raw(session.store)} (expired at ${outputToken.raw(session.expiresAt ?? 'unknown')}, refresh_token=${outputToken.raw(maskToken(session.refreshToken))})`, + ) + + const previousAccessToken = session.accessToken + + let refreshed + try { + refreshed = await refreshStoreAccessToken({ + store: session.store, + refreshToken: session.refreshToken, + }) + } catch (error) { + clearStoredStoreAppSession(session.store, session.userId) + + if (error instanceof AbortError && error.message.startsWith(`Token refresh failed for ${session.store} (HTTP `)) { + throwReauthenticateStoreAuthError(error.message, session.store, session.scopes.join(',')) + } + + if ( + error instanceof AbortError && + error.message === `Token refresh returned an invalid response for ${session.store}.` + ) { + throwReauthenticateStoreAuthError(error.message, session.store, session.scopes.join(',')) + } + + if (error instanceof AbortError && error.message === 'Received an invalid refresh response from Shopify.') { + throw error + } + + throw error + } + + session = buildRefreshedStoredSession(session, refreshed) + + outputDebug( + outputContent`Token refresh succeeded for ${outputToken.raw(session.store)}: ${outputToken.raw(maskToken(previousAccessToken))} → ${outputToken.raw(maskToken(session.accessToken))}, new expiry ${outputToken.raw(session.expiresAt ?? 'unknown')}`, + ) + + setStoredStoreAppSession(session) + return session +} diff --git a/packages/flow-cli/src/cli/services/store-auth/session-store.ts b/packages/flow-cli/src/cli/services/store-auth/session-store.ts new file mode 100644 index 00000000000..0acdb46232a --- /dev/null +++ b/packages/flow-cli/src/cli/services/store-auth/session-store.ts @@ -0,0 +1,300 @@ +import {STORE_AUTH_APP_CLIENT_ID, storeAuthSessionKey} from './config.js' +import {LocalStorage} from '@shopify/cli-kit/node/local-storage' +import {type JsonMapType} from '@shopify/cli-kit/node/toml' + +/** + * Discriminator for a stored store auth session. + * + * - 'standard': created via `shopify store auth`. + * - 'preview': created via `shopify store create preview`; backed by a server-issued Admin API token. + * + * Stored sessions written before this discriminator existed have no `kind` field and are + * read back as 'standard'. + */ +type StoredStoreSessionKind = 'standard' | 'preview' + +interface StoredPreviewStoreMetadata { + /** Placeholder account UUID returned by the preview-store backend when available. */ + placeholderAccountUuid?: string + /** Numeric shop id returned by the preview-store backend. */ + shopId: string + /** Store name returned by the preview-store backend. */ + name: string + /** ISO country code requested for the store, when provided by the caller. */ + country?: string + /** ISO timestamp for when the preview store was created locally. */ + createdAt: string + /** Access URL returned by the preview-store backend. */ + accessUrl?: string +} + +export interface StoredStoreAppSession { + store: string + clientId: string + userId: string + accessToken: string + refreshToken?: string + scopes: string[] + acquiredAt: string + expiresAt?: string + refreshTokenExpiresAt?: string + associatedUser?: { + id: number + email?: string + firstName?: string + lastName?: string + accountOwner?: boolean + } + /** + * Discriminator. Optional in storage for back-compat with sessions written before the + * field existed; `sessionKind()` resolves missing values to 'standard'. + */ + kind?: StoredStoreSessionKind + /** Preview-store-only metadata. Set iff `kind === 'preview'`. */ + preview?: StoredPreviewStoreMetadata +} + +interface StoredStoreAppSessionBucket { + currentUserId: string + sessionsByUserId: {[userId: string]: StoredStoreAppSession} +} + +interface StoreSessionSchema { + [key: string]: StoredStoreAppSessionBucket +} + +type RawStoreSessionStorage = JsonMapType + +let _storeSessionStorage: LocalStorage | undefined + +function storeSessionStorage() { + _storeSessionStorage ??= new LocalStorage({projectName: 'shopify-cli-store'}) + return _storeSessionStorage +} + +function isString(value: unknown): value is string { + return typeof value === 'string' +} + +function sanitizeAssociatedUser(value: unknown): StoredStoreAppSession['associatedUser'] | undefined { + if (!value || typeof value !== 'object') return undefined + + const associatedUser = value as Record + if (typeof associatedUser.id !== 'number') return undefined + + return { + id: associatedUser.id, + ...(isString(associatedUser.email) ? {email: associatedUser.email} : {}), + ...(isString(associatedUser.firstName) ? {firstName: associatedUser.firstName} : {}), + ...(isString(associatedUser.lastName) ? {lastName: associatedUser.lastName} : {}), + ...(typeof associatedUser.accountOwner === 'boolean' ? {accountOwner: associatedUser.accountOwner} : {}), + } +} + +function sanitizePreviewMetadata(value: unknown): StoredPreviewStoreMetadata | undefined { + if (!value || typeof value !== 'object') return undefined + + const metadata = value as Record + if (!isString(metadata.shopId) || !isString(metadata.name) || !isString(metadata.createdAt)) return undefined + + return { + shopId: metadata.shopId, + name: metadata.name, + createdAt: metadata.createdAt, + ...(isString(metadata.placeholderAccountUuid) ? {placeholderAccountUuid: metadata.placeholderAccountUuid} : {}), + ...(isString(metadata.country) ? {country: metadata.country} : {}), + ...(isString(metadata.accessUrl) ? {accessUrl: metadata.accessUrl} : {}), + } +} + +function sanitizeStoredStoreAppSession(value: unknown): StoredStoreAppSession | undefined { + if (!value || typeof value !== 'object') return undefined + + const session = value as Record + if ( + !isString(session.store) || + !isString(session.clientId) || + !isString(session.userId) || + !isString(session.accessToken) || + !Array.isArray(session.scopes) || + !session.scopes.every(isString) || + !isString(session.acquiredAt) + ) { + return undefined + } + + const associatedUser = sanitizeAssociatedUser(session.associatedUser) + + // Discriminator is optional for back-compat: sessions written before this field existed + // are read back as 'standard'. Unknown values are coerced to 'standard' and the field is + // omitted from the result so it doesn't pollute legacy buckets. + const kind: StoredStoreSessionKind = session.kind === 'preview' ? 'preview' : 'standard' + const preview = kind === 'preview' ? sanitizePreviewMetadata(session.preview) : undefined + + // A session declared as 'preview' but missing/malformed metadata is rejected outright, + // because store-list fallback and future re-mint/claim flows rely on this metadata. + if (kind === 'preview' && !preview) return undefined + + return { + store: session.store, + clientId: session.clientId, + userId: session.userId, + accessToken: session.accessToken, + scopes: session.scopes, + acquiredAt: session.acquiredAt, + ...(isString(session.refreshToken) ? {refreshToken: session.refreshToken} : {}), + ...(isString(session.expiresAt) ? {expiresAt: session.expiresAt} : {}), + ...(isString(session.refreshTokenExpiresAt) ? {refreshTokenExpiresAt: session.refreshTokenExpiresAt} : {}), + ...(associatedUser ? {associatedUser} : {}), + ...(kind === 'preview' ? {kind} : {}), + ...(preview ? {preview} : {}), + } +} + +function sanitizeStoredStoreAppSessionBucket( + store: string, + storedBucket: unknown, + storage: LocalStorage, +): StoredStoreAppSessionBucket | undefined { + if (!storedBucket || typeof storedBucket !== 'object') return undefined + + const {sessionsByUserId, currentUserId} = storedBucket as Partial + const looksLikeBucket = sessionsByUserId !== undefined || currentUserId !== undefined + if (!looksLikeBucket) return undefined + + const key = storeAuthSessionKey(store) + if ( + !sessionsByUserId || + typeof sessionsByUserId !== 'object' || + Array.isArray(sessionsByUserId) || + typeof currentUserId !== 'string' + ) { + storage.delete(key) + return undefined + } + + const sanitizedSessionsByUserId = Object.fromEntries( + Object.entries(sessionsByUserId).flatMap(([userId, session]) => { + const sanitizedSession = sanitizeStoredStoreAppSession(session) + return sanitizedSession ? [[userId, sanitizedSession]] : [] + }), + ) + + if (Object.keys(sanitizedSessionsByUserId).length !== Object.keys(sessionsByUserId).length) { + if (sanitizedSessionsByUserId[currentUserId]) { + storage.set(key, { + currentUserId, + sessionsByUserId: sanitizedSessionsByUserId, + }) + } else { + storage.delete(key) + return undefined + } + } + + return { + currentUserId, + sessionsByUserId: sanitizedSessionsByUserId, + } +} + +function readStoredStoreAppSessionBucket( + store: string, + storage: LocalStorage, +): StoredStoreAppSessionBucket | undefined { + return sanitizeStoredStoreAppSessionBucket(store, storage.get(storeAuthSessionKey(store)), storage) +} + +// `conf` persists dotted keys as nested objects. Store-auth callers should not +// learn that layout directly; this helper keeps the current traversal private to +// the persistence seam while higher-level code projects summaries instead. +function readRawStoreSessionStorage(storage: LocalStorage): RawStoreSessionStorage { + return (storage as unknown as {config?: {store?: RawStoreSessionStorage}}).config?.store ?? {} +} + +/** + * Internal persistence helper for projecting the current session for every + * store that has locally stored store auth. + */ +export function listCurrentStoredStoreAppSessions( + storage: LocalStorage = storeSessionStorage(), +): StoredStoreAppSession[] { + const sessions: StoredStoreAppSession[] = [] + const keyPrefix = `${STORE_AUTH_APP_CLIENT_ID}::` + + for (const [key, value] of Object.entries(readRawStoreSessionStorage(storage))) { + if (!key.startsWith(keyPrefix)) continue + + const bucket = sanitizeStoredStoreAppSessionBucket(key.slice(keyPrefix.length), value, storage) + const session = bucket?.sessionsByUserId[bucket.currentUserId] + if (session) sessions.push(session) + } + + return sessions +} + +export function getCurrentStoredStoreAppSession( + store: string, + storage: LocalStorage = storeSessionStorage(), +): StoredStoreAppSession | undefined { + const bucket = readStoredStoreAppSessionBucket(store, storage) + if (!bucket) return undefined + + const session = bucket.sessionsByUserId[bucket.currentUserId] + if (!session) { + storage.delete(storeAuthSessionKey(store)) + return undefined + } + + return session +} + +export function setStoredStoreAppSession( + session: StoredStoreAppSession, + storage: LocalStorage = storeSessionStorage(), +): void { + const key = storeAuthSessionKey(session.store) + const existingBucket = readStoredStoreAppSessionBucket(session.store, storage) + + const nextBucket: StoredStoreAppSessionBucket = { + currentUserId: session.userId, + sessionsByUserId: { + ...(existingBucket?.sessionsByUserId ?? {}), + [session.userId]: session, + }, + } + + storage.set(key, nextBucket) +} + +export function clearStoredStoreAppSession( + store: string, + userIdOrStorage?: string | LocalStorage, + maybeStorage?: LocalStorage, +): void { + const userId = typeof userIdOrStorage === 'string' ? userIdOrStorage : undefined + const storage = (typeof userIdOrStorage === 'string' ? maybeStorage : userIdOrStorage) ?? storeSessionStorage() + + const key = storeAuthSessionKey(store) + + if (!userId) { + storage.delete(key) + return + } + + const existingBucket = readStoredStoreAppSessionBucket(store, storage) + if (!existingBucket) return + + const {[userId]: _removedSession, ...remainingSessions} = existingBucket.sessionsByUserId + + const remainingUserIds = Object.keys(remainingSessions) + if (remainingUserIds.length === 0) { + storage.delete(key) + return + } + + storage.set(key, { + currentUserId: existingBucket.currentUserId === userId ? remainingUserIds[0]! : existingBucket.currentUserId, + sessionsByUserId: remainingSessions, + }) +} diff --git a/packages/flow-cli/src/cli/services/store-auth/token-client.ts b/packages/flow-cli/src/cli/services/store-auth/token-client.ts new file mode 100644 index 00000000000..6aa6d02c96d --- /dev/null +++ b/packages/flow-cli/src/cli/services/store-auth/token-client.ts @@ -0,0 +1,168 @@ +import {maskToken, STORE_AUTH_APP_CLIENT_ID} from './config.js' +import {adminUrl} from '@shopify/cli-kit/node/api/admin' +import {graphqlRequest} from '@shopify/cli-kit/node/api/graphql' +import {AbortError} from '@shopify/cli-kit/node/error' +import {fetch} from '@shopify/cli-kit/node/http' +import {outputContent, outputDebug, outputToken} from '@shopify/cli-kit/node/output' + +export interface StoreTokenResponse { + access_token: string + token_type?: string + scope?: string + expires_in?: number + refresh_token?: string + refresh_token_expires_in?: number + associated_user_scope?: string + associated_user?: { + id: number + first_name?: string + last_name?: string + email?: string + account_owner?: boolean + locale?: string + collaborator?: boolean + email_verified?: boolean + } +} + +interface StoreAccessScopesResponse { + currentAppInstallation?: { + accessScopes?: {handle?: string}[] + } +} + +interface StoreTokenRefreshPayload { + accessToken: string + refreshToken?: string + expiresIn?: number + refreshTokenExpiresIn?: number +} + +function truncateHttpErrorBody(body: string, length = 300): string { + return body.slice(0, length) +} + +export async function exchangeStoreAuthCodeForToken(options: { + store: string + code: string + codeVerifier: string + redirectUri: string +}): Promise { + const endpoint = `https://${options.store}/admin/oauth/access_token` + + outputDebug(outputContent`Exchanging authorization code for token at ${outputToken.raw(endpoint)}`) + + const response = await fetch(endpoint, { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + client_id: STORE_AUTH_APP_CLIENT_ID, + code: options.code, + code_verifier: options.codeVerifier, + redirect_uri: options.redirectUri, + }), + }) + + const body = await response.text() + if (!response.ok) { + outputDebug( + outputContent`Token exchange failed with HTTP ${outputToken.raw(String(response.status))}: ${outputToken.raw(truncateHttpErrorBody(body || response.statusText))}`, + ) + throw new AbortError( + `Failed to exchange OAuth code for an access token (HTTP ${response.status}).`, + body || response.statusText, + ) + } + + let parsed: StoreTokenResponse + try { + parsed = JSON.parse(body) as StoreTokenResponse + } catch { + throw new AbortError('Received an invalid token response from Shopify.') + } + + outputDebug( + outputContent`Token exchange succeeded: access_token=${outputToken.raw(maskToken(parsed.access_token))}, refresh_token=${outputToken.raw(parsed.refresh_token ? maskToken(parsed.refresh_token) : 'none')}, expires_in=${outputToken.raw(String(parsed.expires_in ?? 'unknown'))}s, user=${outputToken.raw(String(parsed.associated_user?.id ?? 'unknown'))} (${outputToken.raw(parsed.associated_user?.email ?? 'no email')})`, + ) + + return parsed +} + +export async function refreshStoreAccessToken(options: { + store: string + refreshToken: string +}): Promise { + const endpoint = `https://${options.store}/admin/oauth/access_token` + + outputDebug( + outputContent`Refreshing access token for ${outputToken.raw(options.store)} using refresh_token=${outputToken.raw(maskToken(options.refreshToken))}`, + ) + + const response = await fetch(endpoint, { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + client_id: STORE_AUTH_APP_CLIENT_ID, + grant_type: 'refresh_token', + refresh_token: options.refreshToken, + }), + }) + + const body = await response.text() + if (!response.ok) { + outputDebug( + outputContent`Token refresh failed with HTTP ${outputToken.raw(String(response.status))}: ${outputToken.raw(truncateHttpErrorBody(body || response.statusText))}`, + ) + throw new AbortError(`Token refresh failed for ${options.store} (HTTP ${response.status}).`) + } + + let parsed: StoreTokenResponse + try { + parsed = JSON.parse(body) as StoreTokenResponse + } catch { + throw new AbortError('Received an invalid refresh response from Shopify.') + } + + if (!parsed.access_token) { + throw new AbortError(`Token refresh returned an invalid response for ${options.store}.`) + } + + return { + accessToken: parsed.access_token, + refreshToken: parsed.refresh_token, + expiresIn: parsed.expires_in, + refreshTokenExpiresIn: parsed.refresh_token_expires_in, + } +} + +const CurrentAppInstallationAccessScopesQuery = `#graphql + query CurrentAppInstallationAccessScopes { + currentAppInstallation { + accessScopes { + handle + } + } + } +` + +export async function fetchCurrentStoreAuthScopes(options: {store: string; accessToken: string}): Promise { + outputDebug( + outputContent`Fetching current app installation scopes for ${outputToken.raw(options.store)} using token ${outputToken.raw(maskToken(options.accessToken))}`, + ) + + const data = await graphqlRequest({ + query: CurrentAppInstallationAccessScopesQuery, + api: 'Admin', + url: adminUrl(options.store, 'unstable'), + token: options.accessToken, + responseOptions: {handleErrors: false}, + }) + + if (!Array.isArray(data.currentAppInstallation?.accessScopes)) { + throw new Error('Shopify did not return currentAppInstallation.accessScopes.') + } + + return data.currentAppInstallation.accessScopes.flatMap((scope) => + typeof scope.handle === 'string' ? [scope.handle] : [], + ) +} diff --git a/packages/flow-cli/src/index.ts b/packages/flow-cli/src/index.ts new file mode 100644 index 00000000000..bd0adb36577 --- /dev/null +++ b/packages/flow-cli/src/index.ts @@ -0,0 +1,48 @@ +import FlowEnvSearch from './cli/commands/env/search.js' +import FlowInit from './cli/commands/init.js' +import FlowResourceSearch from './cli/commands/resource/search.js' +import FlowShopifyqlColumns from './cli/commands/shopifyql/columns.js' +import FlowTaskDescribe from './cli/commands/task/describe.js' +import FlowTaskSearch from './cli/commands/task/search.js' +import FlowTemplateSave from './cli/commands/template/save.js' +import FlowTemplateSearch from './cli/commands/template/search.js' +import FlowTypeShow from './cli/commands/type/show.js' +import FlowWorkflowActivate from './cli/commands/workflow/activate.js' +import FlowWorkflowDeactivate from './cli/commands/workflow/deactivate.js' +import FlowWorkflowDiff from './cli/commands/workflow/diff.js' +import FlowWorkflowList from './cli/commands/workflow/list.js' +import FlowWorkflowPreview from './cli/commands/workflow/preview.js' +import FlowWorkflowPull from './cli/commands/workflow/pull.js' +import FlowWorkflowPush from './cli/commands/workflow/push.js' +import FlowWorkflowShow from './cli/commands/workflow/show.js' +import FlowWorkflowStatus from './cli/commands/workflow/status.js' +import FlowWorkflowValidate from './cli/commands/workflow/validate.js' + +/** + * Explicit command registry for \@shopify/flow-cli. + * Command IDs have no 'flow:' prefix — 'flow' is the binary name. + * Consumed by oclif's "explicit" strategy via package.json#oclif.commands. + */ +const COMMANDS = { + 'env:search': FlowEnvSearch, + init: FlowInit, + 'resource:search': FlowResourceSearch, + 'shopifyql:columns': FlowShopifyqlColumns, + 'task:describe': FlowTaskDescribe, + 'task:search': FlowTaskSearch, + 'template:save': FlowTemplateSave, + 'template:search': FlowTemplateSearch, + 'type:show': FlowTypeShow, + 'workflow:activate': FlowWorkflowActivate, + 'workflow:deactivate': FlowWorkflowDeactivate, + 'workflow:diff': FlowWorkflowDiff, + 'workflow:list': FlowWorkflowList, + 'workflow:preview': FlowWorkflowPreview, + 'workflow:pull': FlowWorkflowPull, + 'workflow:push': FlowWorkflowPush, + 'workflow:show': FlowWorkflowShow, + 'workflow:status': FlowWorkflowStatus, + 'workflow:validate': FlowWorkflowValidate, +} + +export default COMMANDS diff --git a/packages/flow-cli/src/main.ts b/packages/flow-cli/src/main.ts new file mode 100644 index 00000000000..7545fe5e3c5 --- /dev/null +++ b/packages/flow-cli/src/main.ts @@ -0,0 +1,143 @@ +#!/usr/bin/env node +/** + * Standalone entry point for the Flow CLI. + * + * Parses argv directly instead of going through oclif's plugin discovery / + * manifest reading, so the dist/ output works without a plugin manifest. + */ +import COMMANDS from './index.js' +import {Config} from '@oclif/core' +import {dirname} from '@shopify/cli-kit/node/path' +import {arch, homedir, platform, userInfo} from 'node:os' +import {fileURLToPath} from 'node:url' + +const VERSION = '0.0.1' +const BIN = 'flow' +const NAME = '@shopify/flow-cli' + +// ── argv routing ───────────────────────────────────────────────────────────── + +const argv = process.argv.slice(2) + +if (argv.length === 0 || argv[0] === 'help') { + printHelp() + process.exit(0) +} + +if (argv[0] === '--version' || argv[0] === '-v') { + process.stdout.write(`${BIN}/${VERSION}\n`) + process.exit(0) +} + +// Extract up to 2 non-flag tokens as command namespace + subcommand. +const tokens: string[] = [] +let tokenEnd = 0 +while (tokenEnd < argv.length && !argv[tokenEnd]!.startsWith('-') && tokens.length < 2) { + tokens.push(argv[tokenEnd]!) + tokenEnd++ +} +const cmdArgv = argv.slice(tokenEnd) + +// Try "ns:sub" first (e.g. "workflow list"), then bare "cmd" (e.g. "init"). +let commandId = tokens.join(':') +if (!(commandId in COMMANDS) && tokens.length > 1) { + commandId = tokens[0] ?? '' + cmdArgv.unshift(tokens[1]!) +} + +const CommandClass = COMMANDS[commandId as keyof typeof COMMANDS] +if (!CommandClass) { + process.stderr.write(`Unknown command: ${tokens.join(' ')}\n\n`) + printHelp() + process.exit(1) +} + +// ── minimal oclif Config ────────────────────────────────────────────────────── +// +// oclif's Command.run() needs a Config object for flag parsing and lifecycle +// hooks. We build one without any filesystem reads so this file stays +// bundle-friendly: all data is inlined at compile time. + +const root = dirname(fileURLToPath(import.meta.url)) + +// Config constructor just stores options — properties are set manually here +// instead of calling Config.load() which reads package.json / oclif.manifest.json. +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const config = new Config({root}) as any + +config.bin = BIN +config.binAliases = [] +config.binPath = undefined +config.name = NAME +config.version = VERSION +config.root = root +config.channel = 'stable' +config.valid = true +config.arch = arch() +config.platform = platform() +config.windows = platform() === 'win32' +config.shell = userInfo().shell?.split('/').pop() ?? 'unknown' +config.home = homedir() +config.dirname = NAME +config.cacheDir = `${homedir()}/.cache/${NAME}` +config.configDir = `${homedir()}/.config/${NAME}` +config.dataDir = `${homedir()}/.local/share/${NAME}` +config.userAgent = `${BIN}/${VERSION} ${platform()}-${arch()} node-${process.version}` +config.topicSeparator = ':' +config.flexibleTaxonomy = false +config.isSingleCommandCLI = false +config.plugins = new Map() +config._commands = new Map() +config._topics = new Map() +config.pjson = { + name: NAME, + version: VERSION, + description: 'Standalone Shopify Flow CLI', + keywords: [], + files: [], + dependencies: {}, + devDependencies: {}, + engines: {node: '>=18.20.0'}, + oclif: { + bin: BIN, + commands: {}, + topics: {}, + plugins: [], + hooks: {}, + }, +} + +// Flow CLI has no lifecycle hooks — stub runHook to be a no-op. +config.runHook = async () => ({successes: [], failures: []}) + +// Defer to the next event-loop tick so Rollup's single-file bundle can finish +// all module-level var initializations (which follow this entry-point block in +// the output) before the command runs. +setImmediate(async () => { + try { + await CommandClass.run(cmdArgv, config) + } catch (error: unknown) { + // oclif throws ExitError with code 0 after displaying help — treat it as a clean exit. + const exit = (error as {oclif?: {exit?: number}})?.oclif?.exit + if (exit === 0) process.exit(0) + throw error + } +}) + +// ── help ────────────────────────────────────────────────────────────────────── + +function printHelp(): void { + const commands = Object.keys(COMMANDS).sort() + process.stdout.write( + [ + `${BIN} — Shopify Flow CLI (v${VERSION})`, + '', + 'Usage:', + ` ${BIN} [args] [flags]`, + '', + 'Commands:', + ...commands.map((id) => ` ${id}`), + '', + ].join('\n'), + ) +} diff --git a/packages/flow-cli/src/stubs/react-devtools-core.mjs b/packages/flow-cli/src/stubs/react-devtools-core.mjs new file mode 100644 index 00000000000..b64deb2572c --- /dev/null +++ b/packages/flow-cli/src/stubs/react-devtools-core.mjs @@ -0,0 +1 @@ +export default {initialize() {}, connectToDevTools() {}} diff --git a/packages/flow-cli/src/stubs/toml-patch.cjs b/packages/flow-cli/src/stubs/toml-patch.cjs new file mode 100644 index 00000000000..834a305f926 --- /dev/null +++ b/packages/flow-cli/src/stubs/toml-patch.cjs @@ -0,0 +1,3 @@ +// flow-cli never patches TOML files; stub avoids loading the WASM binary. +module.exports.echoToml = (s) => s +module.exports.updateTomlValues = (s) => s diff --git a/packages/flow-cli/tsconfig.json b/packages/flow-cli/tsconfig.json new file mode 100644 index 00000000000..ea7490fa22f --- /dev/null +++ b/packages/flow-cli/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../configurations/tsconfig.json", + "include": ["./src/**/*.ts"], + "exclude": ["./dist"], + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" + }, + "references": [ + {"path": "../cli-kit"} + ] +} diff --git a/packages/store/src/cli/commands/flow/env/search.ts b/packages/store/src/cli/commands/flow/env/search.ts new file mode 100644 index 00000000000..307b937098d --- /dev/null +++ b/packages/store/src/cli/commands/flow/env/search.ts @@ -0,0 +1,88 @@ +import {loadConfig} from '../../../services/flow/project-config.js' +import {dispatchFlowTool, unwrapJsonResult} from '../../../services/flow/dispatch.js' +import StoreCommand from '../../../utilities/store-command.js' +import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli' +import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn' +import {AbortError} from '@shopify/cli-kit/node/error' +import {outputResult} from '@shopify/cli-kit/node/output' +import {Args, Flags} from '@oclif/core' + +const DATA_TYPE_FILTERS = ['LIST', 'ALL'] as const +type DataTypeFilter = (typeof DATA_TYPE_FILTERS)[number] + +export default class FlowEnvSearch extends StoreCommand { + static hidden = true + + static summary = 'Discover Flow environment field paths.' + + static descriptionWithMarkdown = `Searches the Flow environment for field paths under a given Admin API root type. Use this before writing conditions, Liquid, or task config that reference \`{{ order.customer.email }}\`-style paths. + +Pass a single root_type and search term as positional args. Use \`--workflow-id\` to pin the search to a specific workflow's Admin API version.` + + static description = this.descriptionWithoutMarkdown() + + static examples = [ + '<%= config.bin %> <%= command.id %> Order "customer email"', + '<%= config.bin %> <%= command.id %> Product tags --data-type LIST', + ] + + static args = { + 'root-type': Args.string({ + description: "The Admin API type name to search within (e.g. 'Order', 'Product', 'Customer').", + required: true, + }), + 'search-term': Args.string({ + description: "The search term to look for (e.g. 'email', 'customer name of order').", + required: true, + }), + } + + static flags = { + ...globalFlags, + ...jsonFlag, + store: Flags.string({ + char: 's', + description: 'The myshopify.com domain. Falls back to flow.toml.', + env: 'SHOPIFY_FLAG_STORE', + parse: async (input) => normalizeStoreFqdn(input), + }), + 'data-type': Flags.string({ + description: "Filter by data type. 'LIST' for array paths only, 'ALL' for all scalar-ending types.", + options: [...DATA_TYPE_FILTERS], + default: 'ALL', + }), + 'workflow-id': Flags.string({ + description: "Pin the search to a workflow's Admin API version.", + env: 'SHOPIFY_FLAG_FLOW_WORKFLOW_ID', + }), + 'workflow-version': Flags.string({ + description: 'Workflow definition version (paired with --workflow-id).', + env: 'SHOPIFY_FLAG_FLOW_WORKFLOW_VERSION', + }), + } + + public async run(): Promise { + const {args, flags} = await this.parse(FlowEnvSearch) + + const store = flags.store ?? (await loadConfig())?.store + if (!store) { + throw new AbortError('No store. Pass --store, or run `shopify flow init` to create a flow.toml.') + } + + const toolArgs: Record = { + searches: [{root_type: args['root-type'], search_term: args['search-term']}], + data_type_filter: flags['data-type'] as DataTypeFilter, + } + if (flags['workflow-id']) toolArgs.workflow_id = flags['workflow-id'] + if (flags['workflow-version']) toolArgs.workflow_version = flags['workflow-version'] + + const response = await dispatchFlowTool({ + name: 'flow_app_agent_environment_paths_search', + source: 'flow', + store, + args: toolArgs, + }) + + outputResult(JSON.stringify(unwrapJsonResult(response), null, 2)) + } +} diff --git a/packages/store/src/cli/commands/flow/init.ts b/packages/store/src/cli/commands/flow/init.ts new file mode 100644 index 00000000000..65ba778438f --- /dev/null +++ b/packages/store/src/cli/commands/flow/init.ts @@ -0,0 +1,68 @@ +import {writeConfig} from '../../services/flow/project-config.js' +import StoreCommand from '../../utilities/store-command.js' +import {globalFlags} from '@shopify/cli-kit/node/cli' +import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn' +import {mkdir} from '@shopify/cli-kit/node/fs' +import {outputResult} from '@shopify/cli-kit/node/output' +import {cwd, resolvePath} from '@shopify/cli-kit/node/path' +import {Args, Flags} from '@oclif/core' + +export default class FlowInit extends StoreCommand { + static hidden = true + + static summary = 'Initialize a Flow IaC project (creates the directory and writes flow.toml).' + + static descriptionWithMarkdown = `Writes a \`flow.toml\` with the store handle and workflow directory. Pass a path to create (or use) a project directory; defaults to the current directory. + +Lifecycle commands (\`workflow validate/push/pull/diff/activate/deactivate/status\`) read this file when their --store and --workflows-dir flags aren't passed. Refuses to overwrite an existing file unless --force is set.` + + static description = this.descriptionWithoutMarkdown() + + static examples = [ + '<%= config.bin %> <%= command.id %> --store shop.myshopify.com', + '<%= config.bin %> <%= command.id %> ./shop1 --store shop.myshopify.com', + '<%= config.bin %> <%= command.id %> ./shop1 --store shop.myshopify.com --workflows-dir flows --force', + ] + + static args = { + path: Args.string({ + description: 'Project directory to create (or reuse). Defaults to the current directory.', + required: false, + }), + } + + static flags = { + ...globalFlags, + store: Flags.string({ + char: 's', + description: 'The myshopify.com domain of the store this project tracks.', + env: 'SHOPIFY_FLAG_STORE', + parse: async (input) => normalizeStoreFqdn(input), + required: true, + }), + 'workflows-dir': Flags.string({ + description: 'Directory (relative to flow.toml) where workflow JSON files live. Defaults to `workflows`.', + env: 'SHOPIFY_FLAG_FLOW_WORKFLOWS_DIR', + }), + force: Flags.boolean({ + description: 'Overwrite an existing flow.toml.', + default: false, + }), + } + + public async run(): Promise { + const {args, flags} = await this.parse(FlowInit) + + const dir = args.path ? resolvePath(cwd(), args.path) : cwd() + if (args.path) await mkdir(dir) + + const path = await writeConfig({ + dir, + store: flags.store, + workflowsDir: flags['workflows-dir'], + force: flags.force, + }) + + outputResult(`Wrote ${path}.`) + } +} diff --git a/packages/store/src/cli/commands/flow/resource/search.ts b/packages/store/src/cli/commands/flow/resource/search.ts new file mode 100644 index 00000000000..18d72300e18 --- /dev/null +++ b/packages/store/src/cli/commands/flow/resource/search.ts @@ -0,0 +1,99 @@ +import {loadConfig} from '../../../services/flow/project-config.js' +import {dispatchFlowTool} from '../../../services/flow/dispatch.js' +import StoreCommand from '../../../utilities/store-command.js' +import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli' +import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn' +import {AbortError} from '@shopify/cli-kit/node/error' +import {outputResult} from '@shopify/cli-kit/node/output' +import {Args, Flags} from '@oclif/core' + +const RESOURCE_TYPES = [ + 'PRODUCT', + 'PRODUCT_VARIANT', + 'CUSTOMER', + 'SEGMENT', + 'COLLECTION', + 'ORDER', + 'DRAFT_ORDER', + 'APP_INSTALLATION', + 'ARTICLE', + 'BLOG', + 'PAGE', + 'DISCOUNT', + 'LOCATION', + 'MARKETING_AUTOMATION', + 'METAOBJECT_DEFINITION', + 'METAOBJECT', + 'COMPANY', + 'COMPANY_LOCATION', + 'PRODUCT_NETWORK_ORDER', +] as const + +export default class FlowResourceSearch extends StoreCommand { + static hidden = true + + static summary = 'Search for a Shopify resource by query (Sidekick-routed).' + + static descriptionWithMarkdown = `Searches the Admin API for a specific resource type. Returns title, GID, and path. \`PRODUCT_VARIANT\` also returns inventory_item_id; \`LOCATION\` also returns inventory_group_id and inventory_group_name. + +Routes through Sidekick — uses the \`shop.admin.graphql\` Identity scope, not Flow's own scope.` + + static description = this.descriptionWithoutMarkdown() + + static examples = [ + '<%= config.bin %> <%= command.id %> PRODUCT shoes', + '<%= config.bin %> <%= command.id %> CUSTOMER "" --limit 5', + ] + + static args = { + type: Args.string({ + description: 'Resource type to search.', + required: true, + options: [...RESOURCE_TYPES], + }), + query: Args.string({ + description: 'Query string. Wildcards (* or ?) are not supported. Empty string returns up to --limit.', + required: true, + }), + } + + static flags = { + ...globalFlags, + ...jsonFlag, + store: Flags.string({ + char: 's', + description: 'The myshopify.com domain. Falls back to flow.toml.', + env: 'SHOPIFY_FLAG_STORE', + parse: async (input) => normalizeStoreFqdn(input), + }), + limit: Flags.integer({ + description: 'Number of resources to return (default 10, max 50).', + min: 1, + max: 50, + }), + } + + public async run(): Promise { + const {args, flags} = await this.parse(FlowResourceSearch) + + const store = flags.store ?? (await loadConfig())?.store + if (!store) { + throw new AbortError('No store. Pass --store, or run `shopify flow init` to create a flow.toml.') + } + + const toolArgs: Record = { + resource_type: args.type, + query: args.query, + } + if (flags.limit !== undefined) toolArgs.limit = flags.limit + + const result = await dispatchFlowTool({ + name: 'flow_app_agent_search_shop_resource', + source: 'sk', + store, + args: toolArgs, + }) + + outputResult(JSON.stringify(result, null, 2)) + } +} diff --git a/packages/store/src/cli/commands/flow/shopifyql/columns.ts b/packages/store/src/cli/commands/flow/shopifyql/columns.ts new file mode 100644 index 00000000000..f5978d5cde4 --- /dev/null +++ b/packages/store/src/cli/commands/flow/shopifyql/columns.ts @@ -0,0 +1,56 @@ +import {loadConfig} from '../../../services/flow/project-config.js' +import {dispatchFlowTool, unwrapJsonResult} from '../../../services/flow/dispatch.js' +import StoreCommand from '../../../utilities/store-command.js' +import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli' +import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn' +import {AbortError} from '@shopify/cli-kit/node/error' +import {outputResult} from '@shopify/cli-kit/node/output' +import {Args, Flags} from '@oclif/core' + +export default class FlowShopifyqlColumns extends StoreCommand { + static hidden = true + + static summary = 'Resolve the columns a ShopifyQL query produces.' + + static descriptionWithMarkdown = + 'Given a ShopifyQL query string, returns the column names + types the query will produce. These are the fields Flow would add to the environment when the query runs.' + + static description = this.descriptionWithoutMarkdown() + + static examples = [ + '<%= config.bin %> <%= command.id %> "FROM sales SHOW gross_sales BY product_title SINCE -7d"', + ] + + static args = { + query: Args.string({description: 'A ShopifyQL query string.', required: true}), + } + + static flags = { + ...globalFlags, + ...jsonFlag, + store: Flags.string({ + char: 's', + description: 'The myshopify.com domain. Falls back to flow.toml.', + env: 'SHOPIFY_FLAG_STORE', + parse: async (input) => normalizeStoreFqdn(input), + }), + } + + public async run(): Promise { + const {args, flags} = await this.parse(FlowShopifyqlColumns) + + const store = flags.store ?? (await loadConfig())?.store + if (!store) { + throw new AbortError('No store. Pass --store, or run `shopify flow init` to create a flow.toml.') + } + + const response = await dispatchFlowTool({ + name: 'flow_app_agent_shopifyql_query_fields', + source: 'flow', + store, + args: {query: args.query}, + }) + + outputResult(JSON.stringify(unwrapJsonResult(response), null, 2)) + } +} diff --git a/packages/store/src/cli/commands/flow/task/describe.ts b/packages/store/src/cli/commands/flow/task/describe.ts new file mode 100644 index 00000000000..462549c22d5 --- /dev/null +++ b/packages/store/src/cli/commands/flow/task/describe.ts @@ -0,0 +1,160 @@ +import {loadConfig} from '../../../services/flow/project-config.js' +import {dispatchFlowTool, unwrapJsonResult} from '../../../services/flow/dispatch.js' +import StoreCommand from '../../../utilities/store-command.js' +import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli' +import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn' +import {AbortError} from '@shopify/cli-kit/node/error' +import {outputResult} from '@shopify/cli-kit/node/output' +import {Args, Flags} from '@oclif/core' + +interface TaskRef { + id: string + version: string +} + +interface TaskConfiguration { + id: string + version: string + task_type: string + input_port_id?: string + output_port_ids?: string[] + config_fields: unknown[] + return_fields: unknown[] + config_instructions?: Record + error?: string +} + +interface TaskConfigurationResponse { + tasks: TaskConfiguration[] +} + +function parseTaskRef(input: string): TaskRef { + const at = input.lastIndexOf('@') + if (at === -1) { + throw new AbortError( + `Invalid task reference "${input}".`, + 'Use the form `@`, e.g. `shopify::admin::order_created@0.1`.', + ) + } + const id = input.slice(0, at) + const version = input.slice(at + 1) + if (!id || !version) { + throw new AbortError(`Invalid task reference "${input}".`, 'Both id and version are required.') + } + return {id, version} +} + +interface ConfigField { + id?: string + label?: string + description?: string + default_value?: string + validations?: {id?: string; options?: Record; error_message?: string}[] + possible_object_types?: string[] + supports_liquid?: boolean +} + +function formatHuman(tasks: TaskConfiguration[]): string { + return tasks.map(formatTask).join('\n\n---\n\n') +} + +function formatTask(task: TaskConfiguration): string { + const lines: string[] = [] + lines.push(`${task.id}@${task.version} (${task.task_type})`) + if (task.error) { + lines.push(` error: ${task.error}`) + return lines.join('\n') + } + if (task.input_port_id) lines.push(` input port: ${task.input_port_id}`) + if (task.output_port_ids?.length) lines.push(` output ports: ${task.output_port_ids.join(', ')}`) + + if (task.config_fields.length === 0) { + lines.push(' config fields: (none)') + } else { + lines.push(' config fields:') + for (const field of task.config_fields as ConfigField[]) { + const wrapHint = needsValueWrapper(field) ? ' [wrap value: {"value": "...", "default_value": ""}]' : '' + const liquidHint = field.supports_liquid ? ' [supports Liquid]' : '' + lines.push(` - ${field.id ?? '?'}${field.label ? ` — ${field.label}` : ''}${wrapHint}${liquidHint}`) + if (field.description) lines.push(` ${field.description}`) + } + } + + if (task.return_fields.length === 0) { + lines.push(' return fields: (none)') + } else { + lines.push(` return fields: ${task.return_fields.length} (use --json for full schema)`) + } + + return lines.join('\n') +} + +function needsValueWrapper(field: ConfigField): boolean { + return Boolean(field.validations?.some((validation) => (validation.options as {subfield?: string})?.subfield === 'value')) +} + +export default class FlowTaskDescribe extends StoreCommand { + static hidden = true + + static summary = 'Describe one or more Flow tasks: configuration and return schemas.' + + static descriptionWithMarkdown = `Returns the full configuration schema and return-field definitions for one or more Flow tasks. + +Each task is identified as \`@\` (e.g. \`shopify::admin::order_created@0.1\`).` + + static description = this.descriptionWithoutMarkdown() + + static examples = [ + '<%= config.bin %> <%= command.id %> shopify::admin::order_created@0.1', + '<%= config.bin %> <%= command.id %> shopify::admin::order_created@0.1 shopify::admin::add_order_tags@0.1', + ] + + static args = { + task: Args.string({ + description: 'Task reference as `@` (one or more).', + required: true, + }), + } + + static strict = false + + static flags = { + ...globalFlags, + ...jsonFlag, + store: Flags.string({ + char: 's', + description: 'The myshopify.com domain. Falls back to flow.toml.', + env: 'SHOPIFY_FLAG_STORE', + parse: async (input) => normalizeStoreFqdn(input), + }), + } + + public async run(): Promise { + const {argv, flags} = await this.parse(FlowTaskDescribe) + const refs = (argv as string[]).map(parseTaskRef) + + if (refs.length === 0) throw new AbortError('At least one task reference is required.') + + const store = flags.store ?? (await loadConfig())?.store + if (!store) { + throw new AbortError('No store. Pass --store, or run `shopify flow init` to create a flow.toml.') + } + + const response = await dispatchFlowTool({ + name: 'flow_app_agent_task_configuration', + source: 'flow', + store, + args: {tasks: refs}, + }) + + const raw = unwrapJsonResult(response) + const tasks = raw.tasks ?? [] + + if (flags.json) { + outputResult(JSON.stringify({tasks}, null, 2)) + return + } + + outputResult(formatHuman(tasks)) + } +} diff --git a/packages/store/src/cli/commands/flow/task/search.ts b/packages/store/src/cli/commands/flow/task/search.ts new file mode 100644 index 00000000000..f561719b1fc --- /dev/null +++ b/packages/store/src/cli/commands/flow/task/search.ts @@ -0,0 +1,152 @@ +import {loadConfig} from '../../../services/flow/project-config.js' +import {dispatchFlowTool, unwrapJsonResult} from '../../../services/flow/dispatch.js' +import StoreCommand from '../../../utilities/store-command.js' +import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli' +import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn' +import {AbortError} from '@shopify/cli-kit/node/error' +import {outputResult} from '@shopify/cli-kit/node/output' +import {Args, Flags} from '@oclif/core' + +const TASK_TYPES = ['trigger', 'action', 'condition', 'foreach', 'wait'] as const +type TaskTypeFilter = (typeof TASK_TYPES)[number] + +interface RawTask { + id: string + version: string + label: string + description?: string + task_type: string + connector_id?: string + installed?: boolean + publisher?: string +} + +interface RawTaskSearchResult { + tasks: Record +} + +interface FlatTask extends RawTask { + matched_queries: string[] +} + +interface SearchResult { + store: string + total: number + tasks: FlatTask[] + by_query: Record +} + +export default class FlowTaskSearch extends StoreCommand { + static hidden = true + + static summary = 'Search Flow tasks (triggers, conditions, actions, etc.).' + + static descriptionWithMarkdown = `Finds Flow tasks (triggers, conditions, actions, foreach, wait) by natural-language query. Pass each goal as a separate positional argument. + +Returns task references (id + version) you can pass to \`flow task describe\` to get the full configuration schema.` + + static description = this.descriptionWithoutMarkdown() + + static examples = [ + '<%= config.bin %> <%= command.id %> "order created trigger"', + '<%= config.bin %> <%= command.id %> "add order tags" "send email"', + '<%= config.bin %> <%= command.id %> "tagging" --type action', + ] + + static args = { + query: Args.string({description: 'Search query (one or more, space-separated).', required: true}), + } + + static strict = false + + static flags = { + ...globalFlags, + ...jsonFlag, + store: Flags.string({ + char: 's', + description: 'The myshopify.com domain. Falls back to flow.toml.', + env: 'SHOPIFY_FLAG_STORE', + parse: async (input) => normalizeStoreFqdn(input), + }), + type: Flags.string({ + description: 'Filter by task type.', + options: [...TASK_TYPES], + }), + } + + public async run(): Promise { + const {argv, flags} = await this.parse(FlowTaskSearch) + const queries = argv as string[] + + if (queries.length === 0) throw new AbortError('At least one search query is required.') + + const store = flags.store ?? (await loadConfig())?.store + if (!store) { + throw new AbortError('No store. Pass --store, or run `shopify flow init` to create a flow.toml.') + } + + const response = await dispatchFlowTool({ + name: 'flow_app_agent_task_search', + source: 'flow', + store, + args: {search_queries: queries}, + }) + + const raw = unwrapJsonResult(response) + const result = shapeResult(store, raw, flags.type as TaskTypeFilter | undefined) + + if (flags.json) { + outputResult(JSON.stringify(result, null, 2)) + return + } + + outputResult(formatHuman(result)) + } +} + +function shapeResult(store: string, raw: RawTaskSearchResult, typeFilter: TaskTypeFilter | undefined): SearchResult { + const byId = new Map() + const byQuery: Record = {} + + const wantedType = typeFilter?.toUpperCase() + + for (const [query, tasks] of Object.entries(raw.tasks ?? {})) { + const filtered = wantedType ? tasks.filter((task) => task.task_type === wantedType) : tasks + byQuery[query] = filtered + + for (const task of filtered) { + const key = `${task.id}@${task.version}` + const existing = byId.get(key) + if (existing) { + if (!existing.matched_queries.includes(query)) existing.matched_queries.push(query) + } else { + byId.set(key, {...task, matched_queries: [query]}) + } + } + } + + const tasks = [...byId.values()].sort((left, right) => left.id.localeCompare(right.id)) + return {store, total: tasks.length, tasks, by_query: byQuery} +} + +function formatHuman(result: SearchResult): string { + if (result.tasks.length === 0) return `No tasks matched on ${result.store}.` + + const idWidth = Math.max(2, ...result.tasks.map((task) => `${task.id}@${task.version}`.length)) + const typeWidth = Math.max(4, ...result.tasks.map((task) => task.task_type.length)) + const pubWidth = Math.max(3, ...result.tasks.map((task) => (task.publisher ?? '?').length)) + const labelWidth = Math.max(5, ...result.tasks.map((task) => task.label.length)) + + const lines = [ + `${'TYPE'.padEnd(typeWidth)} ${'PUB'.padEnd(pubWidth)} ${'ID@VERSION'.padEnd(idWidth)} ${'LABEL'.padEnd( + labelWidth, + )}`, + ...result.tasks.map( + (task) => + `${task.task_type.padEnd(typeWidth)} ${(task.publisher ?? '?').padEnd(pubWidth)} ${`${task.id}@${task.version}` + .padEnd(idWidth)} ${task.label.padEnd(labelWidth)}`, + ), + ] + + return lines.join('\n') +} diff --git a/packages/store/src/cli/commands/flow/template/save.ts b/packages/store/src/cli/commands/flow/template/save.ts new file mode 100644 index 00000000000..ae058343b78 --- /dev/null +++ b/packages/store/src/cli/commands/flow/template/save.ts @@ -0,0 +1,135 @@ +import {dispatchFlowTool} from '../../../services/flow/dispatch.js' +import {loadConfig, workflowsDirAbsolute} from '../../../services/flow/project-config.js' +import {writeWorkflowFile, type WorkflowJson} from '../../../services/flow/workflow-lifecycle.js' +import StoreCommand from '../../../utilities/store-command.js' +import {globalFlags} from '@shopify/cli-kit/node/cli' +import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn' +import {AbortError} from '@shopify/cli-kit/node/error' +import {fileExists, mkdir} from '@shopify/cli-kit/node/fs' +import {outputResult} from '@shopify/cli-kit/node/output' +import {cwd, joinPath, resolvePath} from '@shopify/cli-kit/node/path' +import {Args, Flags} from '@oclif/core' + +interface TemplateLookupResponseJson { + template_id?: string + title?: string + workflow_json?: WorkflowJson + error?: string + error_code?: string +} + +export default class FlowTemplateSave extends StoreCommand { + static hidden = true + + static summary = 'Save a Flow template into your IaC project as a new workflow.' + + static descriptionWithMarkdown = `Fetches the template by ID (always the leading version) and writes its \`workflow_json\` to \`//workflow.flow.json\`. Sets \`root.workflow_name\` to the template's title — rename it before push if you want a different name. + +No lockfile is written; the file is "new" until you \`push\` it. + +Get a template ID from \`shopify flow template search\` — each result carries a stable \`template_id\` field.` + + static description = this.descriptionWithoutMarkdown() + + static examples = [ + '<%= config.bin %> <%= command.id %> 01HQK000000000000000000000 --as fraud-prevention', + '<%= config.bin %> <%= command.id %> 01HQK... --as high-value-orders --force', + ] + + static args = { + 'template-id': Args.string({ + description: 'Template identifier from a prior `flow template search` result.', + required: true, + }), + } + + static flags = { + ...globalFlags, + store: Flags.string({ + char: 's', + description: 'The myshopify.com domain. Falls back to flow.toml.', + env: 'SHOPIFY_FLAG_STORE', + parse: async (input) => normalizeStoreFqdn(input), + }), + as: Flags.string({ + char: 'a', + description: 'Slug for the new workflow folder. Creates workflows//workflow.flow.json.', + required: true, + }), + 'workflows-dir': Flags.string({ + description: 'Workflows directory (relative to cwd). Falls back to flow.toml.', + env: 'SHOPIFY_FLAG_FLOW_WORKFLOWS_DIR', + }), + force: Flags.boolean({ + description: 'Overwrite an existing workflow.flow.json at the target path.', + default: false, + }), + } + + public async run(): Promise { + const {args, flags} = await this.parse(FlowTemplateSave) + + const config = await loadConfig() + const store = flags.store ?? config?.store + if (!store) { + throw new AbortError('No store. Pass --store, or run `shopify flow init` to create a flow.toml.') + } + + const dir = flags['workflows-dir'] + ? resolvePath(cwd(), flags['workflows-dir']) + : config + ? workflowsDirAbsolute(config) + : undefined + if (!dir) { + throw new AbortError( + 'No workflows directory. Pass --workflows-dir, or run `shopify flow init` to create a flow.toml.', + ) + } + + const targetDir = joinPath(dir, flags.as) + const filePath = joinPath(targetDir, 'workflow.flow.json') + + if ((await fileExists(filePath)) && !flags.force) { + throw new AbortError(`${filePath} already exists.`, 'Pass --force to overwrite.') + } + + const response = await dispatchFlowTool({ + name: 'flow_app_agent_template_lookup', + source: 'flow', + store, + args: {template_id: args['template-id']}, + }) + + const json = unwrapJsonResult(response) + if (json.error) { + throw new AbortError(`Template lookup failed: ${json.error}`, json.error_code) + } + if (!json.workflow_json) { + throw new AbortError(`Template ${args['template-id']} has no workflow_json.`) + } + + const payload: WorkflowJson = json.workflow_json + const root = payload.root as Record | undefined + if (root && !root.workflow_name) { + root.workflow_name = json.title ?? flags.as + } + + await mkdir(targetDir) + await writeWorkflowFile(filePath, payload) + + const lines = [ + `Saved "${json.title ?? args['template-id']}" to ${filePath}.`, + '', + 'Next:', + ` shopify flow workflow validate ${filePath}`, + ` shopify flow workflow push ${filePath} # creates lockfile`, + ] + outputResult(lines.join('\n')) + } +} + +function unwrapJsonResult(response: unknown): TemplateLookupResponseJson { + const typed = response as {ok: boolean; data?: TemplateLookupResponseJson} | undefined + if (!typed?.data) throw new AbortError('Unexpected response shape from template_lookup.') + return typed.data +} diff --git a/packages/store/src/cli/commands/flow/template/search.ts b/packages/store/src/cli/commands/flow/template/search.ts new file mode 100644 index 00000000000..a7559a6e0b1 --- /dev/null +++ b/packages/store/src/cli/commands/flow/template/search.ts @@ -0,0 +1,63 @@ +import {loadConfig} from '../../../services/flow/project-config.js' +import {dispatchFlowTool, unwrapJsonResult} from '../../../services/flow/dispatch.js' +import StoreCommand from '../../../utilities/store-command.js' +import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli' +import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn' +import {AbortError} from '@shopify/cli-kit/node/error' +import {outputResult} from '@shopify/cli-kit/node/output' +import {Args, Flags} from '@oclif/core' + +export default class FlowTemplateSearch extends StoreCommand { + static hidden = true + + static summary = 'Search Flow templates by business goal.' + + static descriptionWithMarkdown = `Searches Flow's template catalog for workflows matching one or more business goals. Pass each goal as a separate positional argument. + +The result is a JSON object keyed by query, with templates and their workflow_json structures. Use a returned template's workflow_json as the starting point for a new workflow.` + + static description = this.descriptionWithoutMarkdown() + + static examples = [ + '<%= config.bin %> <%= command.id %> "fraud prevention"', + '<%= config.bin %> <%= command.id %> "fraud prevention" "high risk orders"', + ] + + static args = { + query: Args.string({description: 'Search query (one or more, space-separated).', required: true}), + } + + static strict = false + + static flags = { + ...globalFlags, + ...jsonFlag, + store: Flags.string({ + char: 's', + description: 'The myshopify.com domain. Falls back to flow.toml.', + env: 'SHOPIFY_FLAG_STORE', + parse: async (input) => normalizeStoreFqdn(input), + }), + } + + public async run(): Promise { + const {argv, flags} = await this.parse(FlowTemplateSearch) + const queries = argv as string[] + + if (queries.length === 0) throw new AbortError('At least one search query is required.') + + const store = flags.store ?? (await loadConfig())?.store + if (!store) { + throw new AbortError('No store. Pass --store, or run `shopify flow init` to create a flow.toml.') + } + + const response = await dispatchFlowTool({ + name: 'flow_app_agent_template_search', + source: 'flow', + store, + args: {search_queries: queries}, + }) + + outputResult(JSON.stringify(unwrapJsonResult(response), null, 2)) + } +} diff --git a/packages/store/src/cli/commands/flow/type/show.ts b/packages/store/src/cli/commands/flow/type/show.ts new file mode 100644 index 00000000000..568a74ff120 --- /dev/null +++ b/packages/store/src/cli/commands/flow/type/show.ts @@ -0,0 +1,69 @@ +import {loadConfig} from '../../../services/flow/project-config.js' +import {dispatchFlowTool, unwrapJsonResult} from '../../../services/flow/dispatch.js' +import StoreCommand from '../../../utilities/store-command.js' +import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli' +import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn' +import {AbortError} from '@shopify/cli-kit/node/error' +import {outputResult} from '@shopify/cli-kit/node/output' +import {Args, Flags} from '@oclif/core' + +export default class FlowTypeShow extends StoreCommand { + static hidden = true + + static summary = "Show a GraphQL type's structure (fields, arguments, return types)." + + static descriptionWithMarkdown = + 'Prints the field structure, argument signatures, and return types for a single Admin API GraphQL type. Use to understand the shape of a type before referencing its fields in workflow JSON.' + + static description = this.descriptionWithoutMarkdown() + + static examples = [ + '<%= config.bin %> <%= command.id %> Product', + '<%= config.bin %> <%= command.id %> Order --workflow-id 01HQK...', + ] + + static args = { + type: Args.string({description: 'The GraphQL type name to inspect.', required: true}), + } + + static flags = { + ...globalFlags, + ...jsonFlag, + store: Flags.string({ + char: 's', + description: 'The myshopify.com domain. Falls back to flow.toml.', + env: 'SHOPIFY_FLAG_STORE', + parse: async (input) => normalizeStoreFqdn(input), + }), + 'workflow-id': Flags.string({ + description: "Pin the lookup to a workflow's Admin API version.", + env: 'SHOPIFY_FLAG_FLOW_WORKFLOW_ID', + }), + 'workflow-version': Flags.string({ + description: 'Workflow definition version (paired with --workflow-id).', + env: 'SHOPIFY_FLAG_FLOW_WORKFLOW_VERSION', + }), + } + + public async run(): Promise { + const {args, flags} = await this.parse(FlowTypeShow) + + const store = flags.store ?? (await loadConfig())?.store + if (!store) { + throw new AbortError('No store. Pass --store, or run `shopify flow init` to create a flow.toml.') + } + + const toolArgs: Record = {type_ref_name: args.type} + if (flags['workflow-id']) toolArgs.workflow_id = flags['workflow-id'] + if (flags['workflow-version']) toolArgs.workflow_version = flags['workflow-version'] + + const response = await dispatchFlowTool({ + name: 'flow_app_agent_object_type_definition_search', + source: 'flow', + store, + args: toolArgs, + }) + + outputResult(JSON.stringify(unwrapJsonResult(response), null, 2)) + } +} diff --git a/packages/store/src/cli/commands/flow/workflow/activate.ts b/packages/store/src/cli/commands/flow/workflow/activate.ts new file mode 100644 index 00000000000..aa4fb0cf187 --- /dev/null +++ b/packages/store/src/cli/commands/flow/workflow/activate.ts @@ -0,0 +1,93 @@ +import {loadConfig} from '../../../services/flow/project-config.js' +import {activateWorkflow} from '../../../services/flow/workflow-lifecycle.js' +import StoreCommand from '../../../utilities/store-command.js' +import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli' +import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn' +import {AbortError} from '@shopify/cli-kit/node/error' +import {outputResult} from '@shopify/cli-kit/node/output' +import {resolvePath} from '@shopify/cli-kit/node/path' +import {Args, Flags} from '@oclif/core' + +export default class FlowWorkflowActivate extends StoreCommand { + static hidden = true + + static summary = 'Activate a workflow on a shop.' + + static descriptionWithMarkdown = `Activates a workflow definition. Three input modes: + +- positional file path: read lockfile sibling for workflow_id and workflow_definition_version +- \`--workflow-id X --workflow-version Y\`: activate that exact pair +- \`--workflow-id X --use-latest\`: resolve the current main version via lookup, then activate + +Hidden workflows must be unhidden first (push with hidden:false).` + + static description = this.descriptionWithoutMarkdown() + + static examples = [ + '<%= config.bin %> <%= command.id %> ./workflow.flow.json --store shop.myshopify.com', + '<%= config.bin %> <%= command.id %> --workflow-id 01HQK... --workflow-version 01HQL... --store shop.myshopify.com', + '<%= config.bin %> <%= command.id %> --workflow-id 01HQK... --use-latest --store shop.myshopify.com', + ] + + static args = { + file: Args.string({ + description: 'Optional path to the workflow JSON file (lockfile drives activation).', + required: false, + parse: async (input) => resolvePath(input), + }), + } + + static flags = { + ...globalFlags, + ...jsonFlag, + store: Flags.string({ + char: 's', + description: 'The myshopify.com domain. Falls back to the `store` field in flow.toml.', + env: 'SHOPIFY_FLAG_STORE', + parse: async (input) => normalizeStoreFqdn(input), + }), + 'workflow-id': Flags.string({ + description: 'Workflow ID (escape hatch when no lockfile).', + env: 'SHOPIFY_FLAG_FLOW_WORKFLOW_ID', + }), + 'workflow-version': Flags.string({ + description: 'Workflow definition version. Required with --workflow-id unless --use-latest.', + env: 'SHOPIFY_FLAG_FLOW_WORKFLOW_VERSION', + }), + 'use-latest': Flags.boolean({ + description: 'Resolve the latest main version via workflow_lookup.', + default: false, + }), + } + + public async run(): Promise { + const {args, flags} = await this.parse(FlowWorkflowActivate) + + if (flags['workflow-id'] && !flags['workflow-version'] && !flags['use-latest']) { + throw new AbortError('--workflow-id requires --workflow-version or --use-latest.') + } + + const store = flags.store ?? (await loadConfig())?.store + if (!store) { + throw new AbortError('No store. Pass --store, or run `shopify flow init` to create a flow.toml.') + } + + const result = await activateWorkflow({ + filePath: args.file, + store, + workflowId: flags['workflow-id'], + workflowVersion: flags['workflow-version'], + useLatest: flags['use-latest'], + }) + + if (flags.json) { + outputResult(JSON.stringify(result, null, 2)) + } else if (result.error) { + outputResult(`Error: ${result.error}${result.error_code ? ` (${result.error_code})` : ''}`) + throw new AbortError('Workflow activation failed.') + } else { + const version = result.workflow_definition_version ?? result.workflow_version + outputResult(`Activated workflow ${result.workflow_id} (version ${version}).`) + } + } +} diff --git a/packages/store/src/cli/commands/flow/workflow/deactivate.ts b/packages/store/src/cli/commands/flow/workflow/deactivate.ts new file mode 100644 index 00000000000..c2b09d8d1e2 --- /dev/null +++ b/packages/store/src/cli/commands/flow/workflow/deactivate.ts @@ -0,0 +1,87 @@ +import {loadConfig} from '../../../services/flow/project-config.js' +import {deactivateWorkflow} from '../../../services/flow/workflow-lifecycle.js' +import StoreCommand from '../../../utilities/store-command.js' +import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli' +import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn' +import {AbortError} from '@shopify/cli-kit/node/error' +import {outputResult} from '@shopify/cli-kit/node/output' +import {resolvePath} from '@shopify/cli-kit/node/path' +import {Args, Flags} from '@oclif/core' + +export default class FlowWorkflowDeactivate extends StoreCommand { + static hidden = true + + static summary = 'Deactivate a workflow on a shop.' + + static descriptionWithMarkdown = + 'Deactivates a workflow definition. Same input modes as `activate`: positional file (lockfile), `--workflow-id` + `--workflow-version`, or `--workflow-id` + `--use-latest`.' + + static description = this.descriptionWithoutMarkdown() + + static examples = [ + '<%= config.bin %> <%= command.id %> ./workflow.flow.json --store shop.myshopify.com', + '<%= config.bin %> <%= command.id %> --workflow-id 01HQK... --workflow-version 01HQL... --store shop.myshopify.com', + ] + + static args = { + file: Args.string({ + description: 'Optional path to the workflow JSON file (lockfile drives deactivation).', + required: false, + parse: async (input) => resolvePath(input), + }), + } + + static flags = { + ...globalFlags, + ...jsonFlag, + store: Flags.string({ + char: 's', + description: 'The myshopify.com domain. Falls back to the `store` field in flow.toml.', + env: 'SHOPIFY_FLAG_STORE', + parse: async (input) => normalizeStoreFqdn(input), + }), + 'workflow-id': Flags.string({ + description: 'Workflow ID (escape hatch when no lockfile).', + env: 'SHOPIFY_FLAG_FLOW_WORKFLOW_ID', + }), + 'workflow-version': Flags.string({ + description: 'Workflow definition version. Required with --workflow-id unless --use-latest.', + env: 'SHOPIFY_FLAG_FLOW_WORKFLOW_VERSION', + }), + 'use-latest': Flags.boolean({ + description: 'Resolve the latest main version via workflow_lookup.', + default: false, + }), + } + + public async run(): Promise { + const {args, flags} = await this.parse(FlowWorkflowDeactivate) + + if (flags['workflow-id'] && !flags['workflow-version'] && !flags['use-latest']) { + throw new AbortError('--workflow-id requires --workflow-version or --use-latest.') + } + + const store = flags.store ?? (await loadConfig())?.store + if (!store) { + throw new AbortError('No store. Pass --store, or run `shopify flow init` to create a flow.toml.') + } + + const result = await deactivateWorkflow({ + filePath: args.file, + store, + workflowId: flags['workflow-id'], + workflowVersion: flags['workflow-version'], + useLatest: flags['use-latest'], + }) + + if (flags.json) { + outputResult(JSON.stringify(result, null, 2)) + } else if (result.error) { + outputResult(`Error: ${result.error}${result.error_code ? ` (${result.error_code})` : ''}`) + throw new AbortError('Workflow deactivation failed.') + } else { + const version = result.workflow_definition_version ?? result.workflow_version + outputResult(`Deactivated workflow ${result.workflow_id} (version ${version}).`) + } + } +} diff --git a/packages/store/src/cli/commands/flow/workflow/diff.ts b/packages/store/src/cli/commands/flow/workflow/diff.ts new file mode 100644 index 00000000000..9faeaff6b6a --- /dev/null +++ b/packages/store/src/cli/commands/flow/workflow/diff.ts @@ -0,0 +1,103 @@ +import {loadConfig} from '../../../services/flow/project-config.js' +import {diffWorkflow, normalizeWorkflowJson, readWorkflowFile, unifiedDiff} from '../../../services/flow/workflow-lifecycle.js' +import StoreCommand from '../../../utilities/store-command.js' +import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli' +import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn' +import {AbortError} from '@shopify/cli-kit/node/error' +import {outputResult} from '@shopify/cli-kit/node/output' +import {resolvePath} from '@shopify/cli-kit/node/path' +import {Args, Flags} from '@oclif/core' + +export default class FlowWorkflowDiff extends StoreCommand { + static hidden = true + + static summary = 'Show the diff between a local workflow file and the remote workflow.' + + static descriptionWithMarkdown = `Compares a local Flow workflow file against the remote workflow on the shop. Both sides are normalized (sorted keys, stable indent) so the diff reflects real changes, not formatting noise. + +Default direction is \`--- remote\` / \`+++ local\` so added lines = what \`push\` would change. + +Exit code: 0 if no differences, 1 if differences found. + +To inspect a remote workflow without a local file, use \`shopify flow workflow show \`.` + + static description = this.descriptionWithoutMarkdown() + + static examples = [ + '<%= config.bin %> <%= command.id %> ./workflow.flow.json --store shop.myshopify.com', + '<%= config.bin %> <%= command.id %> ./workflow.flow.json --json', + ] + + static args = { + file: Args.string({ + description: 'Path to the workflow JSON file.', + required: true, + parse: async (input) => resolvePath(input), + }), + } + + static flags = { + ...globalFlags, + ...jsonFlag, + store: Flags.string({ + char: 's', + description: 'The myshopify.com domain. Falls back to the `store` field in flow.toml.', + env: 'SHOPIFY_FLAG_STORE', + parse: async (input) => normalizeStoreFqdn(input), + }), + 'workflow-id': Flags.string({ + description: 'Compare against a specific workflow ID instead of the lockfile.', + env: 'SHOPIFY_FLAG_FLOW_WORKFLOW_ID', + }), + 'workflow-version': Flags.string({ + description: 'Optional definition version override.', + env: 'SHOPIFY_FLAG_FLOW_WORKFLOW_VERSION', + }), + 'local-only': Flags.boolean({ + description: 'Print the normalized local workflow only (no diff).', + default: false, + }), + } + + public async run(): Promise { + const {args, flags} = await this.parse(FlowWorkflowDiff) + + if (flags['local-only']) { + const local = await readWorkflowFile(args.file) + outputResult(normalizeWorkflowJson(local)) + return + } + + const store = flags.store ?? (await loadConfig())?.store + if (!store) { + throw new AbortError( + 'No store. Pass --store, or run `shopify flow init` to create a flow.toml.', + ) + } + + const result = await diffWorkflow({ + filePath: args.file, + store, + workflowId: flags['workflow-id'], + workflowVersion: flags['workflow-version'], + }) + + if (flags.json) { + outputResult( + JSON.stringify( + {changed: result.changed, local: result.localNormalized, remote: result.remoteNormalized}, + null, + 2, + ), + ) + } else if (result.changed) { + const remoteLabel = `remote: ${store}` + const localLabel = `local: ${args.file}` + outputResult(unifiedDiff(remoteLabel, result.remoteNormalized, localLabel, result.localNormalized)) + } + + if (result.changed) { + process.exitCode = 1 + } + } +} diff --git a/packages/store/src/cli/commands/flow/workflow/list.ts b/packages/store/src/cli/commands/flow/workflow/list.ts new file mode 100644 index 00000000000..a02c08b8012 --- /dev/null +++ b/packages/store/src/cli/commands/flow/workflow/list.ts @@ -0,0 +1,75 @@ +import {loadConfig} from '../../../services/flow/project-config.js' +import {listAllWorkflows} from '../../../services/flow/workflow-lifecycle.js' +import StoreCommand from '../../../utilities/store-command.js' +import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli' +import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn' +import {AbortError} from '@shopify/cli-kit/node/error' +import {outputResult} from '@shopify/cli-kit/node/output' +import {Flags} from '@oclif/core' + +export default class FlowWorkflowList extends StoreCommand { + static hidden = true + + static summary = 'List remote workflows on a shop.' + + static descriptionWithMarkdown = `Prints every workflow on the shop with id, name, hidden flag, and last-updated timestamp. Doesn't read or modify any local files. + +Use this to discover workflow IDs for \`shopify flow workflow show \` or \`shopify flow workflow pull --workflow-id \`. For a project-scoped view that also classifies local files, use \`shopify flow workflow status\`.` + + static description = this.descriptionWithoutMarkdown() + + static examples = [ + '<%= config.bin %> <%= command.id %>', + '<%= config.bin %> <%= command.id %> --include-hidden --json', + ] + + static flags = { + ...globalFlags, + ...jsonFlag, + store: Flags.string({ + char: 's', + description: 'The myshopify.com domain. Falls back to flow.toml.', + env: 'SHOPIFY_FLAG_STORE', + parse: async (input) => normalizeStoreFqdn(input), + }), + 'include-hidden': Flags.boolean({ + description: 'Include hidden workflows.', + default: false, + }), + } + + public async run(): Promise { + const {flags} = await this.parse(FlowWorkflowList) + + const store = flags.store ?? (await loadConfig())?.store + if (!store) { + throw new AbortError('No store. Pass --store, or run `shopify flow init` to create a flow.toml.') + } + + const workflows = await listAllWorkflows({store, includeHidden: flags['include-hidden']}) + + if (flags.json) { + outputResult(JSON.stringify({store, total: workflows.length, workflows}, null, 2)) + return + } + + if (workflows.length === 0) { + outputResult(`No workflows on ${store}.`) + return + } + + const nameWidth = Math.max(4, ...workflows.map((workflow) => workflow.name.length)) + const idWidth = Math.max(2, ...workflows.map((workflow) => workflow.workflow_id.length)) + + const lines = [ + `${'NAME'.padEnd(nameWidth)} ${'ID'.padEnd(idWidth)} HIDDEN UPDATED`, + ...workflows.map( + (workflow) => + `${workflow.name.padEnd(nameWidth)} ${workflow.workflow_id.padEnd(idWidth)} ${ + workflow.hidden ? 'yes ' : 'no ' + } ${workflow.last_updated ?? '—'}`, + ), + ] + outputResult(lines.join('\n')) + } +} diff --git a/packages/store/src/cli/commands/flow/workflow/preview.ts b/packages/store/src/cli/commands/flow/workflow/preview.ts new file mode 100644 index 00000000000..3f2b90074a3 --- /dev/null +++ b/packages/store/src/cli/commands/flow/workflow/preview.ts @@ -0,0 +1,86 @@ +import {loadConfig} from '../../../services/flow/project-config.js' +import {readWorkflowFile} from '../../../services/flow/workflow-lifecycle.js' +import {authenticateFlowStore, throwFlowAuthExpired} from '../../../services/flow/auth.js' +import StoreCommand from '../../../utilities/store-command.js' +import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli' +import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn' +import {AbortError} from '@shopify/cli-kit/node/error' +import {shopifyFetch} from '@shopify/cli-kit/node/http' +import {outputResult} from '@shopify/cli-kit/node/output' +import {resolvePath} from '@shopify/cli-kit/node/path' +import {Args, Flags} from '@oclif/core' + +const FLOW_PREVIEW_URL_PRODUCTION = 'https://flow.shopifycloud.com/flow-core/mcp_preview/transform' +const FLOW_PREVIEW_URL_LOCAL = 'https://flow.shop.dev/flow-core/mcp_preview/transform' + +function previewEndpoint(): string { + return process.env.SHOPIFY_SERVICE_ENV === 'local' ? FLOW_PREVIEW_URL_LOCAL : FLOW_PREVIEW_URL_PRODUCTION +} + +export default class FlowWorkflowPreview extends StoreCommand { + static hidden = true + + static summary = 'Transform a local workflow JSON file into the preview payload used by the MCP preview iframe.' + + static descriptionWithMarkdown = `Reads a local \`.flow.json\` file, calls the Flow MCP preview transform endpoint, and prints the resulting JSON. The output is the payload the \`/public/mcp-preview\` iframe expects via \`postMessage({type: "mcp-workflow-preview", workflow: })\`. + +This is the data path used by the Flow VSCode extension. The extension shells out to this command transparently when the user opens or saves a \`.flow.json\` file — no manual invocation required.` + + static description = this.descriptionWithoutMarkdown() + + static examples = ['<%= config.bin %> <%= command.id %> ./workflow.flow.json --store shop.myshopify.com'] + + static args = { + file: Args.string({ + description: 'Path to the workflow JSON file.', + required: true, + parse: async (input) => resolvePath(input), + }), + } + + static flags = { + ...globalFlags, + ...jsonFlag, + store: Flags.string({ + char: 's', + description: 'The myshopify.com domain. Falls back to the `store` field in flow.toml.', + env: 'SHOPIFY_FLAG_STORE', + parse: async (input) => normalizeStoreFqdn(input), + }), + } + + public async run(): Promise { + const {args, flags} = await this.parse(FlowWorkflowPreview) + + const store = flags.store ?? (await loadConfig())?.store + if (!store) { + throw new AbortError('No store. Pass --store, or run `shopify flow init` to create a flow.toml.') + } + + const payload = await readWorkflowFile(args.file) + const session = await authenticateFlowStore(store) + + const response = await shopifyFetch( + previewEndpoint(), + { + method: 'POST', + headers: { + Authorization: `Bearer ${session.accessToken}`, + 'Content-Type': 'application/json', + 'X-Shopify-Shop-Domain': store, + }, + body: JSON.stringify(payload), + }, + 'slow-request', + ) + + if (response.status === 401) throwFlowAuthExpired(session) + + const text = await response.text() + if (!response.ok) { + throw new AbortError(`Preview transform failed (HTTP ${response.status}).`, text) + } + + outputResult(text) + } +} diff --git a/packages/store/src/cli/commands/flow/workflow/pull.ts b/packages/store/src/cli/commands/flow/workflow/pull.ts new file mode 100644 index 00000000000..a54862d4261 --- /dev/null +++ b/packages/store/src/cli/commands/flow/workflow/pull.ts @@ -0,0 +1,155 @@ +import {loadConfig, workflowsDirAbsolute} from '../../../services/flow/project-config.js' +import {pullAllWorkflows, pullWorkflow} from '../../../services/flow/workflow-lifecycle.js' +import StoreCommand from '../../../utilities/store-command.js' +import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli' +import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn' +import {AbortError} from '@shopify/cli-kit/node/error' +import {outputResult} from '@shopify/cli-kit/node/output' +import {cwd, resolvePath} from '@shopify/cli-kit/node/path' +import {Flags} from '@oclif/core' + +export default class FlowWorkflowPull extends StoreCommand { + static hidden = true + + static summary = 'Pull a Flow workflow definition (or all of them) into local files with lockfiles.' + + static descriptionWithMarkdown = `Without \`--all\`: fetches a single workflow + leading definition from the shop and writes it to \`--out\` (normalized JSON). Sibling \`.flow.lock.json\` is also written. + +With \`--all\`: lists every workflow on the shop and writes one file per workflow into the project's workflows directory. Skips files that already exist unless \`--force\` is set. Requires \`flow.toml\` (run \`shopify flow init\` first), or pass \`--workflows-dir\`.` + + static description = this.descriptionWithoutMarkdown() + + static examples = [ + '<%= config.bin %> <%= command.id %> --workflow-id 01HQK... --out ./workflow.flow.json --store shop.myshopify.com', + '<%= config.bin %> <%= command.id %> --all', + '<%= config.bin %> <%= command.id %> --all --include-hidden --force', + ] + + static flags = { + ...globalFlags, + ...jsonFlag, + store: Flags.string({ + char: 's', + description: 'The myshopify.com domain. Falls back to the `store` field in flow.toml.', + env: 'SHOPIFY_FLAG_STORE', + parse: async (input) => normalizeStoreFqdn(input), + }), + 'workflow-id': Flags.string({ + description: 'ID of a single workflow to pull. Mutually exclusive with --all.', + env: 'SHOPIFY_FLAG_FLOW_WORKFLOW_ID', + exclusive: ['all'], + }), + 'workflow-version': Flags.string({ + description: 'Optional definition version when pulling a single workflow. Defaults to leading.', + env: 'SHOPIFY_FLAG_FLOW_WORKFLOW_VERSION', + }), + out: Flags.string({ + description: 'Path to write a single workflow JSON. Required without --all.', + env: 'SHOPIFY_FLAG_FLOW_OUT', + parse: async (input) => resolvePath(input), + }), + all: Flags.boolean({ + description: 'Pull every workflow on the shop into the project workflows directory.', + default: false, + exclusive: ['workflow-id'], + }), + 'workflows-dir': Flags.string({ + description: 'Directory to write workflows into when using --all. Falls back to flow.toml.', + env: 'SHOPIFY_FLAG_FLOW_WORKFLOWS_DIR', + }), + 'include-hidden': Flags.boolean({ + description: 'Include hidden workflows when pulling --all.', + default: false, + }), + force: Flags.boolean({ + description: 'Overwrite existing files when pulling --all. (Single-pull always overwrites the --out path.)', + default: false, + }), + } + + public async run(): Promise { + const {flags} = await this.parse(FlowWorkflowPull) + + if (flags.all) { + await this.runPullAll(flags) + return + } + + if (!flags['workflow-id'] || !flags.out) { + throw new AbortError( + 'Pull a single workflow with --workflow-id and --out, or pull every workflow with --all.', + ) + } + + const store = flags.store ?? (await loadConfig())?.store + if (!store) { + throw new AbortError( + 'No store. Pass --store, or run `shopify flow init` to write a flow.toml.', + ) + } + + const result = await pullWorkflow({ + workflowId: flags['workflow-id'], + workflowVersion: flags['workflow-version'], + outPath: flags.out, + store, + }) + + if (flags.json) { + outputResult(JSON.stringify(result, null, 2)) + } else { + outputResult(`Pulled workflow ${result.workflowId} (version ${result.workflowDefinitionVersion}) to ${flags.out}.`) + } + } + + private async runPullAll(flags: { + store?: string + 'workflows-dir'?: string + 'include-hidden': boolean + force: boolean + json?: boolean + }): Promise { + const config = await loadConfig() + const store = flags.store ?? config?.store + if (!store) { + throw new AbortError( + '--all requires a store. Pass --store or run `shopify flow init` to create a flow.toml.', + ) + } + + const dir = flags['workflows-dir'] + ? resolvePath(cwd(), flags['workflows-dir']) + : config + ? workflowsDirAbsolute(config) + : undefined + if (!dir) { + throw new AbortError( + '--all requires a workflows directory. Pass --workflows-dir or run `shopify flow init`.', + ) + } + + const result = await pullAllWorkflows({ + store, + outDir: dir, + includeHidden: flags['include-hidden'], + force: flags.force, + }) + + if (flags.json) { + outputResult(JSON.stringify(result, null, 2)) + return + } + + const lines = [ + `Pulled ${result.pulled} of ${result.total} workflow(s) into ${dir}.`, + result.skipped > 0 ? `Skipped ${result.skipped} (file already exists; pass --force to overwrite).` : '', + '', + ...result.items.map((item) => + item.status === 'pulled' + ? ` + ${item.filePath} (${item.workflowId})` + : ` - ${item.filePath} (${item.workflowId}) — ${item.reason ?? 'skipped'}`, + ), + ].filter(Boolean) + outputResult(lines.join('\n')) + } +} diff --git a/packages/store/src/cli/commands/flow/workflow/push.ts b/packages/store/src/cli/commands/flow/workflow/push.ts new file mode 100644 index 00000000000..e6cae0f5293 --- /dev/null +++ b/packages/store/src/cli/commands/flow/workflow/push.ts @@ -0,0 +1,65 @@ +import {loadConfig} from '../../../services/flow/project-config.js' +import {pushWorkflow} from '../../../services/flow/workflow-lifecycle.js' +import StoreCommand from '../../../utilities/store-command.js' +import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli' +import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn' +import {AbortError} from '@shopify/cli-kit/node/error' +import {outputResult} from '@shopify/cli-kit/node/output' +import {resolvePath} from '@shopify/cli-kit/node/path' +import {Args, Flags} from '@oclif/core' + +export default class FlowWorkflowPush extends StoreCommand { + static hidden = true + + static summary = 'Push a Flow workflow JSON file to a shop and write a lockfile.' + + static descriptionWithMarkdown = + 'Creates or updates a workflow on the shop. Writes a sibling `.flow.lock.json` with the returned workflow_id, version, and payload SHA. Workflows are pushed `hidden: false` so they can be activated.' + + static description = this.descriptionWithoutMarkdown() + + static examples = [ + '<%= config.bin %> <%= command.id %> ./workflow.flow.json --store shop.myshopify.com', + ] + + static args = { + file: Args.string({ + description: 'Path to the workflow JSON file.', + required: true, + parse: async (input) => resolvePath(input), + }), + } + + static flags = { + ...globalFlags, + ...jsonFlag, + store: Flags.string({ + char: 's', + description: 'The myshopify.com domain. Falls back to the `store` field in flow.toml.', + env: 'SHOPIFY_FLAG_STORE', + parse: async (input) => normalizeStoreFqdn(input), + }), + } + + public async run(): Promise { + const {args, flags} = await this.parse(FlowWorkflowPush) + + const store = flags.store ?? (await loadConfig())?.store + if (!store) { + throw new AbortError('No store. Pass --store, or run `shopify flow init` to create a flow.toml.') + } + + const result = await pushWorkflow({ + filePath: args.file, + store, + }) + + if (flags.json) { + outputResult(JSON.stringify(result, null, 2)) + } else { + outputResult( + `Pushed workflow ${result.workflowId} (version ${result.workflowDefinitionVersion}). Lockfile updated.`, + ) + } + } +} diff --git a/packages/store/src/cli/commands/flow/workflow/show.ts b/packages/store/src/cli/commands/flow/workflow/show.ts new file mode 100644 index 00000000000..09f67457bdb --- /dev/null +++ b/packages/store/src/cli/commands/flow/workflow/show.ts @@ -0,0 +1,66 @@ +import {loadConfig} from '../../../services/flow/project-config.js' +import {fetchRemoteWorkflow, normalizeWorkflowJson} from '../../../services/flow/workflow-lifecycle.js' +import StoreCommand from '../../../utilities/store-command.js' +import {globalFlags} from '@shopify/cli-kit/node/cli' +import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn' +import {AbortError} from '@shopify/cli-kit/node/error' +import {outputResult} from '@shopify/cli-kit/node/output' +import {Args, Flags} from '@oclif/core' + +export default class FlowWorkflowShow extends StoreCommand { + static hidden = true + + static summary = 'Print a remote workflow definition as normalized JSON.' + + static descriptionWithMarkdown = `Fetches a workflow from the shop and prints its full definition (sorted keys, 2-space indent) to stdout. Doesn't write any files. Use this to inspect a remote workflow without committing to a local file. + +To save the result, redirect to a file. To pull and create a tracked local copy with a lockfile, use \`shopify flow workflow pull\`.` + + static description = this.descriptionWithoutMarkdown() + + static examples = [ + '<%= config.bin %> <%= command.id %> 01HQK000000000000000000000', + '<%= config.bin %> <%= command.id %> 01HQK... --workflow-version 01HQL... --store shop.myshopify.com', + '<%= config.bin %> <%= command.id %> 01HQK... > /tmp/workflow.json', + ] + + static args = { + 'workflow-id': Args.string({ + description: 'ID of the workflow to show.', + required: true, + }), + } + + static flags = { + ...globalFlags, + store: Flags.string({ + char: 's', + description: 'The myshopify.com domain. Falls back to the `store` field in flow.toml.', + env: 'SHOPIFY_FLAG_STORE', + parse: async (input) => normalizeStoreFqdn(input), + }), + 'workflow-version': Flags.string({ + description: 'Optional definition version. Defaults to leading.', + env: 'SHOPIFY_FLAG_FLOW_WORKFLOW_VERSION', + }), + } + + public async run(): Promise { + const {args, flags} = await this.parse(FlowWorkflowShow) + + const store = flags.store ?? (await loadConfig())?.store + if (!store) { + throw new AbortError( + 'No store. Pass --store, or run `shopify flow init` to create a flow.toml.', + ) + } + + const remote = await fetchRemoteWorkflow({ + workflowId: args['workflow-id'], + workflowVersion: flags['workflow-version'], + store, + }) + + outputResult(normalizeWorkflowJson(remote)) + } +} diff --git a/packages/store/src/cli/commands/flow/workflow/status.ts b/packages/store/src/cli/commands/flow/workflow/status.ts new file mode 100644 index 00000000000..2df6c26a68b --- /dev/null +++ b/packages/store/src/cli/commands/flow/workflow/status.ts @@ -0,0 +1,134 @@ +import {loadConfig, workflowsDirAbsolute} from '../../../services/flow/project-config.js' +import {statusProject, type StatusItem, type WorkflowStatus} from '../../../services/flow/workflow-lifecycle.js' +import StoreCommand from '../../../utilities/store-command.js' +import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli' +import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn' +import {AbortError} from '@shopify/cli-kit/node/error' +import {outputResult} from '@shopify/cli-kit/node/output' +import {cwd, resolvePath} from '@shopify/cli-kit/node/path' +import {Flags} from '@oclif/core' + +const STATUS_LABEL: Record = { + clean: 'clean', + drifted: 'drifted', + new: 'new', + orphaned: 'orphaned', + unknown: 'unknown', +} + +export default class FlowWorkflowStatus extends StoreCommand { + static hidden = true + + static summary = 'Show drift between local workflow files and the shop.' + + static descriptionWithMarkdown = `Walks the project's workflows directory and classifies every workflow: + +- \`clean\` — local file matches the remote workflow +- \`drifted\` — local file and remote differ; \`push\` would update remote, \`pull\` would update local +- \`new\` — local file with no lockfile; never pushed +- \`orphaned\` — lockfile points to a workflow that no longer exists on the shop +- \`unknown\` — workflow exists on the shop but isn't tracked locally; \`pull --workflow-id \` to bring it in + +Exits 1 if anything other than \`clean\` is present.` + + static description = this.descriptionWithoutMarkdown() + + static examples = [ + '<%= config.bin %> <%= command.id %>', + '<%= config.bin %> <%= command.id %> --json', + ] + + static flags = { + ...globalFlags, + ...jsonFlag, + store: Flags.string({ + char: 's', + description: 'The myshopify.com domain. Falls back to the `store` field in flow.toml.', + env: 'SHOPIFY_FLAG_STORE', + parse: async (input) => normalizeStoreFqdn(input), + }), + 'workflows-dir': Flags.string({ + description: 'Directory containing workflow JSON files. Falls back to flow.toml.', + env: 'SHOPIFY_FLAG_FLOW_WORKFLOWS_DIR', + }), + 'include-hidden': Flags.boolean({ + description: 'Include hidden remote workflows in the unknown-detection list.', + default: false, + }), + } + + public async run(): Promise { + const {flags} = await this.parse(FlowWorkflowStatus) + + const config = await loadConfig() + const store = flags.store ?? config?.store + if (!store) { + throw new AbortError( + 'No store. Pass --store, or run `shopify flow init` to create a flow.toml.', + ) + } + const dir = flags['workflows-dir'] + ? resolvePath(cwd(), flags['workflows-dir']) + : config + ? workflowsDirAbsolute(config) + : undefined + if (!dir) { + throw new AbortError( + 'No workflows directory. Pass --workflows-dir, or run `shopify flow init`.', + ) + } + + const result = await statusProject({store, workflowsDir: dir, includeHidden: flags['include-hidden']}) + + if (flags.json) { + outputResult(JSON.stringify(result, null, 2)) + } else { + outputResult(formatHuman(result.items, result.counts, store, dir)) + } + + const dirty = result.counts.drifted + result.counts.new + result.counts.orphaned + result.counts.unknown + if (dirty > 0) { + if (!flags.json) { + outputResult(`\nProject not clean: ${formatDirtySummary(result.counts)}. Exiting 1.`) + } + process.exitCode = 1 + } + } +} + +function formatDirtySummary(counts: Record): string { + const parts: string[] = [] + if (counts.drifted > 0) parts.push(`${counts.drifted} drifted`) + if (counts.new > 0) parts.push(`${counts.new} new`) + if (counts.orphaned > 0) parts.push(`${counts.orphaned} orphaned`) + if (counts.unknown > 0) parts.push(`${counts.unknown} unknown`) + return parts.join(', ') +} + +function formatHuman( + items: StatusItem[], + counts: Record, + store: string, + dir: string, +): string { + const header = [ + `Store: ${store}`, + `Workflows dir: ${dir}`, + '', + `clean: ${counts.clean}`, + `drifted: ${counts.drifted}`, + `new: ${counts.new}`, + `orphaned: ${counts.orphaned}`, + `unknown: ${counts.unknown}`, + '', + ] + + const rows = items.map((item) => { + const label = STATUS_LABEL[item.status].padEnd(9) + const ref = item.filePath ?? `(remote: ${item.workflowId ?? '?'}${item.name ? ` "${item.name}"` : ''})` + const tail = item.message ? ` — ${item.message}` : '' + return `${label} ${ref}${tail}` + }) + + return [...header, ...rows].join('\n') +} diff --git a/packages/store/src/cli/commands/flow/workflow/validate.ts b/packages/store/src/cli/commands/flow/workflow/validate.ts new file mode 100644 index 00000000000..6db2d47071b --- /dev/null +++ b/packages/store/src/cli/commands/flow/workflow/validate.ts @@ -0,0 +1,71 @@ +import {loadConfig} from '../../../services/flow/project-config.js' +import {validateWorkflow} from '../../../services/flow/workflow-lifecycle.js' +import StoreCommand from '../../../utilities/store-command.js' +import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli' +import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn' +import {AbortError} from '@shopify/cli-kit/node/error' +import {outputResult} from '@shopify/cli-kit/node/output' +import {resolvePath} from '@shopify/cli-kit/node/path' +import {Args, Flags} from '@oclif/core' + +export default class FlowWorkflowValidate extends StoreCommand { + static hidden = true + + static summary = 'Validate a Flow workflow JSON file against a shop without persisting.' + + static descriptionWithMarkdown = + 'Runs the same validation as push but in dry-run mode (`is_eval`). No DB writes; returns validation errors against the shop scope.' + + static description = this.descriptionWithoutMarkdown() + + static examples = [ + '<%= config.bin %> <%= command.id %> ./workflow.flow.json --store shop.myshopify.com', + ] + + static args = { + file: Args.string({ + description: 'Path to the workflow JSON file.', + required: true, + parse: async (input) => resolvePath(input), + }), + } + + static flags = { + ...globalFlags, + ...jsonFlag, + store: Flags.string({ + char: 's', + description: 'The myshopify.com domain. Falls back to the `store` field in flow.toml.', + env: 'SHOPIFY_FLAG_STORE', + parse: async (input) => normalizeStoreFqdn(input), + }), + } + + public async run(): Promise { + const {args, flags} = await this.parse(FlowWorkflowValidate) + + const store = flags.store ?? (await loadConfig())?.store + if (!store) { + throw new AbortError('No store. Pass --store, or run `shopify flow init` to create a flow.toml.') + } + + const result = await validateWorkflow({ + filePath: args.file, + store, + }) + + if (flags.json) { + outputResult(JSON.stringify(result, null, 2)) + } else if (result.validation_errors && result.validation_errors.length > 0) { + outputResult(`Validation errors:\n${JSON.stringify(result.validation_errors, null, 2)}`) + } else if (result.error) { + outputResult(`Error: ${result.error}`) + } else { + outputResult('Workflow is valid.') + } + + if (result.error || (result.validation_errors && result.validation_errors.length > 0)) { + throw new AbortError('Workflow validation failed.') + } + } +} diff --git a/packages/store/src/cli/services/flow/auth.ts b/packages/store/src/cli/services/flow/auth.ts new file mode 100644 index 00000000000..2b63ba390bc --- /dev/null +++ b/packages/store/src/cli/services/flow/auth.ts @@ -0,0 +1,35 @@ +/* + * Caller authentication for Flow CLI requests. + * + * Flow authenticates the merchant with the credentials minted by + * `shopify store auth` (a pure-PKCE online Admin API access token), the same way + * `shopify store execute` does — no Shopify Identity login. The merchant token is + * used only to authenticate the caller: the Flow backend validates it against the + * shop's Admin API and performs the actual work with its own service auth. + */ +import {loadStoredStoreSession} from '../store/auth/session-lifecycle.js' +import {clearStoredStoreAppSession} from '../store/auth/session-store.js' +import {throwReauthenticateStoreAuthError} from '../store/auth/recovery.js' +import type {StoredStoreAppSession} from '../store/auth/session-store.js' + +/** + * Loads the merchant's stored `shopify store auth` session for `store`, + * transparently refreshing an expired access token. Throws a user-facing + * re-authentication error if no session is stored or the refresh fails. + */ +export async function authenticateFlowStore(store: string): Promise { + return loadStoredStoreSession(store) +} + +/** + * Handles a 401 from a Flow gateway endpoint: the stored token was rejected, so + * clear it and prompt the merchant to re-run `shopify store auth`. + */ +export function throwFlowAuthExpired(session: StoredStoreAppSession): never { + clearStoredStoreAppSession(session.store, session.userId) + throwReauthenticateStoreAuthError( + `Stored store authentication for ${session.store} is no longer valid.`, + session.store, + session.scopes.join(','), + ) +} diff --git a/packages/store/src/cli/services/flow/dispatch.ts b/packages/store/src/cli/services/flow/dispatch.ts new file mode 100644 index 00000000000..f63c95e4979 --- /dev/null +++ b/packages/store/src/cli/services/flow/dispatch.ts @@ -0,0 +1,117 @@ +/* + * Transport primitive for Flow + SK tool calls. + * + * Each first-class CLI command (e.g. `flow template search`, + * `flow workflow push`) constructs the structured args it needs and calls + * `dispatchFlowTool` to send the request. This file owns: + * - Routing (Flow vs SK endpoint, prod vs local) + * - Auth (merchant `shopify store auth` token; see ./auth.ts) + * - Headers (shop domain, eval flag) + * - Response parsing + error envelope + * + * No tool catalog, no name lookup, no zod here. Callers know what they want + * to call; this just dispatches it. + */ +import {authenticateFlowStore, throwFlowAuthExpired} from './auth.js' +import {AbortError} from '@shopify/cli-kit/node/error' +import {shopifyFetch, type Response} from '@shopify/cli-kit/node/http' + +const FLOW_TOOL_CALL_BASE_PRODUCTION = 'https://flow.shopifycloud.com/flow-core/tool_call' +const FLOW_TOOL_CALL_BASE_LOCAL = 'https://flow.shop.dev/flow-core/tool_call' +const SK_TOOL_CALL_BASE_PRODUCTION = 'https://sidekick.shopify.ai/tools/call' +const SK_TOOL_CALL_BASE_LOCAL = 'https://agent-server.shop.dev/tools/call' + +export type ToolSource = 'flow' | 'sk' + +export interface DispatchInput { + name: string + source: ToolSource + store: string + args: Record + isEval?: boolean +} + +function isLocalEnvironment(): boolean { + return process.env.SHOPIFY_SERVICE_ENV === 'local' +} + +export function endpointFor(source: ToolSource): string { + const local = isLocalEnvironment() + if (source === 'flow') return local ? FLOW_TOOL_CALL_BASE_LOCAL : FLOW_TOOL_CALL_BASE_PRODUCTION + return local ? SK_TOOL_CALL_BASE_LOCAL : SK_TOOL_CALL_BASE_PRODUCTION +} + +async function parseResponse(response: Response): Promise { + const text = await response.text() + if (!text.trim()) return {} + try { + return JSON.parse(text) + } catch (error) { + if (!(error instanceof SyntaxError)) throw error + return {raw: text} + } +} + +export async function dispatchFlowTool(input: DispatchInput): Promise { + const session = await authenticateFlowStore(input.store) + const endpoint = endpointFor(input.source) + + const headers: Record = { + Authorization: `Bearer ${session.accessToken}`, + 'Content-Type': 'application/json', + 'X-Shopify-Shop-Domain': input.store, + } + if (input.isEval) headers['X-Shopify-Is-Eval'] = 'true' + + const response = await shopifyFetch( + endpoint, + { + method: 'POST', + headers, + body: JSON.stringify({ + tool: input.name, + arguments: input.args, + }), + }, + 'slow-request', + ) + + if (response.status === 401) throwFlowAuthExpired(session) + + const body = await parseResponse(response) + + if (!response.ok) { + const verbose = process.env.DEBUG && process.env.DEBUG !== '' + const display = verbose ? body : stripBacktraces(body) + const hint = verbose ? undefined : 'Re-run with --verbose to see full backtraces.' + throw new AbortError( + `Flow tool gateway request failed with HTTP ${response.status}.`, + [JSON.stringify(display, null, 2), hint].filter(Boolean).join('\n\n'), + ) + } + + return body +} + +function stripBacktraces(value: unknown): unknown { + if (Array.isArray(value)) return value.map(stripBacktraces) + if (value !== null && typeof value === 'object') { + const entries = Object.entries(value as Record) + .filter(([key]) => key !== 'error_backtrace') + .map(([key, val]) => [key, stripBacktraces(val)] as const) + return Object.fromEntries(entries) + } + return value +} + +export interface ToolJsonResponse { + ok: boolean + tool?: string + data?: T +} + +export function unwrapJsonResult(response: unknown): T { + const typed = response as ToolJsonResponse | undefined + if (!typed?.data) throw new AbortError('Flow tool returned an unexpected response shape.') + return typed.data +} diff --git a/packages/store/src/cli/services/flow/project-config.ts b/packages/store/src/cli/services/flow/project-config.ts new file mode 100644 index 00000000000..20aba3ee1c4 --- /dev/null +++ b/packages/store/src/cli/services/flow/project-config.ts @@ -0,0 +1,115 @@ +/* + * Reader/writer for `flow.toml` — the per-project IaC config file. + * + * Shape: + * store = "shop.myshopify.com" + * + * [workflows] + * dir = "workflows" + * + * Lifecycle commands fall back to this file when --store and --workflows-dir + * aren't passed. `shopify flow init` writes the initial file. + */ +import {formatZodErrors} from './zod-errors.js' +import {AbortError} from '@shopify/cli-kit/node/error' +import {fileExists} from '@shopify/cli-kit/node/fs' +import {cwd, dirname, joinPath, resolvePath} from '@shopify/cli-kit/node/path' +import {zod} from '@shopify/cli-kit/node/schema' +import {TomlFile, TomlFileError} from '@shopify/cli-kit/node/toml/toml-file' + +export const FLOW_CONFIG_FILENAME = 'flow.toml' +const DEFAULT_WORKFLOWS_DIR = 'workflows' + +const FlowTomlSchema = zod + .object({ + store: zod.string().min(1, 'store is required'), + workflows: zod + .object({ + dir: zod.string().min(1).optional(), + }) + .strict() + .optional(), + }) + .strict() + +export interface FlowProjectConfig { + store: string + workflowsDir: string + configPath: string + rootDir: string +} + +export async function findConfigFile(startDir = cwd()): Promise { + let dir = resolvePath(startDir) + while (true) { + const candidate = joinPath(dir, FLOW_CONFIG_FILENAME) + if (await fileExists(candidate)) return candidate + + const parent = dirname(dir) + if (parent === dir) return undefined + dir = parent + } +} + +export async function loadConfig(startDir = cwd()): Promise { + const path = await findConfigFile(startDir) + if (!path) return undefined + + let file: TomlFile + try { + file = await TomlFile.read(path) + } catch (error) { + if (error instanceof TomlFileError) throw new AbortError(`Failed to parse ${path}: ${error.message}`) + throw error + } + + const parsed = FlowTomlSchema.safeParse(file.content) + if (!parsed.success) { + throw new AbortError(`${path} is malformed.`, formatZodErrors(parsed.error)) + } + + return { + store: parsed.data.store, + workflowsDir: parsed.data.workflows?.dir ?? DEFAULT_WORKFLOWS_DIR, + configPath: path, + rootDir: dirname(path), + } +} + +export async function requireConfig(startDir = cwd()): Promise { + const config = await loadConfig(startDir) + if (!config) { + throw new AbortError( + `No ${FLOW_CONFIG_FILENAME} found in this directory or any ancestor.`, + 'Run `shopify flow init` to create one.', + ) + } + return config +} + +export interface WriteConfigInput { + dir: string + store: string + workflowsDir?: string + force?: boolean +} + +export async function writeConfig(input: WriteConfigInput): Promise { + const path = joinPath(input.dir, FLOW_CONFIG_FILENAME) + if ((await fileExists(path)) && !input.force) { + throw new AbortError(`${path} already exists.`, 'Pass --force to overwrite, or edit the file directly.') + } + + const file = new TomlFile(path, {}) + await file.replace({ + store: input.store, + workflows: { + dir: input.workflowsDir ?? DEFAULT_WORKFLOWS_DIR, + }, + }) + return path +} + +export function workflowsDirAbsolute(config: FlowProjectConfig): string { + return resolvePath(config.rootDir, config.workflowsDir) +} diff --git a/packages/store/src/cli/services/flow/workflow-lifecycle.ts b/packages/store/src/cli/services/flow/workflow-lifecycle.ts new file mode 100644 index 00000000000..6175a42b159 --- /dev/null +++ b/packages/store/src/cli/services/flow/workflow-lifecycle.ts @@ -0,0 +1,667 @@ +import {dispatchFlowTool} from './dispatch.js' +import {formatZodErrors} from './zod-errors.js' +import {AbortError} from '@shopify/cli-kit/node/error' +import {fileExists, glob, mkdir, readFile, writeFile} from '@shopify/cli-kit/node/fs' +import {joinPath} from '@shopify/cli-kit/node/path' +import {zod} from '@shopify/cli-kit/node/schema' +import {createHash} from 'crypto' + +const TOOL_CREATE_OR_UPDATE = 'flow_app_agent_create_or_update_workflow_from_json' +const TOOL_WORKFLOW_LOOKUP = 'flow_app_agent_workflow_lookup' +const TOOL_LIST_WORKFLOWS = 'flow_app_agent_list_workflows' +const TOOL_ACTIVATE = 'flow_app_agent_activate_workflow' +const TOOL_DEACTIVATE = 'flow_app_agent_deactivate_workflow' + +const LockfileSchema = zod + .object({ + workflow_id: zod.string().min(1), + workflow_definition_version: zod.string().min(1), + payload_sha256: zod.string().regex(/^[a-f0-9]{64}$/, 'must be a 64-char lowercase hex sha256'), + store: zod.string().min(1), + pushed_at: zod.string().datetime(), + }) + .strict() + +export type Lockfile = zod.infer + +export interface UpsertResponse { + ok: boolean + tool?: string + data?: UpsertResponseJson +} + +export interface UpsertResponseJson { + message?: string + workflow_id?: string + workflow_version?: string + workflow_definition_version?: string + validation_errors?: unknown[] + workflow_definition?: WorkflowJson + error?: string + error_code?: string +} + +export interface LookupResponseJson { + workflow_id?: string + workflow_version?: string + workflow_definition?: WorkflowJson + error?: string + error_code?: string +} + +export type WorkflowJson = Record + +export function normalizeWorkflowJson(payload: WorkflowJson): string { + return `${JSON.stringify(sortKeys(payload), null, 2)}\n` +} + +function sortKeys(value: unknown): unknown { + if (Array.isArray(value)) return value.map(sortKeys) + if (value !== null && typeof value === 'object') { + const entries = Object.entries(value as Record) + .sort(([left], [right]) => left.localeCompare(right)) + .map(([key, val]) => [key, sortKeys(val)] as const) + return Object.fromEntries(entries) + } + return value +} + +export function sha256(content: string): string { + return createHash('sha256').update(content).digest('hex') +} + +export function lockfilePath(workflowFilePath: string): string { + return workflowFilePath.replace(/\.flow\.json$/, '.flow.lock.json') +} + +export async function readWorkflowFile(filePath: string): Promise { + if (!(await fileExists(filePath))) { + throw new AbortError(`Workflow file not found at ${filePath}.`) + } + const raw = await readFile(filePath, {encoding: 'utf8'}) + try { + return JSON.parse(raw) as WorkflowJson + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error' + throw new AbortError(`Workflow file ${filePath} is not valid JSON: ${message}`) + } +} + +export async function readLockfileIfExists(workflowFilePath: string): Promise { + const path = lockfilePath(workflowFilePath) + if (!(await fileExists(path))) return undefined + + const raw = await readFile(path, {encoding: 'utf8'}) + + let parsed: unknown + try { + parsed = JSON.parse(raw) + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error' + throw new AbortError( + `Lockfile at ${path} is not valid JSON: ${message}`, + 'Repair the file by hand, restore from git, or delete and re-run `pull`/`push` to regenerate.', + ) + } + + const result = LockfileSchema.safeParse(parsed) + if (!result.success) { + // Throw on corrupt rather than silently treating as missing — silently + // treating a bad lockfile as "no lockfile" makes `push` create a brand- + // new workflow on the shop, duplicating whatever this lockfile pointed at. + throw new AbortError( + `Lockfile at ${path} has unexpected shape.`, + `${formatZodErrors(result.error)}\n\nRepair the file, restore from git, or delete and re-pull to regenerate.`, + ) + } + return result.data +} + +export async function writeLockfile(workflowFilePath: string, lockfile: Lockfile): Promise { + const path = lockfilePath(workflowFilePath) + await writeFile(path, `${JSON.stringify(lockfile, null, 2)}\n`) +} + +export async function writeWorkflowFile(filePath: string, payload: WorkflowJson): Promise { + await writeFile(filePath, normalizeWorkflowJson(payload)) +} + +function unwrapJsonResult(response: unknown): UpsertResponseJson { + const typed = response as {ok: boolean; data?: UpsertResponseJson} | undefined + if (!typed?.data) throw new AbortError('Flow tool returned an unexpected response shape.') + return typed.data +} + +function unwrapLookupResult(response: unknown): LookupResponseJson { + const typed = response as {ok: boolean; data?: LookupResponseJson} | undefined + if (!typed?.data) throw new AbortError('Flow workflow lookup returned an unexpected response shape.') + return typed.data +} + +export interface ValidateInput { + filePath: string + store: string +} + +export async function validateWorkflow(input: ValidateInput): Promise { + const payload = await readWorkflowFile(input.filePath) + const lockfile = await readLockfileIfExists(input.filePath) + + const args: Record = {workflow_json: payload} + if (lockfile) { + args.workflow_id = lockfile.workflow_id + args.workflow_version = lockfile.workflow_definition_version + } + + const response = await dispatchFlowTool({ + name: TOOL_CREATE_OR_UPDATE, source: 'flow', + store: input.store, + args, + isEval: true, + }) + + return unwrapJsonResult(response) +} + +export interface PushInput { + filePath: string + store: string +} + +export interface PushResult { + workflowId: string + workflowDefinitionVersion: string + validationErrors: unknown[] + lockfile: Lockfile +} + +export async function pushWorkflow(input: PushInput): Promise { + const payload = await readWorkflowFile(input.filePath) + const lockfile = await readLockfileIfExists(input.filePath) + + const args: Record = { + workflow_json: payload, + hidden: false, + } + if (lockfile) { + args.workflow_id = lockfile.workflow_id + args.workflow_version = lockfile.workflow_definition_version + } + + const response = await dispatchFlowTool({ + name: TOOL_CREATE_OR_UPDATE, source: 'flow', + store: input.store, + args, + }) + + const json = unwrapJsonResult(response) + if (!json.workflow_id || !json.workflow_version) { + throw new AbortError(`Push failed: ${json.error ?? 'no workflow_id returned'}`) + } + + const canonicalPayload = json.workflow_definition ?? payload + const canonicalNormalized = normalizeWorkflowJson(canonicalPayload) + if (json.workflow_definition) { + await writeWorkflowFile(input.filePath, canonicalPayload) + } + + const newLockfile: Lockfile = { + workflow_id: json.workflow_id, + workflow_definition_version: json.workflow_version, + payload_sha256: sha256(canonicalNormalized), + store: input.store, + pushed_at: new Date().toISOString(), + } + await writeLockfile(input.filePath, newLockfile) + + return { + workflowId: json.workflow_id, + workflowDefinitionVersion: json.workflow_version, + validationErrors: json.validation_errors ?? [], + lockfile: newLockfile, + } +} + +export interface PullInput { + workflowId: string + workflowVersion?: string + outPath: string + store: string +} + +export interface PullResult { + workflowId: string + workflowDefinitionVersion: string + payload: WorkflowJson + lockfile: Lockfile +} + +export async function pullWorkflow(input: PullInput): Promise { + const args: Record = {workflow_id: input.workflowId} + if (input.workflowVersion) args.version = input.workflowVersion + + const response = await dispatchFlowTool({ + name: TOOL_WORKFLOW_LOOKUP, source: 'flow', + store: input.store, + args, + }) + + const json = unwrapLookupResult(response) + if (!json.workflow_definition || !json.workflow_id || !json.workflow_version) { + throw new AbortError(`Pull failed: ${json.error ?? 'workflow_definition missing from response'}`) + } + + await writeWorkflowFile(input.outPath, json.workflow_definition) + + const lockfile: Lockfile = { + workflow_id: json.workflow_id, + workflow_definition_version: json.workflow_version, + payload_sha256: sha256(normalizeWorkflowJson(json.workflow_definition)), + store: input.store, + pushed_at: new Date().toISOString(), + } + await writeLockfile(input.outPath, lockfile) + + return { + workflowId: json.workflow_id, + workflowDefinitionVersion: json.workflow_version, + payload: json.workflow_definition, + lockfile, + } +} + +export interface DiffInput { + filePath: string + store: string + workflowId?: string + workflowVersion?: string +} + +export interface DiffResult { + changed: boolean + localNormalized: string + remoteNormalized: string +} + +export async function fetchRemoteWorkflow(input: { + workflowId: string + workflowVersion?: string + store: string +}): Promise { + const args: Record = {workflow_id: input.workflowId} + if (input.workflowVersion) args.version = input.workflowVersion + + const response = await dispatchFlowTool({ + name: TOOL_WORKFLOW_LOOKUP, source: 'flow', + store: input.store, + args, + }) + + const json = unwrapLookupResult(response) + if (!json.workflow_definition) { + throw new AbortError(`Remote workflow lookup failed: ${json.error ?? 'no workflow_definition'}`) + } + return json.workflow_definition +} + +export async function diffWorkflow(input: DiffInput): Promise { + const localPayload = await readWorkflowFile(input.filePath) + const localNormalized = normalizeWorkflowJson(localPayload) + + const workflowId = input.workflowId ?? (await readLockfileIfExists(input.filePath))?.workflow_id + if (!workflowId) { + throw new AbortError( + `No lockfile found for ${input.filePath} and no --workflow-id was provided.`, + 'Run `shopify flow workflow push` first, or pass --workflow-id.', + ) + } + + const remotePayload = await fetchRemoteWorkflow({ + workflowId, + workflowVersion: input.workflowVersion, + store: input.store, + }) + const remoteNormalized = normalizeWorkflowJson(remotePayload) + + return { + changed: localNormalized !== remoteNormalized, + localNormalized, + remoteNormalized, + } +} + +export function unifiedDiff(remoteLabel: string, remoteText: string, localLabel: string, localText: string): string { + const remoteLines = remoteText.split('\n') + const localLines = localText.split('\n') + const lines: string[] = [`--- ${remoteLabel}`, `+++ ${localLabel}`] + + const lcs = longestCommonSubsequence(remoteLines, localLines) + let i = 0 + let j = 0 + let k = 0 + while (i < remoteLines.length || j < localLines.length) { + if (k < lcs.length && remoteLines[i] === lcs[k] && localLines[j] === lcs[k]) { + lines.push(` ${remoteLines[i]}`) + i++ + j++ + k++ + } else if (j < localLines.length && (k >= lcs.length || localLines[j] !== lcs[k])) { + lines.push(`+${localLines[j]}`) + j++ + } else if (i < remoteLines.length && (k >= lcs.length || remoteLines[i] !== lcs[k])) { + lines.push(`-${remoteLines[i]}`) + i++ + } + } + return lines.join('\n') +} + +function longestCommonSubsequence(a: string[], b: string[]): string[] { + const dp: number[][] = Array.from({length: a.length + 1}, () => new Array(b.length + 1).fill(0)) + for (let i = 1; i <= a.length; i++) { + for (let j = 1; j <= b.length; j++) { + if (a[i - 1] === b[j - 1]) { + dp[i]![j] = dp[i - 1]![j - 1]! + 1 + } else { + dp[i]![j] = Math.max(dp[i - 1]![j]!, dp[i]![j - 1]!) + } + } + } + const result: string[] = [] + let i = a.length + let j = b.length + while (i > 0 && j > 0) { + if (a[i - 1] === b[j - 1]) { + result.unshift(a[i - 1]!) + i-- + j-- + } else if (dp[i - 1]![j]! >= dp[i]![j - 1]!) { + i-- + } else { + j-- + } + } + return result +} + +export interface ActivationInput { + filePath?: string + store: string + workflowId?: string + workflowVersion?: string + useLatest?: boolean +} + +async function resolveActivationTarget(input: ActivationInput): Promise<{workflowId: string; workflowVersion: string}> { + if (input.workflowId && input.workflowVersion) { + return {workflowId: input.workflowId, workflowVersion: input.workflowVersion} + } + + if (input.workflowId && input.useLatest) { + const remote = await fetchRemoteWorkflow({ + workflowId: input.workflowId, + store: input.store, + }) + const version = (remote as {root?: {workflow_version?: string}})?.root?.workflow_version + if (!version) { + throw new AbortError('Could not resolve latest workflow version from workflow lookup response.') + } + return {workflowId: input.workflowId, workflowVersion: version} + } + + if (input.filePath) { + const lockfile = await readLockfileIfExists(input.filePath) + if (lockfile) { + return { + workflowId: lockfile.workflow_id, + workflowVersion: lockfile.workflow_definition_version, + } + } + } + + throw new AbortError( + 'Cannot resolve workflow target.', + 'Pass a workflow file with a lockfile, or --workflow-id together with --workflow-version or --use-latest.', + ) +} + +export async function activateWorkflow(input: ActivationInput): Promise { + const {workflowId, workflowVersion} = await resolveActivationTarget(input) + const response = await dispatchFlowTool({ + name: TOOL_ACTIVATE, source: 'flow', + store: input.store, + args: {workflow_id: workflowId, workflow_definition_version: workflowVersion}, + }) + return unwrapJsonResult(response) +} + +export async function deactivateWorkflow(input: ActivationInput): Promise { + const {workflowId, workflowVersion} = await resolveActivationTarget(input) + const response = await dispatchFlowTool({ + name: TOOL_DEACTIVATE, source: 'flow', + store: input.store, + args: {workflow_id: workflowId, workflow_definition_version: workflowVersion}, + }) + return unwrapJsonResult(response) +} + +export interface RemoteWorkflowSummary { + workflow_id: string + name: string + hidden: boolean + last_updated?: string +} + +interface ListWorkflowsResponseJson { + workflows?: RemoteWorkflowSummary[] + pagination?: { + has_next_page?: boolean + next_cursor?: string + total_count?: number + } + error?: string + error_code?: string +} + +export async function listAllWorkflows(input: { + store: string + includeHidden?: boolean +}): Promise { + const all: RemoteWorkflowSummary[] = [] + let cursor: string | undefined + + while (true) { + const args: Record = {page_size: 100} + if (cursor) args.cursor = cursor + if (input.includeHidden) args.include_hidden = true + + const response = await dispatchFlowTool({ + name: TOOL_LIST_WORKFLOWS, source: 'flow', + store: input.store, + args, + }) + + const json = (response as {ok: boolean; data?: ListWorkflowsResponseJson} | undefined)?.data + if (!json) throw new AbortError('Flow list_workflows returned an unexpected response shape.') + if (json.error) throw new AbortError(`list_workflows failed: ${json.error}`) + + if (json.workflows) all.push(...json.workflows) + + if (!json.pagination?.has_next_page || !json.pagination.next_cursor) break + cursor = json.pagination.next_cursor + } + + return all +} + +export const WORKFLOW_FILENAME = 'workflow.flow.json' + +export function workflowSlugFor(name: string): string { + const slug = name.toLowerCase().normalize('NFKD').replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') + return slug || 'workflow' +} + +export function workflowDirAndFilename(name: string): {dir: string; filename: string} { + return {dir: workflowSlugFor(name), filename: WORKFLOW_FILENAME} +} + +export interface PullAllInput { + store: string + outDir: string + includeHidden?: boolean + force?: boolean +} + +export interface PullAllItem { + workflowId: string + name: string + filePath: string + status: 'pulled' | 'skipped' + reason?: string +} + +export interface PullAllResult { + total: number + pulled: number + skipped: number + items: PullAllItem[] +} + +export async function pullAllWorkflows(input: PullAllInput): Promise { + const remote = await listAllWorkflows({store: input.store, includeHidden: input.includeHidden}) + await mkdir(input.outDir) + + const usedSlugs = new Set() + const items: PullAllItem[] = [] + + for (const workflow of remote) { + const baseSlug = workflowSlugFor(workflow.name) + let slug = baseSlug + let suffix = 2 + while (usedSlugs.has(slug)) { + slug = `${baseSlug}-${suffix}` + suffix += 1 + } + usedSlugs.add(slug) + + const workflowDir = joinPath(input.outDir, slug) + const filePath = joinPath(workflowDir, WORKFLOW_FILENAME) + + if ((await fileExists(filePath)) && !input.force) { + items.push({ + workflowId: workflow.workflow_id, + name: workflow.name, + filePath, + status: 'skipped', + reason: 'file exists (pass --force to overwrite)', + }) + continue + } + + await mkdir(workflowDir) + await pullWorkflow({ + workflowId: workflow.workflow_id, + outPath: filePath, + store: input.store, + }) + + items.push({ + workflowId: workflow.workflow_id, + name: workflow.name, + filePath, + status: 'pulled', + }) + } + + return { + total: items.length, + pulled: items.filter((item) => item.status === 'pulled').length, + skipped: items.filter((item) => item.status === 'skipped').length, + items, + } +} + +export type WorkflowStatus = 'clean' | 'drifted' | 'new' | 'orphaned' | 'unknown' + +export interface StatusItem { + status: WorkflowStatus + filePath?: string + workflowId?: string + name?: string + message?: string +} + +export interface StatusInput { + store: string + workflowsDir: string + includeHidden?: boolean +} + +export interface StatusResult { + store: string + items: StatusItem[] + counts: Record +} + +async function classifyLocalFile(filePath: string, store: string): Promise { + const lockfile = await readLockfileIfExists(filePath) + if (!lockfile) { + return {status: 'new', filePath} + } + + try { + const localPayload = await readWorkflowFile(filePath) + const localNormalized = normalizeWorkflowJson(localPayload) + const remotePayload = await fetchRemoteWorkflow({ + workflowId: lockfile.workflow_id, + store, + }) + const remoteNormalized = normalizeWorkflowJson(remotePayload) + + return { + status: localNormalized === remoteNormalized ? 'clean' : 'drifted', + filePath, + workflowId: lockfile.workflow_id, + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + return { + status: 'orphaned', + filePath, + workflowId: lockfile.workflow_id, + message, + } + } +} + +export async function statusProject(input: StatusInput): Promise { + const localFiles = await glob('**/*.flow.json', { + cwd: input.workflowsDir, + absolute: true, + }) + + const localItems = await Promise.all(localFiles.map((filePath) => classifyLocalFile(filePath, input.store))) + + const trackedIds = new Set(localItems.map((item) => item.workflowId).filter((id): id is string => Boolean(id))) + + const remote = await listAllWorkflows({store: input.store, includeHidden: input.includeHidden}) + const unknownItems: StatusItem[] = remote + .filter((workflow) => !trackedIds.has(workflow.workflow_id)) + .map((workflow) => ({ + status: 'unknown' as WorkflowStatus, + workflowId: workflow.workflow_id, + name: workflow.name, + })) + + const items = [...localItems, ...unknownItems] + const counts: Record = { + clean: 0, + drifted: 0, + new: 0, + orphaned: 0, + unknown: 0, + } + for (const item of items) counts[item.status] += 1 + + return {store: input.store, items, counts} +} diff --git a/packages/store/src/cli/services/flow/zod-errors.ts b/packages/store/src/cli/services/flow/zod-errors.ts new file mode 100644 index 00000000000..9796937bcfb --- /dev/null +++ b/packages/store/src/cli/services/flow/zod-errors.ts @@ -0,0 +1,16 @@ +import type {zod} from '@shopify/cli-kit/node/schema' + +/** + * Format a ZodError into a single human-readable string suitable for an + * AbortError detail line. Each issue is rendered as `: `, + * joined with `; `. Paths are dotted (`workflows.dir`) or `(root)` for + * top-level errors. + */ +export function formatZodErrors(error: zod.ZodError): string { + return error.errors + .map((issue) => { + const path = issue.path.length > 0 ? issue.path.join('.') : '(root)' + return `${path}: ${issue.message}` + }) + .join('; ') +} diff --git a/packages/store/src/index.ts b/packages/store/src/index.ts index 89caa4a6946..d006b626de8 100644 --- a/packages/store/src/index.ts +++ b/packages/store/src/index.ts @@ -1,4 +1,23 @@ import StoreAuthList from './cli/commands/store/auth/list.js' +import FlowEnvSearch from './cli/commands/flow/env/search.js' +import FlowInit from './cli/commands/flow/init.js' +import FlowResourceSearch from './cli/commands/flow/resource/search.js' +import FlowShopifyqlColumns from './cli/commands/flow/shopifyql/columns.js' +import FlowTaskDescribe from './cli/commands/flow/task/describe.js' +import FlowTaskSearch from './cli/commands/flow/task/search.js' +import FlowTemplateSave from './cli/commands/flow/template/save.js' +import FlowTemplateSearch from './cli/commands/flow/template/search.js' +import FlowTypeShow from './cli/commands/flow/type/show.js' +import FlowWorkflowActivate from './cli/commands/flow/workflow/activate.js' +import FlowWorkflowDeactivate from './cli/commands/flow/workflow/deactivate.js' +import FlowWorkflowDiff from './cli/commands/flow/workflow/diff.js' +import FlowWorkflowList from './cli/commands/flow/workflow/list.js' +import FlowWorkflowPreview from './cli/commands/flow/workflow/preview.js' +import FlowWorkflowPull from './cli/commands/flow/workflow/pull.js' +import FlowWorkflowPush from './cli/commands/flow/workflow/push.js' +import FlowWorkflowShow from './cli/commands/flow/workflow/show.js' +import FlowWorkflowStatus from './cli/commands/flow/workflow/status.js' +import FlowWorkflowValidate from './cli/commands/flow/workflow/validate.js' import StoreAuth from './cli/commands/store/auth.js' import StoreCreateDev from './cli/commands/store/create/dev.js' import StoreCreatePreview from './cli/commands/store/create/preview.js' @@ -8,6 +27,25 @@ import StoreList from './cli/commands/store/list.js' const COMMANDS = { 'store:auth:list': StoreAuthList, + 'flow:env:search': FlowEnvSearch, + 'flow:init': FlowInit, + 'flow:resource:search': FlowResourceSearch, + 'flow:shopifyql:columns': FlowShopifyqlColumns, + 'flow:task:describe': FlowTaskDescribe, + 'flow:task:search': FlowTaskSearch, + 'flow:template:save': FlowTemplateSave, + 'flow:template:search': FlowTemplateSearch, + 'flow:type:show': FlowTypeShow, + 'flow:workflow:activate': FlowWorkflowActivate, + 'flow:workflow:deactivate': FlowWorkflowDeactivate, + 'flow:workflow:diff': FlowWorkflowDiff, + 'flow:workflow:list': FlowWorkflowList, + 'flow:workflow:preview': FlowWorkflowPreview, + 'flow:workflow:pull': FlowWorkflowPull, + 'flow:workflow:push': FlowWorkflowPush, + 'flow:workflow:show': FlowWorkflowShow, + 'flow:workflow:status': FlowWorkflowStatus, + 'flow:workflow:validate': FlowWorkflowValidate, 'store:auth': StoreAuth, 'store:create:dev': StoreCreateDev, 'store:create:preview': StoreCreatePreview, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2c62418855b..a1fc2b6c1d5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -587,7 +587,7 @@ importers: version: 8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) '@vitest/eslint-plugin': specifier: 1.1.44 - version: 1.1.44(@typescript-eslint/utils@8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.8(@opentelemetry/api@1.9.1)(@types/node@22.19.17)(@vitest/coverage-istanbul@3.2.6)(jsdom@28.1.0)(msw@2.14.6(@types/node@22.19.17)(typescript@5.9.3))(vite@6.4.3(@types/node@22.19.17)(jiti@2.6.1)(sass@1.100.0)(tsx@4.22.4)(yaml@2.9.0))) + version: 1.1.44(@typescript-eslint/utils@8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.8(@opentelemetry/api@1.9.1)(@types/node@22.19.17)(@vitest/coverage-istanbul@3.2.6(vitest@4.1.8))(jsdom@28.1.0)(msw@2.14.6(@types/node@22.19.17)(typescript@5.9.3))(vite@6.4.3(@types/node@22.19.17)(jiti@2.6.1)(sass@1.100.0)(tsx@4.22.4)(yaml@2.9.0))) eslint: specifier: ^9.0.0 version: 9.39.4(jiti@2.6.1) @@ -623,6 +623,43 @@ importers: specifier: 3.8.4 version: 3.8.4 + packages/flow-cli: + dependencies: + '@oclif/core': + specifier: 4.8.3 + version: 4.8.3 + '@shopify/cli-kit': + specifier: 4.3.0 + version: link:../cli-kit + devDependencies: + '@rollup/plugin-alias': + specifier: ^6.0.0 + version: 6.0.0(rollup@3.29.5) + '@rollup/plugin-commonjs': + specifier: ^29.0.3 + version: 29.0.3(rollup@3.29.5) + '@rollup/plugin-json': + specifier: ^6.1.0 + version: 6.1.0(rollup@3.29.5) + '@rollup/plugin-node-resolve': + specifier: ^16.0.3 + version: 16.0.3(rollup@3.29.5) + esbuild: + specifier: 0.28.1 + version: 0.28.1 + esbuild-plugin-copy: + specifier: ^2.1.1 + version: 2.1.1(esbuild@0.28.1) + fast-glob: + specifier: 3.3.3 + version: 3.3.3 + rollup: + specifier: ^3.29.5 + version: 3.29.5 + typescript: + specifier: 5.8.2 + version: 5.8.2 + packages/organizations: dependencies: '@graphql-typed-document-node/core': @@ -920,48 +957,56 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@ast-grep/napi-linux-arm64-gnu@0.43.0': resolution: {integrity: sha512-yJSRPxwwrvVW94J2rtaatcixSAGWcSaHNbAh6soXD6HXgq6I7uMc+cyMnJstFL789yd6Pu3QIhTlpD9VY0oYhw==, tarball: https://registry.npmjs.org/@ast-grep/napi-linux-arm64-gnu/-/napi-linux-arm64-gnu-0.43.0.tgz} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@ast-grep/napi-linux-arm64-musl@0.34.1': resolution: {integrity: sha512-IXdqwTbkdqHrcuQb448Qzd82QdTqVFe/f0sSkFYQTic8P2qNzmiHsVnxgEFsQPPbe09BVAoZ885j3OnaNfcDYA==, tarball: https://registry.npmjs.org/@ast-grep/napi-linux-arm64-musl/-/napi-linux-arm64-musl-0.34.1.tgz} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@ast-grep/napi-linux-arm64-musl@0.43.0': resolution: {integrity: sha512-mknXLDsf66HvT/JEl18ZQSvR7/qWgWfqh3eHuVRqD2lE6cKBDXRnAzx9ZNZkUnL2Z5ph54Yk8dKVu09k45cegA==, tarball: https://registry.npmjs.org/@ast-grep/napi-linux-arm64-musl/-/napi-linux-arm64-musl-0.43.0.tgz} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@ast-grep/napi-linux-x64-gnu@0.34.1': resolution: {integrity: sha512-on4LyIeN/zN7SIh8zr5v+NTzVu3kXm2mG28ib1Qe9GVcf35dz52ckf7bilulayKSa2MHZWAXMjuc6NYMiNEw+w==, tarball: https://registry.npmjs.org/@ast-grep/napi-linux-x64-gnu/-/napi-linux-x64-gnu-0.34.1.tgz} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@ast-grep/napi-linux-x64-gnu@0.43.0': resolution: {integrity: sha512-KM6M5KKFsHG9Y7VKCKnMsWQQ1sYwj/SPdyr91SKp66AeFJ5xMtXb13WVQ3Joe9NEsi84dzzOBIJgMddz+UMvQw==, tarball: https://registry.npmjs.org/@ast-grep/napi-linux-x64-gnu/-/napi-linux-x64-gnu-0.43.0.tgz} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@ast-grep/napi-linux-x64-musl@0.34.1': resolution: {integrity: sha512-l1R5L9LOp0jTPjs8C+LUndZOA8cRw7PFlvoVxVbi2jCfcns00dqatSYc4yA/ke6ng2K0LSxjoV/jS8tefve0sA==, tarball: https://registry.npmjs.org/@ast-grep/napi-linux-x64-musl/-/napi-linux-x64-musl-0.34.1.tgz} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@ast-grep/napi-linux-x64-musl@0.43.0': resolution: {integrity: sha512-NfjI74m7CEEOsLi7ZkYwcxY10CfKIHPHRrr9aqMulmnrC4FGvxQMk1qpDrTqkjmEd8LdZt3PsPrmBa8AZCErew==, tarball: https://registry.npmjs.org/@ast-grep/napi-linux-x64-musl/-/napi-linux-x64-musl-0.43.0.tgz} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@ast-grep/napi-win32-arm64-msvc@0.34.1': resolution: {integrity: sha512-eVsdMtnY7jmN2xQjYY9gaqIxRHA44+QYivlP1uLbg8w3P4YlZWTFgOJ7aa357Hg/257mjeQCpodCkr0lGRsSYQ==, tarball: https://registry.npmjs.org/@ast-grep/napi-win32-arm64-msvc/-/napi-win32-arm64-msvc-0.34.1.tgz} @@ -2967,41 +3012,49 @@ packages: resolution: {integrity: sha512-aaWUYXFaB9ztrICg0WHuz0tzoil+OkSpWi+wtM9PsV+vNQTYWIPclO+OpSp4am68/bdtuMuITOH99EvEIfv7ZA==, tarball: https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-22.0.2.tgz} cpu: [arm64] os: [linux] + libc: [glibc] '@nx/nx-linux-arm64-gnu@22.7.5': resolution: {integrity: sha512-QLnkJl3HkHsPfpLiNiAiMfpfAeFpic0U1diAxF8RqChOkCpQ7ulvyBVgE1UrQxvhd+gFQ3ed5RNDxtCRw8nTiw==, tarball: https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-22.7.5.tgz} cpu: [arm64] os: [linux] + libc: [glibc] '@nx/nx-linux-arm64-musl@22.0.2': resolution: {integrity: sha512-ylT5GBJCUpTXp5ud8f/uRyW9OA2KR65nuFQ5iXNf1KXwfjGuinFDvZEDDj0zGQ4E/PwLrInqBkkSH25Ry99lOQ==, tarball: https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-22.0.2.tgz} cpu: [arm64] os: [linux] + libc: [musl] '@nx/nx-linux-arm64-musl@22.7.5': resolution: {integrity: sha512-cEP6KmwBgnb38+jTTaibWCjwXcHmigqhTfy0tN1be7WZr6bHxbqNLsXqKRN70PSNA3HouZcxw1cdRL8tqbPBBA==, tarball: https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-22.7.5.tgz} cpu: [arm64] os: [linux] + libc: [musl] '@nx/nx-linux-x64-gnu@22.0.2': resolution: {integrity: sha512-N8beYlkdKbAC5CA3i5WoqUUbbsSO/0cQk3gMW7c41bouqdMWDUKG6m50d4yHk8V7RFC+sqY59tso3rYmXW3big==, tarball: https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-22.0.2.tgz} cpu: [x64] os: [linux] + libc: [glibc] '@nx/nx-linux-x64-gnu@22.7.5': resolution: {integrity: sha512-tbaX1tZCSpGifDNBfDdEZAMxVF3Yg4bhFP/bm1needc0diqb+Zflc0u5tM5/6BWDMITQDwenJVsNiQ8ZdtJURA==, tarball: https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-22.7.5.tgz} cpu: [x64] os: [linux] + libc: [glibc] '@nx/nx-linux-x64-musl@22.0.2': resolution: {integrity: sha512-Q0joIxZHs9JVr/+6x1bee7z+7Z4SoO0mbhADuugjxly50O44Igg+rx78Iou00VrtSR+Ht5NlpILxOe4GhpFCpA==, tarball: https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-22.0.2.tgz} cpu: [x64] os: [linux] + libc: [musl] '@nx/nx-linux-x64-musl@22.7.5': resolution: {integrity: sha512-H0M7csOZIgPT822LqjxSXzf4MXRND15vIkAQe3F3Jlr3Si8LC3tzbL52aVcRfgb8MF/xOB5U47mSwxWt1M2bPQ==, tarball: https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-22.7.5.tgz} cpu: [x64] os: [linux] + libc: [musl] '@nx/nx-win32-arm64-msvc@22.0.2': resolution: {integrity: sha512-/4FXsBh+SB6fKFeVBFptPPWJIeFPQWmK29Q+XLrjYW/31bOs1k2uwn+7QYX0D+Z4HiME3iiRdAInFD9pVlyZbQ==, tarball: https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-22.0.2.tgz} @@ -3314,41 +3367,49 @@ packages: resolution: {integrity: sha512-0bJnmYFp62JdZ4nVMDUZ/C58BCZOCcqgKtnUlp7L9Ojf/czIN+3j72YlLPeWLkzlr6SlYvIQA4SGV/HyO0d+qg==, tarball: https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.20.0.tgz} cpu: [arm64] os: [linux] + libc: [glibc] '@oxc-resolver/binding-linux-arm64-musl@11.20.0': resolution: {integrity: sha512-wKHHzPKZo7Ufhv/Bt6yxT7FOgnIgW4gwXcJUipkShGp68W3wGVqvr1Sr0fY65lN0Oy6y41+g2kIDvkgZaMMUkw==, tarball: https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.20.0.tgz} cpu: [arm64] os: [linux] + libc: [musl] '@oxc-resolver/binding-linux-ppc64-gnu@11.20.0': resolution: {integrity: sha512-RN8goF7Ie0B79L4i4G6OeBocTgSC56vJbQ65VJje+oXnldVpLnOU7j/AQ/dP94TcCS+Yh6WG8u3Qt4ETteXFNQ==, tarball: https://registry.npmjs.org/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.20.0.tgz} cpu: [ppc64] os: [linux] + libc: [glibc] '@oxc-resolver/binding-linux-riscv64-gnu@11.20.0': resolution: {integrity: sha512-5l1yU6/xQEqLZRzxqmMxJfWPslpwCmBsdDGaBvABPehxquCXDC7dd7oraNdKSJUMDXSM7VvVj8H2D2FTjU7oWw==, tarball: https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.20.0.tgz} cpu: [riscv64] os: [linux] + libc: [glibc] '@oxc-resolver/binding-linux-riscv64-musl@11.20.0': resolution: {integrity: sha512-xHEvkbgz6UC+A3JOyDQy76LkUaxsNSfIr3/GV8slwZsnuooJiIB34gzJfsyvR4JdCYNUUPsRJc/w/oWkODu+hg==, tarball: https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.20.0.tgz} cpu: [riscv64] os: [linux] + libc: [musl] '@oxc-resolver/binding-linux-s390x-gnu@11.20.0': resolution: {integrity: sha512-aWPDUUmSeyHvlW+SoEUd+JIJsQhVhu6a5tBpDRMu058naPAchTgAVGCFy35zjbnFlt0i8hLWziff6HX0D3LU4g==, tarball: https://registry.npmjs.org/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.20.0.tgz} cpu: [s390x] os: [linux] + libc: [glibc] '@oxc-resolver/binding-linux-x64-gnu@11.20.0': resolution: {integrity: sha512-x2YeSimvhJjKLVD8KSu8f/rqU1potcdEMkApIPJqjZWN7c2Fpt4g2X32WDg1p+XDAmyT7nuQGe0vnhvXeLbH+g==, tarball: https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.20.0.tgz} cpu: [x64] os: [linux] + libc: [glibc] '@oxc-resolver/binding-linux-x64-musl@11.20.0': resolution: {integrity: sha512-kcRLEIxpZefeYfLChjpgFf3ilBzRDZ+yobMrpRsQlSrxuFGtm3U6PMU7AaEpMqo3NfDGVyJJseAjnRLzMFHjwQ==, tarball: https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.20.0.tgz} cpu: [x64] os: [linux] + libc: [musl] '@oxc-resolver/binding-openharmony-arm64@11.20.0': resolution: {integrity: sha512-HHcfnApSZGtKhTiHqe8OZruOZe5XuFQH5/E0Yhj3u8fnFvzkM4/k6WjacUf4SvA0SPEAbfbgYmVPuo0VX/fIBQ==, tarball: https://registry.npmjs.org/@oxc-resolver/binding-openharmony-arm64/-/binding-openharmony-arm64-11.20.0.tgz} @@ -3399,36 +3460,42 @@ packages: engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm-musl@2.5.6': resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==, tarball: https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [musl] '@parcel/watcher-linux-arm64-glibc@2.5.6': resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==, tarball: https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm64-musl@2.5.6': resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==, tarball: https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [musl] '@parcel/watcher-linux-x64-glibc@2.5.6': resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==, tarball: https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-x64-musl@2.5.6': resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==, tarball: https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [musl] '@parcel/watcher-win32-arm64@2.5.6': resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==, tarball: https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz} @@ -3518,128 +3585,186 @@ packages: '@rolldown/pluginutils@1.0.0-rc.3': resolution: {integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==, tarball: https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz} - '@rollup/rollup-android-arm-eabi@4.59.0': - resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==, tarball: https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz} + '@rollup/plugin-alias@6.0.0': + resolution: {integrity: sha512-tPCzJOtS7uuVZd+xPhoy5W4vThe6KWXNmsFCNktaAh5RTqcLiSfT4huPQIXkgJ6YCOjJHvecOAzQxLFhPxKr+g==, tarball: https://registry.npmjs.org/@rollup/plugin-alias/-/plugin-alias-6.0.0.tgz} + engines: {node: '>=20.19.0'} + peerDependencies: + rollup: '>=4.0.0' + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-commonjs@29.0.3': + resolution: {integrity: sha512-ZaOxZceP7SOUW7Lqw5IRVweSQYWaeIPnXIGLiB690EBA3FGJTO40EEr2L5yZplJWsgTCogILRSpcAe7+U0Otdg==, tarball: https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-29.0.3.tgz} + engines: {node: '>=16.0.0 || 14 >= 14.17'} + peerDependencies: + rollup: ^2.68.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-json@6.1.0': + resolution: {integrity: sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==, tarball: https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-node-resolve@16.0.3': + resolution: {integrity: sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==, tarball: https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.78.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/pluginutils@5.4.0': + resolution: {integrity: sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==, tarball: https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.62.2': + resolution: {integrity: sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==, tarball: https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.59.0': - resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==, tarball: https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz} + '@rollup/rollup-android-arm64@4.62.2': + resolution: {integrity: sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==, tarball: https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.59.0': - resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==, tarball: https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz} + '@rollup/rollup-darwin-arm64@4.62.2': + resolution: {integrity: sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==, tarball: https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.59.0': - resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==, tarball: https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz} + '@rollup/rollup-darwin-x64@4.62.2': + resolution: {integrity: sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==, tarball: https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.59.0': - resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==, tarball: https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz} + '@rollup/rollup-freebsd-arm64@4.62.2': + resolution: {integrity: sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==, tarball: https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.59.0': - resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==, tarball: https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz} + '@rollup/rollup-freebsd-x64@4.62.2': + resolution: {integrity: sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==, tarball: https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.59.0': - resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz} + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': + resolution: {integrity: sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz} cpu: [arm] os: [linux] + libc: [glibc] - '@rollup/rollup-linux-arm-musleabihf@4.59.0': - resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz} + '@rollup/rollup-linux-arm-musleabihf@4.62.2': + resolution: {integrity: sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz} cpu: [arm] os: [linux] + libc: [musl] - '@rollup/rollup-linux-arm64-gnu@4.59.0': - resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz} + '@rollup/rollup-linux-arm64-gnu@4.62.2': + resolution: {integrity: sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz} cpu: [arm64] os: [linux] + libc: [glibc] - '@rollup/rollup-linux-arm64-musl@4.59.0': - resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz} + '@rollup/rollup-linux-arm64-musl@4.62.2': + resolution: {integrity: sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz} cpu: [arm64] os: [linux] + libc: [musl] - '@rollup/rollup-linux-loong64-gnu@4.59.0': - resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz} + '@rollup/rollup-linux-loong64-gnu@4.62.2': + resolution: {integrity: sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz} cpu: [loong64] os: [linux] + libc: [glibc] - '@rollup/rollup-linux-loong64-musl@4.59.0': - resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz} + '@rollup/rollup-linux-loong64-musl@4.62.2': + resolution: {integrity: sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz} cpu: [loong64] os: [linux] + libc: [musl] - '@rollup/rollup-linux-ppc64-gnu@4.59.0': - resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz} + '@rollup/rollup-linux-ppc64-gnu@4.62.2': + resolution: {integrity: sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz} cpu: [ppc64] os: [linux] + libc: [glibc] - '@rollup/rollup-linux-ppc64-musl@4.59.0': - resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz} + '@rollup/rollup-linux-ppc64-musl@4.62.2': + resolution: {integrity: sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz} cpu: [ppc64] os: [linux] + libc: [musl] - '@rollup/rollup-linux-riscv64-gnu@4.59.0': - resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz} + '@rollup/rollup-linux-riscv64-gnu@4.62.2': + resolution: {integrity: sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz} cpu: [riscv64] os: [linux] + libc: [glibc] - '@rollup/rollup-linux-riscv64-musl@4.59.0': - resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz} + '@rollup/rollup-linux-riscv64-musl@4.62.2': + resolution: {integrity: sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz} cpu: [riscv64] os: [linux] + libc: [musl] - '@rollup/rollup-linux-s390x-gnu@4.59.0': - resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz} + '@rollup/rollup-linux-s390x-gnu@4.62.2': + resolution: {integrity: sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz} cpu: [s390x] os: [linux] + libc: [glibc] - '@rollup/rollup-linux-x64-gnu@4.59.0': - resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz} + '@rollup/rollup-linux-x64-gnu@4.62.2': + resolution: {integrity: sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz} cpu: [x64] os: [linux] + libc: [glibc] - '@rollup/rollup-linux-x64-musl@4.59.0': - resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz} + '@rollup/rollup-linux-x64-musl@4.62.2': + resolution: {integrity: sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz} cpu: [x64] os: [linux] + libc: [musl] - '@rollup/rollup-openbsd-x64@4.59.0': - resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==, tarball: https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz} + '@rollup/rollup-openbsd-x64@4.62.2': + resolution: {integrity: sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==, tarball: https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz} cpu: [x64] os: [openbsd] - '@rollup/rollup-openharmony-arm64@4.59.0': - resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==, tarball: https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz} + '@rollup/rollup-openharmony-arm64@4.62.2': + resolution: {integrity: sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==, tarball: https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.59.0': - resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==, tarball: https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz} + '@rollup/rollup-win32-arm64-msvc@4.62.2': + resolution: {integrity: sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==, tarball: https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.59.0': - resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==, tarball: https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz} + '@rollup/rollup-win32-ia32-msvc@4.62.2': + resolution: {integrity: sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==, tarball: https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.59.0': - resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==, tarball: https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz} + '@rollup/rollup-win32-x64-gnu@4.62.2': + resolution: {integrity: sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==, tarball: https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.59.0': - resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==, tarball: https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz} + '@rollup/rollup-win32-x64-msvc@4.62.2': + resolution: {integrity: sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==, tarball: https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz} cpu: [x64] os: [win32] @@ -4083,6 +4208,9 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==, tarball: https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz} + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==, tarball: https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz} + '@types/fs-extra@9.0.13': resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==, tarball: https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz} @@ -4158,6 +4286,9 @@ packages: '@types/readdir-glob@1.1.5': resolution: {integrity: sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==, tarball: https://registry.npmjs.org/@types/readdir-glob/-/readdir-glob-1.1.5.tgz} + '@types/resolve@1.20.2': + resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==, tarball: https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz} + '@types/retry@0.12.5': resolution: {integrity: sha512-3xSjTp3v03X/lSQLkczaN9UIEwJMoMCA1+Nb5HfbJEQWogdeQIyVtTvxPXDQjZ5zws8rFQfVfRdz03ARihPJgw==, tarball: https://registry.npmjs.org/@types/retry/-/retry-0.12.5.tgz} @@ -4283,41 +4414,49 @@ packages: resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==, tarball: https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz} cpu: [arm64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-arm64-musl@1.11.1': resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==, tarball: https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz} cpu: [arm64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==, tarball: https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz} cpu: [ppc64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==, tarball: https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz} cpu: [riscv64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==, tarball: https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz} cpu: [riscv64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==, tarball: https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz} cpu: [s390x] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.11.1': resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==, tarball: https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz} cpu: [x64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-musl@1.11.1': resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==, tarball: https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz} cpu: [x64] os: [linux] + libc: [musl] '@unrs/resolver-binding-wasm32-wasi@1.11.1': resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==, tarball: https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz} @@ -4743,10 +4882,6 @@ packages: brace-expansion@2.1.0: resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==, tarball: https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz} - brace-expansion@5.0.5: - resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==, tarball: https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz} - engines: {node: 18 || 20 || >=22} - brace-expansion@5.0.6: resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==, tarball: https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz} engines: {node: 18 || 20 || >=22} @@ -5004,6 +5139,9 @@ packages: resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==, tarball: https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz} engines: {node: '>=4.0.0'} + commondir@1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==, tarball: https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz} + compress-commons@4.1.2: resolution: {integrity: sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==, tarball: https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz} engines: {node: '>= 10'} @@ -5736,6 +5874,9 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==, tarball: https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz} engines: {node: '>=4.0'} + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==, tarball: https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz} + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==, tarball: https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz} @@ -6505,6 +6646,9 @@ packages: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==, tarball: https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz} engines: {node: '>= 0.4'} + is-module@1.0.0: + resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==, tarball: https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz} + is-negative-zero@2.0.3: resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==, tarball: https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz} engines: {node: '>= 0.4'} @@ -6531,6 +6675,9 @@ packages: is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==, tarball: https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz} + is-reference@1.2.1: + resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==, tarball: https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz} + is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==, tarball: https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz} engines: {node: '>= 0.4'} @@ -7614,10 +7761,6 @@ packages: resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==, tarball: https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz} engines: {node: '>=12'} - picomatch@4.0.3: - resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==, tarball: https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz} - engines: {node: '>=12'} - picomatch@4.0.4: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==, tarball: https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz} engines: {node: '>=12'} @@ -8006,8 +8149,13 @@ packages: resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==, tarball: https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz} engines: {node: '>=8.0'} - rollup@4.59.0: - resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==, tarball: https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz} + rollup@3.29.5: + resolution: {integrity: sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==, tarball: https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz} + engines: {node: '>=14.18.0', npm: '>=8.0.0'} + hasBin: true + + rollup@4.62.2: + resolution: {integrity: sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==, tarball: https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -8671,6 +8819,11 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' + typescript@5.8.2: + resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==, tarball: https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz} + engines: {node: '>=14.17'} + hasBin: true + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==, tarball: https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz} engines: {node: '>=14.17'} @@ -10823,7 +10976,7 @@ snapshots: '@es-joy/jsdoccomment@0.50.2': dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 '@typescript-eslint/types': 8.56.1 comment-parser: 1.4.1 esquery: 1.7.0 @@ -12700,79 +12853,119 @@ snapshots: '@rolldown/pluginutils@1.0.0-rc.3': {} - '@rollup/rollup-android-arm-eabi@4.59.0': + '@rollup/plugin-alias@6.0.0(rollup@3.29.5)': + optionalDependencies: + rollup: 3.29.5 + + '@rollup/plugin-commonjs@29.0.3(rollup@3.29.5)': + dependencies: + '@rollup/pluginutils': 5.4.0(rollup@3.29.5) + commondir: 1.0.1 + estree-walker: 2.0.2 + fdir: 6.5.0(picomatch@4.0.4) + is-reference: 1.2.1 + magic-string: 0.30.21 + picomatch: 4.0.4 + optionalDependencies: + rollup: 3.29.5 + + '@rollup/plugin-json@6.1.0(rollup@3.29.5)': + dependencies: + '@rollup/pluginutils': 5.4.0(rollup@3.29.5) + optionalDependencies: + rollup: 3.29.5 + + '@rollup/plugin-node-resolve@16.0.3(rollup@3.29.5)': + dependencies: + '@rollup/pluginutils': 5.4.0(rollup@3.29.5) + '@types/resolve': 1.20.2 + deepmerge: 4.3.1 + is-module: 1.0.0 + resolve: 1.22.11 + optionalDependencies: + rollup: 3.29.5 + + '@rollup/pluginutils@5.4.0(rollup@3.29.5)': + dependencies: + '@types/estree': 1.0.9 + estree-walker: 2.0.2 + picomatch: 4.0.4 + optionalDependencies: + rollup: 3.29.5 + + '@rollup/rollup-android-arm-eabi@4.62.2': optional: true - '@rollup/rollup-android-arm64@4.59.0': + '@rollup/rollup-android-arm64@4.62.2': optional: true - '@rollup/rollup-darwin-arm64@4.59.0': + '@rollup/rollup-darwin-arm64@4.62.2': optional: true - '@rollup/rollup-darwin-x64@4.59.0': + '@rollup/rollup-darwin-x64@4.62.2': optional: true - '@rollup/rollup-freebsd-arm64@4.59.0': + '@rollup/rollup-freebsd-arm64@4.62.2': optional: true - '@rollup/rollup-freebsd-x64@4.59.0': + '@rollup/rollup-freebsd-x64@4.62.2': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.59.0': + '@rollup/rollup-linux-arm-musleabihf@4.62.2': optional: true - '@rollup/rollup-linux-arm64-gnu@4.59.0': + '@rollup/rollup-linux-arm64-gnu@4.62.2': optional: true - '@rollup/rollup-linux-arm64-musl@4.59.0': + '@rollup/rollup-linux-arm64-musl@4.62.2': optional: true - '@rollup/rollup-linux-loong64-gnu@4.59.0': + '@rollup/rollup-linux-loong64-gnu@4.62.2': optional: true - '@rollup/rollup-linux-loong64-musl@4.59.0': + '@rollup/rollup-linux-loong64-musl@4.62.2': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.59.0': + '@rollup/rollup-linux-ppc64-gnu@4.62.2': optional: true - '@rollup/rollup-linux-ppc64-musl@4.59.0': + '@rollup/rollup-linux-ppc64-musl@4.62.2': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.59.0': + '@rollup/rollup-linux-riscv64-gnu@4.62.2': optional: true - '@rollup/rollup-linux-riscv64-musl@4.59.0': + '@rollup/rollup-linux-riscv64-musl@4.62.2': optional: true - '@rollup/rollup-linux-s390x-gnu@4.59.0': + '@rollup/rollup-linux-s390x-gnu@4.62.2': optional: true - '@rollup/rollup-linux-x64-gnu@4.59.0': + '@rollup/rollup-linux-x64-gnu@4.62.2': optional: true - '@rollup/rollup-linux-x64-musl@4.59.0': + '@rollup/rollup-linux-x64-musl@4.62.2': optional: true - '@rollup/rollup-openbsd-x64@4.59.0': + '@rollup/rollup-openbsd-x64@4.62.2': optional: true - '@rollup/rollup-openharmony-arm64@4.59.0': + '@rollup/rollup-openharmony-arm64@4.62.2': optional: true - '@rollup/rollup-win32-arm64-msvc@4.59.0': + '@rollup/rollup-win32-arm64-msvc@4.62.2': optional: true - '@rollup/rollup-win32-ia32-msvc@4.59.0': + '@rollup/rollup-win32-ia32-msvc@4.62.2': optional: true - '@rollup/rollup-win32-x64-gnu@4.59.0': + '@rollup/rollup-win32-x64-gnu@4.62.2': optional: true - '@rollup/rollup-win32-x64-msvc@4.59.0': + '@rollup/rollup-win32-x64-msvc@4.62.2': optional: true '@shikijs/engine-oniguruma@3.23.0': @@ -13500,6 +13693,8 @@ snapshots: '@types/estree@1.0.8': {} + '@types/estree@1.0.9': {} + '@types/fs-extra@9.0.13': dependencies: '@types/node': 18.19.130 @@ -13573,6 +13768,8 @@ snapshots: dependencies: '@types/node': 18.19.130 + '@types/resolve@1.20.2': {} + '@types/retry@0.12.5': {} '@types/semver@7.7.1': {} @@ -13664,7 +13861,7 @@ snapshots: debug: 4.4.3(supports-color@8.1.1) minimatch: 10.2.5 semver: 7.8.4 - tinyglobby: 0.2.15 + tinyglobby: 0.2.16 ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: @@ -13769,17 +13966,17 @@ snapshots: magicast: 0.3.5 test-exclude: 7.0.2 tinyrainbow: 2.0.0 - vitest: 4.1.8(@opentelemetry/api@1.9.1)(@types/node@22.19.17)(@vitest/coverage-istanbul@3.2.6)(jsdom@28.1.0)(msw@2.14.6(@types/node@22.19.17)(typescript@5.9.3))(vite@6.4.3(@types/node@22.19.17)(jiti@2.6.1)(sass@1.100.0)(tsx@4.22.4)(yaml@2.9.0)) + vitest: 4.1.8(@opentelemetry/api@1.9.1)(@types/node@18.19.130)(@vitest/coverage-istanbul@3.2.6)(jsdom@28.1.0)(msw@2.14.6(@types/node@18.19.130)(typescript@5.9.3))(vite@6.4.3(@types/node@18.19.130)(jiti@2.6.1)(sass@1.100.0)(tsx@4.22.4)(yaml@2.9.0)) transitivePeerDependencies: - supports-color - '@vitest/eslint-plugin@1.1.44(@typescript-eslint/utils@8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.8(@opentelemetry/api@1.9.1)(@types/node@22.19.17)(@vitest/coverage-istanbul@3.2.6)(jsdom@28.1.0)(msw@2.14.6(@types/node@22.19.17)(typescript@5.9.3))(vite@6.4.3(@types/node@22.19.17)(jiti@2.6.1)(sass@1.100.0)(tsx@4.22.4)(yaml@2.9.0)))': + '@vitest/eslint-plugin@1.1.44(@typescript-eslint/utils@8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.8(@opentelemetry/api@1.9.1)(@types/node@22.19.17)(@vitest/coverage-istanbul@3.2.6(vitest@4.1.8))(jsdom@28.1.0)(msw@2.14.6(@types/node@22.19.17)(typescript@5.9.3))(vite@6.4.3(@types/node@22.19.17)(jiti@2.6.1)(sass@1.100.0)(tsx@4.22.4)(yaml@2.9.0)))': dependencies: '@typescript-eslint/utils': 8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.4(jiti@2.6.1) optionalDependencies: typescript: 5.9.3 - vitest: 4.1.8(@opentelemetry/api@1.9.1)(@types/node@22.19.17)(@vitest/coverage-istanbul@3.2.6)(jsdom@28.1.0)(msw@2.14.6(@types/node@22.19.17)(typescript@5.9.3))(vite@6.4.3(@types/node@22.19.17)(jiti@2.6.1)(sass@1.100.0)(tsx@4.22.4)(yaml@2.9.0)) + vitest: 4.1.8(@opentelemetry/api@1.9.1)(@types/node@22.19.17)(@vitest/coverage-istanbul@3.2.6(vitest@4.1.8))(jsdom@28.1.0)(msw@2.14.6(@types/node@22.19.17)(typescript@5.9.3))(vite@6.4.3(@types/node@22.19.17)(jiti@2.6.1)(sass@1.100.0)(tsx@4.22.4)(yaml@2.9.0)) '@vitest/eslint-plugin@1.1.44(@typescript-eslint/utils@8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.8)': dependencies: @@ -13815,6 +14012,7 @@ snapshots: optionalDependencies: msw: 2.14.6(@types/node@22.19.17)(typescript@5.9.3) vite: 6.4.3(@types/node@22.19.17)(jiti@2.6.1)(sass@1.100.0)(tsx@4.22.4)(yaml@2.9.0) + optional: true '@vitest/pretty-format@4.1.8': dependencies: @@ -14224,10 +14422,6 @@ snapshots: dependencies: balanced-match: 1.0.2 - brace-expansion@5.0.5: - dependencies: - balanced-match: 4.0.4 - brace-expansion@5.0.6: dependencies: balanced-match: 4.0.4 @@ -14441,7 +14635,7 @@ snapshots: cli-truncate@5.1.1: dependencies: slice-ansi: 7.1.2 - string-width: 8.2.0 + string-width: 8.2.1 cli-width@4.1.0: {} @@ -14524,6 +14718,8 @@ snapshots: common-tags@1.8.2: {} + commondir@1.0.1: {} + compress-commons@4.1.2: dependencies: buffer-crc32: 0.2.13 @@ -15134,7 +15330,7 @@ snapshots: get-tsconfig: 4.13.6 is-bun-module: 2.0.0 stable-hash-x: 0.2.0 - tinyglobby: 0.2.15 + tinyglobby: 0.2.16 unrs-resolver: 1.11.1 optionalDependencies: eslint-plugin-import-x: 4.16.1(@typescript-eslint/utils@8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1)) @@ -15394,9 +15590,11 @@ snapshots: estraverse@5.3.0: {} + estree-walker@2.0.2: {} + estree-walker@3.0.3: dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 esutils@2.0.3: {} @@ -15499,10 +15697,6 @@ snapshots: dependencies: walk-up-path: 4.0.0 - fdir@6.5.0(picomatch@4.0.3): - optionalDependencies: - picomatch: 4.0.3 - fdir@6.5.0(picomatch@4.0.4): optionalDependencies: picomatch: 4.0.4 @@ -16268,6 +16462,8 @@ snapshots: is-map@2.0.3: {} + is-module@1.0.0: {} + is-negative-zero@2.0.3: {} is-node-process@1.2.0: {} @@ -16285,6 +16481,10 @@ snapshots: is-potential-custom-element-name@1.0.1: {} + is-reference@1.2.1: + dependencies: + '@types/estree': 1.0.9 + is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -16838,7 +17038,7 @@ snapshots: minimatch@10.2.5: dependencies: - brace-expansion: 5.0.5 + brace-expansion: 5.0.6 minimatch@3.1.5: dependencies: @@ -17529,8 +17729,6 @@ snapshots: picomatch@4.0.2: {} - picomatch@4.0.3: {} - picomatch@4.0.4: {} pify@4.0.1: {} @@ -17964,35 +18162,39 @@ snapshots: semver-compare: 1.0.0 sprintf-js: 1.1.3 - rollup@4.59.0: + rollup@3.29.5: + optionalDependencies: + fsevents: 2.3.3 + + rollup@4.62.2: dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.59.0 - '@rollup/rollup-android-arm64': 4.59.0 - '@rollup/rollup-darwin-arm64': 4.59.0 - '@rollup/rollup-darwin-x64': 4.59.0 - '@rollup/rollup-freebsd-arm64': 4.59.0 - '@rollup/rollup-freebsd-x64': 4.59.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.59.0 - '@rollup/rollup-linux-arm-musleabihf': 4.59.0 - '@rollup/rollup-linux-arm64-gnu': 4.59.0 - '@rollup/rollup-linux-arm64-musl': 4.59.0 - '@rollup/rollup-linux-loong64-gnu': 4.59.0 - '@rollup/rollup-linux-loong64-musl': 4.59.0 - '@rollup/rollup-linux-ppc64-gnu': 4.59.0 - '@rollup/rollup-linux-ppc64-musl': 4.59.0 - '@rollup/rollup-linux-riscv64-gnu': 4.59.0 - '@rollup/rollup-linux-riscv64-musl': 4.59.0 - '@rollup/rollup-linux-s390x-gnu': 4.59.0 - '@rollup/rollup-linux-x64-gnu': 4.59.0 - '@rollup/rollup-linux-x64-musl': 4.59.0 - '@rollup/rollup-openbsd-x64': 4.59.0 - '@rollup/rollup-openharmony-arm64': 4.59.0 - '@rollup/rollup-win32-arm64-msvc': 4.59.0 - '@rollup/rollup-win32-ia32-msvc': 4.59.0 - '@rollup/rollup-win32-x64-gnu': 4.59.0 - '@rollup/rollup-win32-x64-msvc': 4.59.0 + '@rollup/rollup-android-arm-eabi': 4.62.2 + '@rollup/rollup-android-arm64': 4.62.2 + '@rollup/rollup-darwin-arm64': 4.62.2 + '@rollup/rollup-darwin-x64': 4.62.2 + '@rollup/rollup-freebsd-arm64': 4.62.2 + '@rollup/rollup-freebsd-x64': 4.62.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.62.2 + '@rollup/rollup-linux-arm-musleabihf': 4.62.2 + '@rollup/rollup-linux-arm64-gnu': 4.62.2 + '@rollup/rollup-linux-arm64-musl': 4.62.2 + '@rollup/rollup-linux-loong64-gnu': 4.62.2 + '@rollup/rollup-linux-loong64-musl': 4.62.2 + '@rollup/rollup-linux-ppc64-gnu': 4.62.2 + '@rollup/rollup-linux-ppc64-musl': 4.62.2 + '@rollup/rollup-linux-riscv64-gnu': 4.62.2 + '@rollup/rollup-linux-riscv64-musl': 4.62.2 + '@rollup/rollup-linux-s390x-gnu': 4.62.2 + '@rollup/rollup-linux-x64-gnu': 4.62.2 + '@rollup/rollup-linux-x64-musl': 4.62.2 + '@rollup/rollup-openbsd-x64': 4.62.2 + '@rollup/rollup-openharmony-arm64': 4.62.2 + '@rollup/rollup-win32-arm64-msvc': 4.62.2 + '@rollup/rollup-win32-ia32-msvc': 4.62.2 + '@rollup/rollup-win32-x64-gnu': 4.62.2 + '@rollup/rollup-win32-x64-msvc': 4.62.2 fsevents: 2.3.3 rrweb-cssom@0.7.1: {} @@ -18502,8 +18704,8 @@ snapshots: tinyglobby@0.2.15: dependencies: - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 tinyglobby@0.2.16: dependencies: @@ -18705,6 +18907,8 @@ snapshots: transitivePeerDependencies: - supports-color + typescript@5.8.2: {} + typescript@5.9.3: {} typical@4.0.0: {} @@ -18835,7 +19039,7 @@ snapshots: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 postcss: 8.5.10 - rollup: 4.59.0 + rollup: 4.62.2 tinyglobby: 0.2.16 optionalDependencies: '@types/node': 18.19.130 @@ -18851,7 +19055,7 @@ snapshots: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 postcss: 8.5.10 - rollup: 4.59.0 + rollup: 4.62.2 tinyglobby: 0.2.16 optionalDependencies: '@types/node': 22.19.17 @@ -18891,7 +19095,7 @@ snapshots: transitivePeerDependencies: - msw - vitest@4.1.8(@opentelemetry/api@1.9.1)(@types/node@22.19.17)(@vitest/coverage-istanbul@3.2.6)(jsdom@28.1.0)(msw@2.14.6(@types/node@22.19.17)(typescript@5.9.3))(vite@6.4.3(@types/node@22.19.17)(jiti@2.6.1)(sass@1.100.0)(tsx@4.22.4)(yaml@2.9.0)): + vitest@4.1.8(@opentelemetry/api@1.9.1)(@types/node@22.19.17)(@vitest/coverage-istanbul@3.2.6(vitest@4.1.8))(jsdom@28.1.0)(msw@2.14.6(@types/node@22.19.17)(typescript@5.9.3))(vite@6.4.3(@types/node@22.19.17)(jiti@2.6.1)(sass@1.100.0)(tsx@4.22.4)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.8 '@vitest/mocker': 4.1.8(msw@2.14.6(@types/node@22.19.17)(typescript@5.9.3))(vite@6.4.3(@types/node@22.19.17)(jiti@2.6.1)(sass@1.100.0)(tsx@4.22.4)(yaml@2.9.0)) @@ -18920,6 +19124,7 @@ snapshots: jsdom: 28.1.0 transitivePeerDependencies: - msw + optional: true vscode-css-languageservice@6.3.2: dependencies: @@ -19052,7 +19257,7 @@ snapshots: widest-line@6.0.0: dependencies: - string-width: 8.2.0 + string-width: 8.2.1 word-wrap@1.2.5: {}