diff --git a/docs-shopify.dev/generated/generated_docs_data_v2.json b/docs-shopify.dev/generated/generated_docs_data_v2.json index 2520c347e81..a70b48c0fdc 100644 --- a/docs-shopify.dev/generated/generated_docs_data_v2.json +++ b/docs-shopify.dev/generated/generated_docs_data_v2.json @@ -411,6 +411,98 @@ "value": "export interface appbulkstatus {\n /**\n * Alias of the Shopify account to use for authentication.\n * @environment SHOPIFY_FLAG_AUTH_ALIAS\n */\n '--auth-alias '?: string\n\n /**\n * The Client ID of your app.\n * @environment SHOPIFY_FLAG_CLIENT_ID\n */\n '--client-id '?: string\n\n /**\n * The name of the app configuration.\n * @environment SHOPIFY_FLAG_APP_CONFIG\n */\n '-c, --config '?: string\n\n /**\n * 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.\n * @environment SHOPIFY_FLAG_ID\n */\n '--id '?: string\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * The path to your app directory.\n * @environment SHOPIFY_FLAG_PATH\n */\n '--path '?: string\n\n /**\n * Reset all your settings.\n * @environment SHOPIFY_FLAG_RESET\n */\n '--reset'?: ''\n\n /**\n * The store domain. Must be an existing dev store.\n * @environment SHOPIFY_FLAG_STORE\n */\n '-s, --store '?: string\n\n /**\n * Increase the verbosity of the output. May include sensitive data.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}" } }, + "appchannelspecgenerate": { + "docs-shopify.dev/commands/interfaces/app-channel-spec-generate.interface.ts": { + "filePath": "docs-shopify.dev/commands/interfaces/app-channel-spec-generate.interface.ts", + "name": "appchannelspecgenerate", + "description": "The following flags are available for the `app channel-spec generate` command:", + "isPublicDocs": true, + "members": [ + { + "filePath": "docs-shopify.dev/commands/interfaces/app-channel-spec-generate.interface.ts", + "syntaxKind": "PropertySignature", + "name": "--auth-alias ", + "value": "string", + "description": "Alias of the Shopify account to use for authentication.", + "isOptional": true, + "environmentValue": "SHOPIFY_FLAG_AUTH_ALIAS" + }, + { + "filePath": "docs-shopify.dev/commands/interfaces/app-channel-spec-generate.interface.ts", + "syntaxKind": "PropertySignature", + "name": "--client-id ", + "value": "string", + "description": "The Client ID of your app.", + "isOptional": true, + "environmentValue": "SHOPIFY_FLAG_CLIENT_ID" + }, + { + "filePath": "docs-shopify.dev/commands/interfaces/app-channel-spec-generate.interface.ts", + "syntaxKind": "PropertySignature", + "name": "--no-color", + "value": "''", + "description": "Disable color output.", + "isOptional": true, + "environmentValue": "SHOPIFY_FLAG_NO_COLOR" + }, + { + "filePath": "docs-shopify.dev/commands/interfaces/app-channel-spec-generate.interface.ts", + "syntaxKind": "PropertySignature", + "name": "--overwrite", + "value": "''", + "description": "Overwrite the existing channel spec file if one already exists.", + "isOptional": true, + "environmentValue": "SHOPIFY_FLAG_OVERWRITE" + }, + { + "filePath": "docs-shopify.dev/commands/interfaces/app-channel-spec-generate.interface.ts", + "syntaxKind": "PropertySignature", + "name": "--path ", + "value": "string", + "description": "The path to your app directory.", + "isOptional": true, + "environmentValue": "SHOPIFY_FLAG_PATH" + }, + { + "filePath": "docs-shopify.dev/commands/interfaces/app-channel-spec-generate.interface.ts", + "syntaxKind": "PropertySignature", + "name": "--reset", + "value": "''", + "description": "Reset all your settings.", + "isOptional": true, + "environmentValue": "SHOPIFY_FLAG_RESET" + }, + { + "filePath": "docs-shopify.dev/commands/interfaces/app-channel-spec-generate.interface.ts", + "syntaxKind": "PropertySignature", + "name": "--stdout", + "value": "''", + "description": "Print the generated TOML to stdout instead of writing it to a file.", + "isOptional": true, + "environmentValue": "SHOPIFY_FLAG_STDOUT" + }, + { + "filePath": "docs-shopify.dev/commands/interfaces/app-channel-spec-generate.interface.ts", + "syntaxKind": "PropertySignature", + "name": "--verbose", + "value": "''", + "description": "Increase the verbosity of the output. May include sensitive data.", + "isOptional": true, + "environmentValue": "SHOPIFY_FLAG_VERBOSE" + }, + { + "filePath": "docs-shopify.dev/commands/interfaces/app-channel-spec-generate.interface.ts", + "syntaxKind": "PropertySignature", + "name": "-c, --config ", + "value": "string", + "description": "The name of the app configuration.", + "isOptional": true, + "environmentValue": "SHOPIFY_FLAG_APP_CONFIG" + } + ], + "value": "export interface appchannelspecgenerate {\n /**\n * Alias of the Shopify account to use for authentication.\n * @environment SHOPIFY_FLAG_AUTH_ALIAS\n */\n '--auth-alias '?: string\n\n /**\n * The Client ID of your app.\n * @environment SHOPIFY_FLAG_CLIENT_ID\n */\n '--client-id '?: string\n\n /**\n * The name of the app configuration.\n * @environment SHOPIFY_FLAG_APP_CONFIG\n */\n '-c, --config '?: string\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * Overwrite the existing channel spec file if one already exists.\n * @environment SHOPIFY_FLAG_OVERWRITE\n */\n '--overwrite'?: ''\n\n /**\n * The path to your app directory.\n * @environment SHOPIFY_FLAG_PATH\n */\n '--path '?: string\n\n /**\n * Reset all your settings.\n * @environment SHOPIFY_FLAG_RESET\n */\n '--reset'?: ''\n\n /**\n * Print the generated TOML to stdout instead of writing it to a file.\n * @environment SHOPIFY_FLAG_STDOUT\n */\n '--stdout'?: ''\n\n /**\n * Increase the verbosity of the output. May include sensitive data.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}" + } + }, "appconfiglink": { "docs-shopify.dev/commands/interfaces/app-config-link.interface.ts": { "filePath": "docs-shopify.dev/commands/interfaces/app-config-link.interface.ts", diff --git a/packages/app/src/cli/commands/app/channel-spec/generate.ts b/packages/app/src/cli/commands/app/channel-spec/generate.ts new file mode 100644 index 00000000000..01c4e56b842 --- /dev/null +++ b/packages/app/src/cli/commands/app/channel-spec/generate.ts @@ -0,0 +1,52 @@ +import {appFlags} from '../../../flags.js' +import {linkedAppContext} from '../../../services/app-context.js' +import {generateChannelSpec} from '../../../services/channel-spec/generate.js' +import AppLinkedCommand, {AppLinkedCommandOutput} from '../../../utilities/app-linked-command.js' +import {Flags} from '@oclif/core' +import {globalFlags} from '@shopify/cli-kit/node/cli' + +export default class ChannelSpecGenerate extends AppLinkedCommand { + static summary = 'Generate a channel spec TOML file from the Shopify-authored default.' + + static descriptionWithMarkdown = `Generates a deployable \`channel_config\` extension spec from the Shopify-authored default channel specification for your app. + + The generated TOML file contains only public \`channel_config\` fields. Review it, commit it to your app, then deploy it with \`shopify app deploy\`. This command never deploys the spec itself.` + + static description = this.descriptionWithoutMarkdown() + + static flags = { + ...globalFlags, + ...appFlags, + stdout: Flags.boolean({ + description: 'Print the generated TOML to stdout instead of writing it to a file.', + env: 'SHOPIFY_FLAG_STDOUT', + default: false, + }), + overwrite: Flags.boolean({ + description: 'Overwrite the existing channel spec file if one already exists.', + env: 'SHOPIFY_FLAG_OVERWRITE', + default: false, + }), + } + + public async run(): Promise { + const {flags} = await this.parse(ChannelSpecGenerate) + + const {app, remoteApp, developerPlatformClient} = await linkedAppContext({ + directory: flags.path, + clientId: flags['client-id'], + forceRelink: flags.reset, + userProvidedConfigName: flags.config, + }) + + await generateChannelSpec({ + app, + remoteApp, + developerPlatformClient, + stdout: flags.stdout, + overwrite: flags.overwrite, + }) + + return {app} + } +} diff --git a/packages/app/src/cli/index.ts b/packages/app/src/cli/index.ts index fc2d9c42b10..cc9783d8de4 100644 --- a/packages/app/src/cli/index.ts +++ b/packages/app/src/cli/index.ts @@ -1,6 +1,7 @@ import Build from './commands/app/build.js' import BulkCancel from './commands/app/bulk/cancel.js' import BulkStatus from './commands/app/bulk/status.js' +import ChannelSpecGenerate from './commands/app/channel-spec/generate.js' import ConfigLink from './commands/app/config/link.js' import ConfigUse from './commands/app/config/use.js' import ConfigPull from './commands/app/config/pull.js' @@ -45,6 +46,7 @@ export const commands: {[key: string]: typeof AppLinkedCommand | typeof AppUnlin 'app:build': Build, 'app:bulk:cancel': BulkCancel, 'app:bulk:status': BulkStatus, + 'app:channel-spec:generate': ChannelSpecGenerate, 'app:deploy': Deploy, 'app:dev': Dev, 'app:dev:clean': DevClean, diff --git a/packages/app/src/cli/services/channel-spec/fetch.ts b/packages/app/src/cli/services/channel-spec/fetch.ts new file mode 100644 index 00000000000..0c847b82f23 --- /dev/null +++ b/packages/app/src/cli/services/channel-spec/fetch.ts @@ -0,0 +1,100 @@ +import {OrganizationApp} from '../../models/organization.js' +import {DeveloperPlatformClient} from '../../utilities/developer-platform-client.js' +import {appManagementHeaders} from '@shopify/cli-kit/node/api/app-management' +import {appManagementFqdn} from '@shopify/cli-kit/node/context/fqdn' +import {shopifyFetch} from '@shopify/cli-kit/node/http' +import {AbortError} from '@shopify/cli-kit/node/error' + +export interface ChannelSpecExportWarning { + code: string + message: string +} + +export type ChannelSpecExportResult = + | { + success: true + handle: string + filename: string + toml: string + warnings: ChannelSpecExportWarning[] + } + | { + success: false + reason: string + } + +interface FetchChannelSpecExportOptions { + remoteApp: OrganizationApp + developerPlatformClient: DeveloperPlatformClient +} + +/** + * Fetches the partner-safe channel spec export for an app. + * + * The export itself is produced server-side by the Channels-owned exporter, which projects the + * Shopify-authored default channel specification into the public channel_config schema and + * validates it before returning it. The CLI intentionally does not transform or validate the + * TOML locally: the backend response is the deployable artifact. + */ +export async function fetchChannelSpecExport({ + remoteApp, + developerPlatformClient, +}: FetchChannelSpecExportOptions): Promise { + const fqdn = await appManagementFqdn() + const url = `https://${fqdn}/app_management/unstable/organizations/${remoteApp.organizationId}/apps/${remoteApp.id}/channel_spec_export.json` + const token = (await developerPlatformClient.session()).token + + const response = await shopifyFetch(url, { + method: 'GET', + headers: appManagementHeaders(token), + }) + + if (response.status === 404) { + // A 404 is not part of the export contract (failures are 422 with a reason code). It means the + // export endpoint isn't available (not deployed yet), or the app/organization couldn't be found. + throw new AbortError( + 'The channel spec export endpoint is not available for this app.', + 'Confirm the app and organization are correct, and that the channel spec export backend is available.', + ) + } + + let payload: {[key: string]: unknown} + try { + payload = (await response.json()) as {[key: string]: unknown} + } catch { + throw new AbortError(`Failed to fetch the channel spec export: unexpected response (status ${response.status}).`) + } + + if (!response.ok) { + const reason = typeof payload.reason === 'string' ? payload.reason : `http_${response.status}` + return {success: false, reason} + } + + const {handle, filename, toml, warnings} = payload + if (typeof handle !== 'string' || typeof filename !== 'string' || typeof toml !== 'string') { + throw new AbortError('Failed to fetch the channel spec export: the response was missing required fields.') + } + + return { + success: true, + handle, + filename, + toml, + warnings: parseWarnings(warnings), + } +} + +function parseWarnings(warnings: unknown): ChannelSpecExportWarning[] { + if (!Array.isArray(warnings)) return [] + return warnings.flatMap((warning) => { + if ( + warning && + typeof warning === 'object' && + typeof (warning as {code?: unknown}).code === 'string' && + typeof (warning as {message?: unknown}).message === 'string' + ) { + return [warning as ChannelSpecExportWarning] + } + return [] + }) +} diff --git a/packages/app/src/cli/services/channel-spec/generate.test.ts b/packages/app/src/cli/services/channel-spec/generate.test.ts new file mode 100644 index 00000000000..81ab05f963f --- /dev/null +++ b/packages/app/src/cli/services/channel-spec/generate.test.ts @@ -0,0 +1,155 @@ +import {fetchChannelSpecExport} from './fetch.js' +import {generateChannelSpec, CHANNEL_SPEC_DIRECTORY} from './generate.js' +import {AppLinkedInterface} from '../../models/app/app.js' +import {testAppLinked, testDeveloperPlatformClient, testOrganizationApp} from '../../models/app/app.test-data.js' +import {describe, expect, test, vi} from 'vitest' +import {fileExists, inTemporaryDirectory, mkdir, readFile, writeFile} from '@shopify/cli-kit/node/fs' +import {dirname, joinPath} from '@shopify/cli-kit/node/path' +import {mockAndCaptureOutput} from '@shopify/cli-kit/node/testing/output' + +vi.mock('./fetch.js') + +const TOML = 'handle = "example"\nlabel = "Example Channel"\n' + +function successResult(warnings: {code: string; message: string}[] = []) { + return { + success: true as const, + handle: 'example', + filename: 'example.toml', + toml: TOML, + warnings, + } +} + +function testOptions(app: AppLinkedInterface, {stdout = false, overwrite = false} = {}) { + return { + app, + remoteApp: testOrganizationApp(), + developerPlatformClient: testDeveloperPlatformClient(), + stdout, + overwrite, + } +} + +describe('generateChannelSpec', () => { + test('writes the TOML to the channel-config specifications directory', async () => { + await inTemporaryDirectory(async (tmpDir) => { + // Given + vi.mocked(fetchChannelSpecExport).mockResolvedValue(successResult()) + const app = testAppLinked({directory: tmpDir}) + const outputMock = mockAndCaptureOutput() + + // When + await generateChannelSpec(testOptions(app)) + + // Then + const outputPath = joinPath(tmpDir, CHANNEL_SPEC_DIRECTORY, 'example.toml') + await expect(fileExists(outputPath)).resolves.toBe(true) + await expect(readFile(outputPath)).resolves.toEqual(TOML) + expect(outputMock.info()).toContain('Generated a channel spec') + expect(outputMock.info()).toContain('shopify app deploy') + }) + }) + + test('refuses to overwrite an existing spec without --overwrite', async () => { + await inTemporaryDirectory(async (tmpDir) => { + // Given + vi.mocked(fetchChannelSpecExport).mockResolvedValue(successResult()) + const app = testAppLinked({directory: tmpDir}) + const outputPath = joinPath(tmpDir, CHANNEL_SPEC_DIRECTORY, 'example.toml') + await mkdir(dirname(outputPath)) + await writeFile(outputPath, 'existing = true\n') + + // When/Then + await expect(generateChannelSpec(testOptions(app))).rejects.toThrow(/already exists/) + await expect(readFile(outputPath)).resolves.toEqual('existing = true\n') + }) + }) + + test('overwrites an existing spec with --overwrite', async () => { + await inTemporaryDirectory(async (tmpDir) => { + // Given + vi.mocked(fetchChannelSpecExport).mockResolvedValue(successResult()) + const app = testAppLinked({directory: tmpDir}) + const outputPath = joinPath(tmpDir, CHANNEL_SPEC_DIRECTORY, 'example.toml') + await mkdir(dirname(outputPath)) + await writeFile(outputPath, 'existing = true\n') + + // When + await generateChannelSpec(testOptions(app, {overwrite: true})) + + // Then + await expect(readFile(outputPath)).resolves.toEqual(TOML) + }) + }) + + test('prints only the TOML to stdout with --stdout, keeping warnings out-of-band', async () => { + await inTemporaryDirectory(async (tmpDir) => { + // Given + const warning = { + code: 'automatic_product_feed_management', + message: + 'This generated spec enables automatic product feed management. Review the generated configuration before deploying.', + } + vi.mocked(fetchChannelSpecExport).mockResolvedValue(successResult([warning])) + const app = testAppLinked({directory: tmpDir}) + const outputMock = mockAndCaptureOutput() + + // When + await generateChannelSpec(testOptions(app, {stdout: true})) + + // Then + expect(outputMock.output()).toContain(TOML) + expect(outputMock.output()).not.toContain(warning.code) + expect(outputMock.warn()).toContain(warning.message) + await expect(fileExists(joinPath(tmpDir, CHANNEL_SPEC_DIRECTORY, 'example.toml'))).resolves.toBe(false) + }) + }) + + test('renders backend warnings when writing the file', async () => { + await inTemporaryDirectory(async (tmpDir) => { + // Given + const warning = { + code: 'automatic_product_feed_management', + message: + 'This generated spec enables automatic product feed management. Review the generated configuration before deploying.', + } + vi.mocked(fetchChannelSpecExport).mockResolvedValue(successResult([warning])) + const app = testAppLinked({directory: tmpDir}) + const outputMock = mockAndCaptureOutput() + + // When + await generateChannelSpec(testOptions(app)) + + // Then + expect(outputMock.warn()).toContain(warning.message) + await expect(readFile(joinPath(tmpDir, CHANNEL_SPEC_DIRECTORY, 'example.toml'))).resolves.not.toContain( + warning.message, + ) + }) + }) + + test('aborts with partner-facing guidance when no export is available', async () => { + await inTemporaryDirectory(async (tmpDir) => { + // Given + vi.mocked(fetchChannelSpecExport).mockResolvedValue({success: false, reason: 'no_exportable_frozen_record'}) + const app = testAppLinked({directory: tmpDir}) + + // When/Then + await expect(generateChannelSpec(testOptions(app))).rejects.toThrow( + /No deployable channel spec is available for this app yet/, + ) + }) + }) + + test('aborts with the reason code when the backend returns an unknown reason', async () => { + await inTemporaryDirectory(async (tmpDir) => { + // Given + vi.mocked(fetchChannelSpecExport).mockResolvedValue({success: false, reason: 'mystery_reason'}) + const app = testAppLinked({directory: tmpDir}) + + // When/Then + await expect(generateChannelSpec(testOptions(app))).rejects.toThrow(/mystery_reason/) + }) + }) +}) diff --git a/packages/app/src/cli/services/channel-spec/generate.ts b/packages/app/src/cli/services/channel-spec/generate.ts new file mode 100644 index 00000000000..8c251116163 --- /dev/null +++ b/packages/app/src/cli/services/channel-spec/generate.ts @@ -0,0 +1,85 @@ +import {fetchChannelSpecExport, ChannelSpecExportWarning} from './fetch.js' +import {AppLinkedInterface} from '../../models/app/app.js' +import {OrganizationApp} from '../../models/organization.js' +import {DeveloperPlatformClient} from '../../utilities/developer-platform-client.js' +import {AbortError} from '@shopify/cli-kit/node/error' +import {fileExists, mkdir, writeFile} from '@shopify/cli-kit/node/fs' +import {dirname, joinPath, relativePath} from '@shopify/cli-kit/node/path' +import {outputResult, outputWarn} from '@shopify/cli-kit/node/output' +import {renderSuccess, renderWarning} from '@shopify/cli-kit/node/ui' + +export const CHANNEL_SPEC_DIRECTORY = joinPath('extensions', 'channel-config', 'specifications') + +const FAILURE_MESSAGES: {[reason: string]: string} = { + no_exportable_frozen_record: + 'No deployable channel spec is available for this app yet.\n\n' + + "The Shopify-authored default can't currently be exported to the public channel_config schema.", + multiple_exportable_records: + "This app has more than one Shopify-authored channel spec, so a single spec can't be exported automatically.", + not_allowlisted: "This app isn't part of the channel spec export prototype yet.", + contains_no_public_fields: + 'The Shopify-authored default for this app contains no fields that are part of the public channel_config schema.', + invalid_public_schema: + 'The Shopify-authored default for this app could not be projected into a valid public channel_config spec.', +} + +export interface GenerateChannelSpecOptions { + app: AppLinkedInterface + remoteApp: OrganizationApp + developerPlatformClient: DeveloperPlatformClient + stdout: boolean + overwrite: boolean +} + +/** + * Generates a deployable channel_config spec TOML file for the app. + * + * On success the TOML is either printed to stdout (`--stdout`) or written to + * `extensions/channel-config/specifications/.toml` inside the app directory. Warnings + * returned by the backend are rendered out-of-band and are never written into the TOML file. + * This command never deploys; the partner reviews the generated file and runs `shopify app deploy`. + */ +export async function generateChannelSpec(options: GenerateChannelSpecOptions): Promise { + const {app, remoteApp, developerPlatformClient, stdout, overwrite} = options + + const result = await fetchChannelSpecExport({remoteApp, developerPlatformClient}) + + if (!result.success) { + const message = FAILURE_MESSAGES[result.reason] + if (message) throw new AbortError(message) + throw new AbortError(`The channel spec for this app could not be exported (reason: ${result.reason}).`) + } + + if (stdout) { + // Warnings go to stderr so stdout carries only the TOML and stays pipeable. + result.warnings.forEach((warning) => outputWarn(warning.message)) + outputResult(result.toml) + return + } + + const outputPath = joinPath(app.directory, CHANNEL_SPEC_DIRECTORY, result.filename) + if (!overwrite && (await fileExists(outputPath))) { + throw new AbortError( + `A channel spec already exists at ${relativePath(app.directory, outputPath)}.`, + 'Re-run with `--overwrite` to replace it.', + ) + } + + await mkdir(dirname(outputPath)) + await writeFile(outputPath, result.toml) + + result.warnings.forEach((warning) => renderExportWarning(warning)) + + renderSuccess({ + headline: ['Generated a channel spec for', {userInput: remoteApp.title}, {char: '.'}], + body: ['The spec was written to', {filePath: relativePath(app.directory, outputPath)}, {char: '.'}], + nextSteps: [ + 'Review the generated spec before deploying it.', + ['Run', {command: 'shopify app deploy'}, 'to deploy the spec as part of your app.'], + ], + }) +} + +function renderExportWarning(warning: ChannelSpecExportWarning): void { + renderWarning({body: warning.message}) +} diff --git a/packages/cli/README.md b/packages/cli/README.md index 445154f2fa8..89a99724e4b 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -4,6 +4,7 @@ * [`shopify app bulk cancel`](#shopify-app-bulk-cancel) * [`shopify app bulk execute`](#shopify-app-bulk-execute) * [`shopify app bulk status`](#shopify-app-bulk-status) +* [`shopify app channel-spec generate`](#shopify-app-channel-spec-generate) * [`shopify app config link`](#shopify-app-config-link) * [`shopify app config pull`](#shopify-app-config-pull) * [`shopify app config use [config] [flags]`](#shopify-app-config-use-config-flags) @@ -365,6 +366,62 @@ DESCRIPTION Use "`bulk execute`" (https://shopify.dev/docs/api/shopify-cli/app/app-bulk-execute) to start a new bulk operation. ``` +## `shopify app channel-spec generate` + +Generate a channel spec TOML file from the Shopify-authored default. + +``` +USAGE + $ shopify app channel-spec generate [--auth-alias ] [--client-id | -c ] [--no-color] [--overwrite] + [--path ] [--reset | ] [--stdout] [--verbose] + +FLAGS + -c, --config= + The name of the app configuration. + [env: SHOPIFY_FLAG_APP_CONFIG] + + --auth-alias= + Alias of the Shopify account to use for authentication. + [env: SHOPIFY_FLAG_AUTH_ALIAS] + + --client-id= + The Client ID of your app. + [env: SHOPIFY_FLAG_CLIENT_ID] + + --no-color + Disable color output. + [env: SHOPIFY_FLAG_NO_COLOR] + + --overwrite + Overwrite the existing channel spec file if one already exists. + [env: SHOPIFY_FLAG_OVERWRITE] + + --path= + The path to your app directory. + [env: SHOPIFY_FLAG_PATH] + + --reset + Reset all your settings. + [env: SHOPIFY_FLAG_RESET] + + --stdout + Print the generated TOML to stdout instead of writing it to a file. + [env: SHOPIFY_FLAG_STDOUT] + + --verbose + Increase the verbosity of the output. May include sensitive data. + [env: SHOPIFY_FLAG_VERBOSE] + +DESCRIPTION + Generate a channel spec TOML file from the Shopify-authored default. + + Generates a deployable `channel_config` extension spec from the Shopify-authored default channel specification for + your app. + + The generated TOML file contains only public `channel_config` fields. Review it, commit it to your app, then deploy it + with `shopify app deploy`. This command never deploys the spec itself. +``` + ## `shopify app config link` Fetch your app configuration from the Developer Dashboard. diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index 7078a731040..33ef9eeb369 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -462,6 +462,106 @@ "strict": true, "summary": "Check the status of bulk operations." }, + "app:channel-spec:generate": { + "aliases": [ + ], + "args": { + }, + "customPluginName": "@shopify/app", + "description": "Generates a deployable `channel_config` extension spec from the Shopify-authored default channel specification for your app.\n\n The generated TOML file contains only public `channel_config` fields. Review it, commit it to your app, then deploy it with `shopify app deploy`. This command never deploys the spec itself.", + "descriptionWithMarkdown": "Generates a deployable `channel_config` extension spec from the Shopify-authored default channel specification for your app.\n\n The generated TOML file contains only public `channel_config` fields. Review it, commit it to your app, then deploy it with `shopify app deploy`. This command never deploys the spec itself.", + "flags": { + "auth-alias": { + "description": "Alias of the Shopify account to use for authentication.", + "env": "SHOPIFY_FLAG_AUTH_ALIAS", + "hasDynamicHelp": false, + "multiple": false, + "name": "auth-alias", + "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" + }, + "no-color": { + "allowNo": false, + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", + "type": "boolean" + }, + "overwrite": { + "allowNo": false, + "description": "Overwrite the existing channel spec file if one already exists.", + "env": "SHOPIFY_FLAG_OVERWRITE", + "name": "overwrite", + "type": "boolean" + }, + "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" + }, + "stdout": { + "allowNo": false, + "description": "Print the generated TOML to stdout instead of writing it to a file.", + "env": "SHOPIFY_FLAG_STDOUT", + "name": "stdout", + "type": "boolean" + }, + "verbose": { + "allowNo": false, + "description": "Increase the verbosity of the output. May include sensitive data.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", + "type": "boolean" + } + }, + "hasDynamicHelp": false, + "hiddenAliases": [ + ], + "id": "app:channel-spec:generate", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true, + "summary": "Generate a channel spec TOML file from the Shopify-authored default." + }, "app:config:link": { "aliases": [ ],