From b74e72ee3db10eb4dcc9a23de0453592775ba7e1 Mon Sep 17 00:00:00 2001 From: Gonzalo Riestra Date: Mon, 21 Sep 2026 10:04:16 +0200 Subject: [PATCH] Add typed JSON output to app config link --- .../generated/generated_docs_data_v2.json | 11 +- .../src/cli/commands/app/config/link.test.ts | 100 ++++++++--- .../app/src/cli/commands/app/config/link.ts | 17 +- .../app/src/cli/services/app/config/link.ts | 50 +++--- .../services/app/config/link/result.test.ts | 60 +++++++ .../cli/services/app/config/link/result.ts | 41 +++++ .../src/cli/services/app/config/link/types.ts | 46 +++++ packages/cli/README.md | 162 +++++++++++++++++- packages/cli/oclif.manifest.json | 11 +- .../rules/json-output-command-exceptions.js | 1 - 10 files changed, 434 insertions(+), 65 deletions(-) create mode 100644 packages/app/src/cli/services/app/config/link/result.test.ts create mode 100644 packages/app/src/cli/services/app/config/link/result.ts create mode 100644 packages/app/src/cli/services/app/config/link/types.ts diff --git a/docs-shopify.dev/generated/generated_docs_data_v2.json b/docs-shopify.dev/generated/generated_docs_data_v2.json index 3f355f86057..8c3c92e69db 100644 --- a/docs-shopify.dev/generated/generated_docs_data_v2.json +++ b/docs-shopify.dev/generated/generated_docs_data_v2.json @@ -543,9 +543,18 @@ "description": "The name of the app configuration.", "isOptional": true, "environmentValue": "SHOPIFY_FLAG_APP_CONFIG" + }, + { + "filePath": "docs-shopify.dev/commands/interfaces/app-config-link.interface.ts", + "syntaxKind": "PropertySignature", + "name": "-j, --json", + "value": "''", + "description": "Output the result as JSON. Automatically disables color output.", + "isOptional": true, + "environmentValue": "SHOPIFY_FLAG_JSON" } ], - "value": "export interface appconfiglink {\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. Required if non interactive.\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 name of the app configuration file to create or overwrite.\n * @environment SHOPIFY_FLAG_APP_CONFIG_FILE_NAME\n */\n '--file-name '?: string\n\n /**\n * Overwrite an existing configuration file without prompting.\n * @environment SHOPIFY_FLAG_FORCE\n */\n '--force'?: ''\n\n /**\n * Print the command's JSON schemas.\n * @environment SHOPIFY_FLAG_JSON_SCHEMA\n */\n '--json-schema'?: ''\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 * Increase the verbosity of the output. May include sensitive data.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}" + "value": "export interface appconfiglink {\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. Required if non interactive.\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 name of the app configuration file to create or overwrite.\n * @environment SHOPIFY_FLAG_APP_CONFIG_FILE_NAME\n */\n '--file-name '?: string\n\n /**\n * Overwrite an existing configuration file without prompting.\n * @environment SHOPIFY_FLAG_FORCE\n */\n '--force'?: ''\n\n /**\n * Output the result as JSON. Automatically disables color output.\n * @environment SHOPIFY_FLAG_JSON\n */\n '-j, --json'?: ''\n\n /**\n * Print the command's JSON schemas.\n * @environment SHOPIFY_FLAG_JSON_SCHEMA\n */\n '--json-schema'?: ''\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 * Increase the verbosity of the output. May include sensitive data.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}" } }, "appconfigpull": { diff --git a/packages/app/src/cli/commands/app/config/link.test.ts b/packages/app/src/cli/commands/app/config/link.test.ts index d1c976be5fd..230eac2cd82 100644 --- a/packages/app/src/cli/commands/app/config/link.test.ts +++ b/packages/app/src/cli/commands/app/config/link.test.ts @@ -1,7 +1,11 @@ import ConfigLink from './link.js' -import link from '../../../services/app/config/link.js' +import {linkAppConfiguration as link} from '../../../services/app/config/link.js' +import {appConfigLinkJsonOutputSchema} from '../../../services/app/config/link/types.js' import {linkedAppContext} from '../../../services/app-context.js' import {testAppLinked, testOrganizationApp} from '../../../models/app/app.test-data.js' +import {mockAndCaptureStandardStreams} from '@shopify/cli-kit/node/testing/output' +import * as context from '@shopify/cli-kit/node/context/local' +import {Config} from '@oclif/core' import {inTemporaryDirectory} from '@shopify/cli-kit/node/fs' import {terminalSupportsPrompting} from '@shopify/cli-kit/node/system' import {beforeEach, describe, expect, test, vi} from 'vitest' @@ -17,36 +21,45 @@ describe('app config link command', () => { vi.mocked(terminalSupportsPrompting).mockReturnValue(true) }) - test('accepts --client-id with --file-name to link a specific app to a specific config file', async () => { - await inTemporaryDirectory(async (tmp) => { - const app = testAppLinked() - vi.mocked(link).mockResolvedValue({ - remoteApp: testOrganizationApp(), - configFileName: 'shopify.app.staging.toml', - configuration: app.configuration, - }) - vi.mocked(linkedAppContext).mockResolvedValue({app} as Awaited>) + test.each([{outputFlags: []}, {outputFlags: ['--json']}])( + 'accepts explicit app and file selection with output flags %j', + async ({outputFlags}) => { + await inTemporaryDirectory(async (tmp) => { + const app = testAppLinked() + vi.mocked(link).mockResolvedValue({ + remoteApp: testOrganizationApp(), + configFileName: 'shopify.app.staging.toml', + configuration: app.configuration, + packageManager: 'npm', + result: appConfigLinkJsonOutputSchema.validate({ + configFile: '/app/shopify.app.toml', + configuration: app.configuration, + app: testOrganizationApp(), + }), + }) + vi.mocked(linkedAppContext).mockResolvedValue({app} as Awaited>) - await ConfigLink.run( - ['--path', tmp, '--client-id', 'api-key', '--file-name', 'staging', '--force'], - import.meta.url, - ) + await ConfigLink.run( + ['--path', tmp, '--client-id', 'api-key', '--file-name', 'staging', '--force', ...outputFlags], + import.meta.url, + ) - expect(link).toHaveBeenCalledWith({ - directory: tmp, - apiKey: 'api-key', - configName: undefined, - fileName: 'staging', - force: true, + expect(link).toHaveBeenCalledWith({ + directory: tmp, + apiKey: 'api-key', + configName: undefined, + fileName: 'staging', + force: true, + }) + expect(linkedAppContext).toHaveBeenCalledWith({ + directory: tmp, + clientId: undefined, + forceRelink: false, + userProvidedConfigName: 'shopify.app.staging.toml', + }) }) - expect(linkedAppContext).toHaveBeenCalledWith({ - directory: tmp, - clientId: undefined, - forceRelink: false, - userProvidedConfigName: 'shopify.app.staging.toml', - }) - }) - }) + }, + ) test('accepts --config without requiring --file-name when --force is not passed', async () => { await inTemporaryDirectory(async (tmp) => { @@ -55,6 +68,12 @@ describe('app config link command', () => { remoteApp: testOrganizationApp(), configFileName: 'shopify.app.secondary.toml', configuration: app.configuration, + packageManager: 'npm', + result: appConfigLinkJsonOutputSchema.validate({ + configFile: '/app/shopify.app.toml', + configuration: app.configuration, + app: testOrganizationApp(), + }), }) vi.mocked(linkedAppContext).mockResolvedValue({app} as Awaited>) @@ -84,3 +103,28 @@ describe('app config link command', () => { }) }) }) + +test('does not write a JSON result if final app loading fails after linking', async () => { + const app = testAppLinked() + vi.mocked(link).mockResolvedValue({ + remoteApp: testOrganizationApp(), + configuration: app.configuration, + configFileName: 'shopify.app.toml', + packageManager: 'npm', + result: appConfigLinkJsonOutputSchema.validate({ + configFile: app.configPath, + configuration: app.configuration, + app: testOrganizationApp(), + }), + }) + vi.mocked(linkedAppContext).mockRejectedValue(new Error('App loading failed')) + const command = new ConfigLink(['--json', '--client-id', 'key'], await Config.load()) + vi.spyOn(context, 'isUnitTest').mockReturnValue(false) + const streams = mockAndCaptureStandardStreams() + try { + await expect(command.run()).rejects.toThrow('App loading failed') + expect(streams.stdout()).toBe('') + } finally { + streams.restore() + } +}) diff --git a/packages/app/src/cli/commands/app/config/link.ts b/packages/app/src/cli/commands/app/config/link.ts index ded25562bc5..56939fc9a14 100644 --- a/packages/app/src/cli/commands/app/config/link.ts +++ b/packages/app/src/cli/commands/app/config/link.ts @@ -1,9 +1,11 @@ import {appFlags} from '../../../flags.js' import {linkedAppContext} from '../../../services/app-context.js' -import link, {LinkOptions} from '../../../services/app/config/link.js' +import {linkAppConfiguration, LinkOptions} from '../../../services/app/config/link.js' import AppLinkedCommand, {AppLinkedCommandOutput} from '../../../utilities/app-linked-command.js' +import {appConfigLinkJsonOutputSchema} from '../../../services/app/config/link/types.js' +import {renderAppConfigLinkResult} from '../../../services/app/config/link/result.js' import {Flags} from '@oclif/core' -import {globalFlags, requiredIfNonInteractive} from '@shopify/cli-kit/node/cli' +import {globalFlags, jsonFlag, requiredIfNonInteractive} from '@shopify/cli-kit/node/cli' export default class ConfigLink extends AppLinkedCommand { static summary = 'Fetch your app configuration from the Developer Dashboard.' @@ -13,10 +15,15 @@ export default class ConfigLink extends AppLinkedCommand { 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. ` + static get jsonOutputSchema() { + return appConfigLinkJsonOutputSchema + } + static description = this.descriptionForHelp() static flags = { ...globalFlags, + ...jsonFlag, ...appFlags, 'organization-id': Flags.string({ hidden: true, @@ -50,7 +57,8 @@ export default class ConfigLink extends AppLinkedCommand { force: flags.force ?? false, } - const result = await link(options) + const result = await linkAppConfiguration(options) + if (!flags.json) renderAppConfigLinkResult(result.result, result.packageManager, 'text') const {app} = await linkedAppContext({ directory: flags.path, @@ -59,6 +67,9 @@ export default class ConfigLink extends AppLinkedCommand { userProvidedConfigName: result.configFileName, }) + // JSON must wait until all command work succeeds, so a later failure cannot emit a second document. + if (flags.json) renderAppConfigLinkResult(result.result, result.packageManager, 'json') + return {app} } } diff --git a/packages/app/src/cli/services/app/config/link.ts b/packages/app/src/cli/services/app/config/link.ts index 92d80fa90e1..445e5f42258 100644 --- a/packages/app/src/cli/services/app/config/link.ts +++ b/packages/app/src/cli/services/app/config/link.ts @@ -1,3 +1,5 @@ +import {appConfigLinkJsonOutputSchema, type AppConfigLinkResult} from './link/types.js' +import {renderAppConfigLinkResult} from './link/result.js' import {setCurrentConfigPreference} from './use.js' import {AppConfiguration, CurrentAppConfiguration, CliBuildPreferences, getAppScopes} from '../../../models/app/app.js' import {OrganizationApp} from '../../../models/organization.js' @@ -22,8 +24,6 @@ import {fetchSpecifications} from '../../generate/fetch-extension-specifications import {AppConfigurationUsedByCli} from '../../../models/extensions/specifications/types/app_config.js' import {getTomls} from '../../../utilities/app/config/getTomls.js' import {loadLocalExtensionsSpecifications} from '../../../models/extensions/load-specifications.js' -import {renderSuccess} from '@shopify/cli-kit/node/ui' -import {formatPackageManagerCommand} from '@shopify/cli-kit/node/output' import {deepMergeObjects, isEmpty} from '@shopify/cli-kit/common/object' import {fileExists} from '@shopify/cli-kit/node/fs' import {joinPath} from '@shopify/cli-kit/node/path' @@ -60,6 +60,14 @@ interface LinkOutput { * @returns The final app configuration object that was written to the filesystem */ export default async function link(options: LinkOptions, shouldRenderSuccess = true): Promise { + const output = await linkAppConfiguration(options) + if (shouldRenderSuccess) renderAppConfigLinkResult(output.result, output.packageManager, 'text') + return output +} + +export async function linkAppConfiguration( + options: LinkOptions, +): Promise { // First, select (or create, if the user chooses to) a remote app to link to const {remoteApp, appDirectory, developerPlatformClient} = await selectOrCreateRemoteAppToLinkTo(options) @@ -87,11 +95,17 @@ export default async function link(options: LinkOptions, shouldRenderSuccess = t localAppOptions, }) - if (shouldRenderSuccess) { - renderSuccessMessage(configFileName, mergedAppConfiguration.name, localAppOptions.packageManager) + return { + remoteApp, + configFileName, + configuration: mergedAppConfiguration, + packageManager: localAppOptions.packageManager, + result: appConfigLinkJsonOutputSchema.validate({ + configFile: joinPath(appDirectory, configFileName), + configuration: mergedAppConfiguration, + app: remoteApp, + }), } - - return {remoteApp, configFileName, configuration: mergedAppConfiguration} } function abortIfLinkPromptCannotRun(missingFlags: string[]) { @@ -431,30 +445,6 @@ function buildOptionsForGeneratedConfigFile(options: { } } -function renderSuccessMessage(configFileName: string, appName: string, packageManager: PackageManager) { - renderSuccess({ - headline: `${configFileName} is now linked to "${appName}" on Shopify`, - body: `Using ${configFileName} as your default config.`, - nextSteps: [ - [`Make updates to ${configFileName} in your local project`], - [ - 'To upload your config, run', - { - command: formatPackageManagerCommand(packageManager, 'shopify app deploy'), - }, - ], - ], - reference: [ - { - link: { - label: 'App configuration', - url: 'https://shopify.dev/docs/apps/tools/cli/configuration', - }, - }, - ], - }) -} - /** * Given a remote app, and some local app configuration, build a top-level app configuration object to use locally. * diff --git a/packages/app/src/cli/services/app/config/link/result.test.ts b/packages/app/src/cli/services/app/config/link/result.test.ts new file mode 100644 index 00000000000..de3d610f7e9 --- /dev/null +++ b/packages/app/src/cli/services/app/config/link/result.test.ts @@ -0,0 +1,60 @@ +import {appConfigLinkJsonOutputSchema} from './types.js' +import {renderAppConfigLinkResult} from './result.js' +import ConfigLink from '../../../../commands/app/config/link.js' +import {testOrganizationApp} from '../../../../models/app/app.test-data.js' +import {describe, expect, test, vi} from 'vitest' +import {mockAndCaptureStandardStreams} from '@shopify/cli-kit/node/testing/output' +import * as context from '@shopify/cli-kit/node/context/local' +import {runWithCommandEventsForCommand} from '@shopify/cli-kit/node/command-events' +import {outputInfo} from '@shopify/cli-kit/node/output' + +function result() { + return appConfigLinkJsonOutputSchema.validate({ + configFile: '/app/shopify.app.toml', + configuration: {client_id: 'key', name: 'Example', embedded: false, custom_module: {enabled: true}}, + app: testOrganizationApp({apiKey: 'key', developmentStorePreviewEnabled: false}), + }) +} + +describe('config link result', () => { + test('retains public configuration and app fields while excluding credentials and runtime state', () => { + const value = result() + const encoded = JSON.parse(appConfigLinkJsonOutputSchema.encode(value)) + expect(encoded.configuration).toEqual({ + client_id: 'key', + name: 'Example', + embedded: false, + custom_module: {enabled: true}, + }) + expect(encoded.app).toMatchObject({apiKey: 'key', developmentStorePreviewEnabled: false, grantedScopes: []}) + expect(encoded.app).not.toHaveProperty('apiSecretKeys') + expect(encoded.app).not.toHaveProperty('developerPlatformClient') + expect(encoded.app).not.toHaveProperty('flags') + expect(encoded.app).not.toHaveProperty('appType') + }) + + test('rejects a missing configuration client ID', () => { + expect(() => appConfigLinkJsonOutputSchema.validate({...result(), configuration: {name: 'Example'}})).toThrow() + }) + + test('exposes the schema and JSON flag', () => { + expect(ConfigLink.jsonOutputSchema).toBe(appConfigLinkJsonOutputSchema) + expect(ConfigLink.flags.json).toBeDefined() + expect(ConfigLink.description).toContain('AppConfigLinkResult') + }) + + test('writes one encoded result to stdout and diagnostics to stderr', () => { + vi.spyOn(context, 'isUnitTest').mockReturnValue(false) + const streams = mockAndCaptureStandardStreams() + try { + runWithCommandEventsForCommand(['--json'], () => { + outputInfo('Configuration fetched') + renderAppConfigLinkResult(result(), 'npm', 'json') + }) + expect(streams.stdout()).toBe(`${appConfigLinkJsonOutputSchema.encode(result())}\n`) + expect(JSON.parse(streams.stderr())).toMatchObject({type: 'diagnostic', message: 'Configuration fetched'}) + } finally { + streams.restore() + } + }) +}) diff --git a/packages/app/src/cli/services/app/config/link/result.ts b/packages/app/src/cli/services/app/config/link/result.ts new file mode 100644 index 00000000000..2c0b660c6bb --- /dev/null +++ b/packages/app/src/cli/services/app/config/link/result.ts @@ -0,0 +1,41 @@ +import {appConfigLinkJsonOutputSchema, type AppConfigLinkResult} from './types.js' +import {renderSuccess} from '@shopify/cli-kit/node/ui' +import {formatPackageManagerCommand, outputResult} from '@shopify/cli-kit/node/output' +import {basename} from '@shopify/cli-kit/node/path' +import {PackageManager} from '@shopify/cli-kit/node/node-package-manager' + +export function renderAppConfigLinkResult( + result: AppConfigLinkResult, + packageManager: PackageManager, + format: 'json' | 'text', +): void { + if (format === 'json') { + outputResult(appConfigLinkJsonOutputSchema.encode(result)) + return + } + renderSuccessMessage(basename(result.configFile), String(result.configuration.name), packageManager) +} + +function renderSuccessMessage(configFileName: string, appName: string, packageManager: PackageManager) { + renderSuccess({ + headline: `${configFileName} is now linked to "${appName}" on Shopify`, + body: `Using ${configFileName} as your default config.`, + nextSteps: [ + [`Make updates to ${configFileName} in your local project`], + [ + 'To upload your config, run', + { + command: formatPackageManagerCommand(packageManager, 'shopify app deploy'), + }, + ], + ], + reference: [ + { + link: { + label: 'App configuration', + url: 'https://shopify.dev/docs/apps/tools/cli/configuration', + }, + }, + ], + }) +} diff --git a/packages/app/src/cli/services/app/config/link/types.ts b/packages/app/src/cli/services/app/config/link/types.ts new file mode 100644 index 00000000000..32d9ff8d488 --- /dev/null +++ b/packages/app/src/cli/services/app/config/link/types.ts @@ -0,0 +1,46 @@ +import {defineJsonOutputSchema, type InferJsonOutputSchema} from '@shopify/cli-kit/node/json-output-schema' +import {zod} from '@shopify/cli-kit/node/schema' + +const appProxySchema = zod.object({subPath: zod.string(), subPathPrefix: zod.string(), url: zod.string()}) +const privacyWebhooksSchema = zod.object({ + customerDeletionUrl: zod.string().optional(), + customerDataRequestUrl: zod.string().optional(), + shopDeletionUrl: zod.string().optional(), +}) +const remoteAppSchema = zod.object({ + id: zod.string(), + title: zod.string(), + apiKey: zod.string(), + organizationId: zod.string(), + appType: zod.string().optional(), + newApp: zod.boolean().optional(), + grantedScopes: zod.array(zod.string()), + developmentStorePreviewEnabled: zod.boolean().optional(), + applicationUrl: zod.string().optional(), + redirectUrlWhitelist: zod.array(zod.string()).optional(), + requestedAccessScopes: zod.array(zod.string()).optional(), + webhookApiVersion: zod.string().optional(), + embedded: zod.boolean().optional(), + posEmbedded: zod.boolean().optional(), + preferencesUrl: zod.string().optional(), + gdprWebhooks: privacyWebhooksSchema.optional(), + appProxy: appProxySchema.optional(), + configuration: zod.record(zod.unknown()).optional(), +}) + +// Configuration modules are supplied by the platform. Keep their public values, +// including module sections unknown to this CLI version. +const configurationSchema = zod.object({client_id: zod.string()}).passthrough() + +export const appConfigLinkJsonOutputSchema = defineJsonOutputSchema({ + name: 'AppConfigLinkResult', + schema: zod.object({configFile: zod.string(), configuration: configurationSchema, app: remoteAppSchema}), + definitions: { + AppConfiguration: configurationSchema, + LinkedApp: remoteAppSchema, + PrivacyWebhooks: privacyWebhooksSchema, + AppProxy: appProxySchema, + }, +}) + +export type AppConfigLinkResult = InferJsonOutputSchema diff --git a/packages/cli/README.md b/packages/cli/README.md index 17960ea28ef..d3269940464 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -390,13 +390,17 @@ Fetch your app configuration from the Developer Dashboard. ``` USAGE $ shopify app config link [--auth-alias ] [--client-id | -c ] [--force [--file-name | - ]] [--json-schema] [--no-color] [--path ] [--reset | ] [--verbose] + ]] [-j] [--json-schema] [--no-color] [--path ] [--reset | ] [--verbose] FLAGS -c, --config= The name of the app configuration. [env: SHOPIFY_FLAG_APP_CONFIG] + -j, --json + Output the result as JSON. Automatically disables color output. + [env: SHOPIFY_FLAG_JSON] + --auth-alias= Alias of the Shopify account to use for authentication. [env: SHOPIFY_FLAG_AUTH_ALIAS] @@ -441,6 +445,162 @@ DESCRIPTION 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. + + + Output from `--json` conforms to the `AppConfigLinkResult` schema. + + Use `--json-schema` to print the result, error, and event schemas. + + ```json + { + "type": "object", + "properties": { + "configFile": { + "type": "string" + }, + "configuration": { + "$ref": "#/definitions/AppConfiguration" + }, + "app": { + "$ref": "#/definitions/LinkedApp" + } + }, + "required": [ + "configFile", + "configuration", + "app" + ], + "additionalProperties": false, + "title": "AppConfigLinkResult", + "definitions": { + "AppConfiguration": { + "type": "object", + "properties": { + "client_id": { + "type": "string" + } + }, + "required": [ + "client_id" + ], + "additionalProperties": true + }, + "LinkedApp": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "title": { + "type": "string" + }, + "apiKey": { + "type": "string" + }, + "organizationId": { + "type": "string" + }, + "appType": { + "type": "string" + }, + "newApp": { + "type": "boolean" + }, + "grantedScopes": { + "type": "array", + "items": { + "type": "string" + } + }, + "developmentStorePreviewEnabled": { + "type": "boolean" + }, + "applicationUrl": { + "type": "string" + }, + "redirectUrlWhitelist": { + "type": "array", + "items": { + "type": "string" + } + }, + "requestedAccessScopes": { + "type": "array", + "items": { + "type": "string" + } + }, + "webhookApiVersion": { + "type": "string" + }, + "embedded": { + "type": "boolean" + }, + "posEmbedded": { + "type": "boolean" + }, + "preferencesUrl": { + "type": "string" + }, + "gdprWebhooks": { + "$ref": "#/definitions/PrivacyWebhooks" + }, + "appProxy": { + "$ref": "#/definitions/AppProxy" + }, + "configuration": { + "type": "object", + "additionalProperties": {} + } + }, + "required": [ + "id", + "title", + "apiKey", + "organizationId", + "grantedScopes" + ], + "additionalProperties": false + }, + "PrivacyWebhooks": { + "type": "object", + "properties": { + "customerDeletionUrl": { + "type": "string" + }, + "customerDataRequestUrl": { + "type": "string" + }, + "shopDeletionUrl": { + "type": "string" + } + }, + "additionalProperties": false + }, + "AppProxy": { + "type": "object", + "properties": { + "subPath": { + "type": "string" + }, + "subPathPrefix": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "required": [ + "subPath", + "subPathPrefix", + "url" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + } + ``` ``` ## `shopify app config pull` diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index 1720d52a067..6bcb99e73e4 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -496,7 +496,7 @@ "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 ", + "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 \n\nOutput from `--json` conforms to the `AppConfigLinkResult` schema.\n\nUse `--json-schema` to print the result, error, and event schemas.\n\n```json\n{\n \"type\": \"object\",\n \"properties\": {\n \"configFile\": {\n \"type\": \"string\"\n },\n \"configuration\": {\n \"$ref\": \"#/definitions/AppConfiguration\"\n },\n \"app\": {\n \"$ref\": \"#/definitions/LinkedApp\"\n }\n },\n \"required\": [\n \"configFile\",\n \"configuration\",\n \"app\"\n ],\n \"additionalProperties\": false,\n \"title\": \"AppConfigLinkResult\",\n \"definitions\": {\n \"AppConfiguration\": {\n \"type\": \"object\",\n \"properties\": {\n \"client_id\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"client_id\"\n ],\n \"additionalProperties\": true\n },\n \"LinkedApp\": {\n \"type\": \"object\",\n \"properties\": {\n \"id\": {\n \"type\": \"string\"\n },\n \"title\": {\n \"type\": \"string\"\n },\n \"apiKey\": {\n \"type\": \"string\"\n },\n \"organizationId\": {\n \"type\": \"string\"\n },\n \"appType\": {\n \"type\": \"string\"\n },\n \"newApp\": {\n \"type\": \"boolean\"\n },\n \"grantedScopes\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"developmentStorePreviewEnabled\": {\n \"type\": \"boolean\"\n },\n \"applicationUrl\": {\n \"type\": \"string\"\n },\n \"redirectUrlWhitelist\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"requestedAccessScopes\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"webhookApiVersion\": {\n \"type\": \"string\"\n },\n \"embedded\": {\n \"type\": \"boolean\"\n },\n \"posEmbedded\": {\n \"type\": \"boolean\"\n },\n \"preferencesUrl\": {\n \"type\": \"string\"\n },\n \"gdprWebhooks\": {\n \"$ref\": \"#/definitions/PrivacyWebhooks\"\n },\n \"appProxy\": {\n \"$ref\": \"#/definitions/AppProxy\"\n },\n \"configuration\": {\n \"type\": \"object\",\n \"additionalProperties\": {}\n }\n },\n \"required\": [\n \"id\",\n \"title\",\n \"apiKey\",\n \"organizationId\",\n \"grantedScopes\"\n ],\n \"additionalProperties\": false\n },\n \"PrivacyWebhooks\": {\n \"type\": \"object\",\n \"properties\": {\n \"customerDeletionUrl\": {\n \"type\": \"string\"\n },\n \"customerDataRequestUrl\": {\n \"type\": \"string\"\n },\n \"shopDeletionUrl\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"AppProxy\": {\n \"type\": \"object\",\n \"properties\": {\n \"subPath\": {\n \"type\": \"string\"\n },\n \"subPathPrefix\": {\n \"type\": \"string\"\n },\n \"url\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"subPath\",\n \"subPathPrefix\",\n \"url\"\n ],\n \"additionalProperties\": false\n }\n },\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n}\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 ", "flags": { "auth-alias": { @@ -552,6 +552,15 @@ "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", + "type": "boolean" + }, "json-schema": { "allowNo": false, "description": "Print the command's JSON schemas.", diff --git a/packages/eslint-plugin-cli/rules/json-output-command-exceptions.js b/packages/eslint-plugin-cli/rules/json-output-command-exceptions.js index 7a670bcbce1..e22ef5fb339 100644 --- a/packages/eslint-plugin-cli/rules/json-output-command-exceptions.js +++ b/packages/eslint-plugin-cli/rules/json-output-command-exceptions.js @@ -6,7 +6,6 @@ const commandExceptions = [ 'packages/app/src/cli/commands/app/bulk/cancel.ts', 'packages/app/src/cli/commands/app/bulk/execute.ts', 'packages/app/src/cli/commands/app/bulk/status.ts', - 'packages/app/src/cli/commands/app/config/link.ts', 'packages/app/src/cli/commands/app/config/pull.ts', 'packages/app/src/cli/commands/app/config/use.ts', 'packages/app/src/cli/commands/app/deploy.ts',