From 03dbe5c23ac2631844aade26585aaa083cfc22be Mon Sep 17 00:00:00 2001 From: Gonzalo Riestra Date: Mon, 21 Sep 2026 10:06:37 +0200 Subject: [PATCH] Add typed JSON output to app config pull --- .../generated/generated_docs_data_v2.json | 11 +- .../src/cli/commands/app/config/pull.test.ts | 109 ++++++++++++ .../app/src/cli/commands/app/config/pull.ts | 18 +- .../src/cli/services/app/config/pull.test.ts | 50 ++++++ .../app/src/cli/services/app/config/pull.ts | 17 +- .../cli/services/app/config/pull/result.ts | 15 ++ .../src/cli/services/app/config/pull/types.ts | 10 ++ packages/cli/README.md | 163 +++++++++++++++++- packages/cli/oclif.manifest.json | 11 +- .../rules/json-output-command-exceptions.js | 1 - 10 files changed, 382 insertions(+), 23 deletions(-) create mode 100644 packages/app/src/cli/commands/app/config/pull.test.ts create mode 100644 packages/app/src/cli/services/app/config/pull.test.ts create mode 100644 packages/app/src/cli/services/app/config/pull/result.ts create mode 100644 packages/app/src/cli/services/app/config/pull/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 8c3c92e69db..5ea64ce3e34 100644 --- a/docs-shopify.dev/generated/generated_docs_data_v2.json +++ b/docs-shopify.dev/generated/generated_docs_data_v2.json @@ -635,9 +635,18 @@ "description": "The name of the app configuration.", "isOptional": true, "environmentValue": "SHOPIFY_FLAG_APP_CONFIG" + }, + { + "filePath": "docs-shopify.dev/commands/interfaces/app-config-pull.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 appconfigpull {\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 * 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 appconfigpull {\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 * 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}" } }, "appconfiguse": { diff --git a/packages/app/src/cli/commands/app/config/pull.test.ts b/packages/app/src/cli/commands/app/config/pull.test.ts new file mode 100644 index 00000000000..5ac0fe50120 --- /dev/null +++ b/packages/app/src/cli/commands/app/config/pull.test.ts @@ -0,0 +1,109 @@ +import ConfigPull from './pull.js' +import pull from '../../../services/app/config/pull.js' +import {appConfigPullJsonOutputSchema} from '../../../services/app/config/pull/types.js' +import {linkedAppContext} from '../../../services/app-context.js' +import {testAppLinked, testOrganizationApp} from '../../../models/app/app.test-data.js' +import {Config} from '@oclif/core' +import {expect, test, vi} from 'vitest' +import * as context from '@shopify/cli-kit/node/context/local' +import * as ui from '@shopify/cli-kit/node/ui' +import {mockAndCaptureStandardStreams} from '@shopify/cli-kit/node/testing/output' + +vi.mock('../../../services/app/config/pull.js') +vi.mock('../../../services/app-context.js') + +function setup() { + const app = testAppLinked() + const remoteApp = testOrganizationApp() + const result = appConfigPullJsonOutputSchema.validate({ + configFile: app.configPath, + configuration: app.configuration, + app: remoteApp, + }) + vi.mocked(linkedAppContext).mockResolvedValue({app, remoteApp} as Awaited>) + vi.mocked(pull).mockResolvedValue(result) + return {app, remoteApp, result} +} + +test('exposes the schema in help and retains app flags', () => { + expect(ConfigPull.jsonOutputSchema).toBe(appConfigPullJsonOutputSchema) + expect(ConfigPull.description).toContain('AppConfigPullResult') + expect(ConfigPull.flags.json).toBeDefined() + expect(ConfigPull.flags.config).toBeDefined() +}) + +test('writes the real encoded result to stdout', async () => { + const {app, result} = setup() + const command = new ConfigPull(['--json'], await Config.load()) + vi.spyOn(context, 'isUnitTest').mockReturnValue(false) + const streams = mockAndCaptureStandardStreams() + try { + await expect(command.run()).resolves.toEqual({app}) + expect(streams.stdout()).toBe(`${appConfigPullJsonOutputSchema.encode(result)}\n`) + expect(streams.stderr()).toBe('') + } finally { + streams.restore() + } +}) + +test('preserves text presentation and passes the selected configuration to the service', async () => { + const {app, remoteApp} = setup() + const render = vi.spyOn(ui, 'renderSuccess').mockReturnValue(undefined) + await new ConfigPull(['--config', 'staging'], await Config.load()).run() + expect(pull).toHaveBeenCalledWith({ + directory: expect.any(String), + configName: 'staging', + configPath: app.configPath, + configuration: app.configuration, + remoteApp, + }) + expect(render).toHaveBeenCalledWith({ + headline: `Pulled latest configuration for "${app.configuration.name}"`, + body: 'Updated shopify.app.toml with the remote data.', + }) +}) + +test('propagates failures without writing a success document', async () => { + setup() + vi.mocked(pull).mockRejectedValue(new Error('Remote configuration unavailable')) + const command = new ConfigPull(['--json'], await Config.load()) + vi.spyOn(context, 'isUnitTest').mockReturnValue(false) + const streams = mockAndCaptureStandardStreams() + try { + await expect(command.run()).rejects.toThrow('Remote configuration unavailable') + expect(streams.stdout()).toBe('') + } finally { + streams.restore() + } +}) + +test('rejects an invalid file path while all other fields are valid', () => { + expect(() => appConfigPullJsonOutputSchema.validate({...setup().result, configFile: null})).toThrow() +}) + +test('preserves all available public remote fields without exposing credentials', () => { + const remoteApp = testOrganizationApp({ + appType: 'custom', + newApp: false, + grantedScopes: ['read_products'], + developmentStorePreviewEnabled: false, + applicationUrl: 'https://example.com', + redirectUrlWhitelist: [], + requestedAccessScopes: [], + webhookApiVersion: '2026-07', + embedded: false, + posEmbedded: false, + preferencesUrl: '', + gdprWebhooks: {customerDeletionUrl: '', customerDataRequestUrl: '', shopDeletionUrl: ''}, + appProxy: {subPath: 'example', subPathPrefix: 'apps', url: 'https://example.com/proxy'}, + configuration: testAppLinked().configuration, + }) + const publicApp = Object.fromEntries( + Object.entries(remoteApp).filter( + ([key]) => !['apiSecretKeys', 'flags', 'disabledFlags', 'developerPlatformClient'].includes(key), + ), + ) + const result = appConfigPullJsonOutputSchema.validate({...setup().result, app: remoteApp}) + const encoded = JSON.parse(appConfigPullJsonOutputSchema.encode(result)) + expect(encoded.app).toEqual(publicApp) +}) diff --git a/packages/app/src/cli/commands/app/config/pull.ts b/packages/app/src/cli/commands/app/config/pull.ts index aeb7c267601..76c0c5caf62 100644 --- a/packages/app/src/cli/commands/app/config/pull.ts +++ b/packages/app/src/cli/commands/app/config/pull.ts @@ -2,9 +2,9 @@ import {appFlags} from '../../../flags.js' import {linkedAppContext} from '../../../services/app-context.js' import pull from '../../../services/app/config/pull.js' import AppLinkedCommand, {AppLinkedCommandOutput} from '../../../utilities/app-linked-command.js' -import {renderSuccess} from '@shopify/cli-kit/node/ui' -import {globalFlags} from '@shopify/cli-kit/node/cli' -import {basename} from '@shopify/cli-kit/node/path' +import {appConfigPullJsonOutputSchema} from '../../../services/app/config/pull/types.js' +import {renderAppConfigPullResult} from '../../../services/app/config/pull/result.js' +import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli' export default class ConfigPull extends AppLinkedCommand { static summary = 'Refresh an already-linked app configuration without prompts.' @@ -13,10 +13,15 @@ export default class ConfigPull extends AppLinkedCommand { This 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.` + static get jsonOutputSchema() { + return appConfigPullJsonOutputSchema + } + static description = this.descriptionForHelp() static flags = { ...globalFlags, + ...jsonFlag, ...appFlags, } @@ -30,7 +35,7 @@ This command reuses the existing linked app and organization and skips all inter userProvidedConfigName: flags.config, }) - const {configuration, configPath} = await pull({ + const result = await pull({ directory: flags.path, configName: flags.config, configPath: app.configPath, @@ -38,10 +43,7 @@ This command reuses the existing linked app and organization and skips all inter remoteApp, }) - renderSuccess({ - headline: `Pulled latest configuration for "${configuration.name}"`, - body: `Updated ${basename(configPath)} with the remote data.`, - }) + renderAppConfigPullResult(result, flags.json ? 'json' : 'text') return {app} } diff --git a/packages/app/src/cli/services/app/config/pull.test.ts b/packages/app/src/cli/services/app/config/pull.test.ts new file mode 100644 index 00000000000..fb605ee23f8 --- /dev/null +++ b/packages/app/src/cli/services/app/config/pull.test.ts @@ -0,0 +1,50 @@ +import pull from './pull.js' +import {loadLocalAppOptions, overwriteLocalConfigFileWithRemoteAppConfiguration} from './link.js' +import {testAppLinked, testOrganizationApp} from '../../../models/app/app.test-data.js' +import {fetchSpecifications} from '../../generate/fetch-extension-specifications.js' +import {expect, test, vi} from 'vitest' + +vi.mock('./link.js') +vi.mock('../../generate/fetch-extension-specifications.js') + +test('returns the written configuration and public app metadata', async () => { + const app = testAppLinked() + const remoteApp = testOrganizationApp() + vi.mocked(fetchSpecifications).mockResolvedValue([]) + vi.mocked(loadLocalAppOptions).mockResolvedValue({ + state: 'unable-to-load-config', + localAppIdMatchedRemote: false, + scopes: '', + existingBuildOptions: undefined, + existingConfig: undefined, + appDirectory: undefined, + packageManager: 'npm', + }) + vi.mocked(overwriteLocalConfigFileWithRemoteAppConfiguration).mockResolvedValue(app.configuration) + const result = await pull({ + directory: app.directory, + configPath: app.configPath, + configuration: app.configuration, + remoteApp, + }) + expect(result.configFile).toBe(app.configPath) + expect(result.configuration).toEqual(app.configuration) + expect(result.app.apiKey).toBe(remoteApp.apiKey) + expect(result.app).not.toHaveProperty('apiSecretKeys') + expect(overwriteLocalConfigFileWithRemoteAppConfiguration).toHaveBeenCalledWith( + expect.objectContaining({configFileName: 'shopify.app.toml', remoteApp, appDirectory: app.directory}), + ) +}) + +test('rejects an unlinked configuration before fetching remote specifications', async () => { + const app = testAppLinked() + await expect( + pull({ + directory: app.directory, + configPath: app.configPath, + configuration: {...app.configuration, client_id: ''}, + remoteApp: testOrganizationApp(), + }), + ).rejects.toThrow('The selected configuration is not linked to a remote app.') + expect(fetchSpecifications).not.toHaveBeenCalled() +}) diff --git a/packages/app/src/cli/services/app/config/pull.ts b/packages/app/src/cli/services/app/config/pull.ts index 67c7155265b..5d2eda406cf 100644 --- a/packages/app/src/cli/services/app/config/pull.ts +++ b/packages/app/src/cli/services/app/config/pull.ts @@ -1,5 +1,4 @@ -// packages/app/src/cli/services/app/config/pull.ts - +import {appConfigPullJsonOutputSchema, type AppConfigPullResult} from './pull/types.js' import {LinkOptions, loadLocalAppOptions, overwriteLocalConfigFileWithRemoteAppConfiguration} from './link.js' import {CurrentAppConfiguration} from '../../../models/app/app.js' import {OrganizationApp} from '../../../models/organization.js' @@ -18,16 +17,10 @@ interface PullOptions { remoteApp: OrganizationApp } -interface PullOutput { - configPath: string - configuration: CurrentAppConfiguration - remoteApp: OrganizationApp -} - /** * Refresh an already-linked app configuration without prompting for org/app. */ -export default async function pull(options: PullOptions): Promise { +export default async function pull(options: PullOptions): Promise { const {directory, configName, configPath, configuration, remoteApp} = options if (!configuration.client_id) { @@ -68,5 +61,9 @@ export default async function pull(options: PullOptions): Promise { localAppOptions, }) - return {configPath, configuration: mergedConfiguration, remoteApp} + return appConfigPullJsonOutputSchema.validate({ + configFile: configPath, + configuration: mergedConfiguration, + app: remoteApp, + }) } diff --git a/packages/app/src/cli/services/app/config/pull/result.ts b/packages/app/src/cli/services/app/config/pull/result.ts new file mode 100644 index 00000000000..7d4d0fc71d1 --- /dev/null +++ b/packages/app/src/cli/services/app/config/pull/result.ts @@ -0,0 +1,15 @@ +import {appConfigPullJsonOutputSchema, type AppConfigPullResult} from './types.js' +import {renderSuccess} from '@shopify/cli-kit/node/ui' +import {outputResult} from '@shopify/cli-kit/node/output' +import {basename} from '@shopify/cli-kit/node/path' + +export function renderAppConfigPullResult(result: AppConfigPullResult, format: 'json' | 'text'): void { + if (format === 'json') { + outputResult(appConfigPullJsonOutputSchema.encode(result)) + return + } + renderSuccess({ + headline: `Pulled latest configuration for "${result.configuration.name}"`, + body: `Updated ${basename(result.configFile)} with the remote data.`, + }) +} diff --git a/packages/app/src/cli/services/app/config/pull/types.ts b/packages/app/src/cli/services/app/config/pull/types.ts new file mode 100644 index 00000000000..ce01281e2b6 --- /dev/null +++ b/packages/app/src/cli/services/app/config/pull/types.ts @@ -0,0 +1,10 @@ +import {appConfigLinkJsonOutputSchema} from '../link/types.js' +import {defineJsonOutputSchema, type InferJsonOutputSchema} from '@shopify/cli-kit/node/json-output-schema' + +export const appConfigPullJsonOutputSchema = defineJsonOutputSchema({ + name: 'AppConfigPullResult', + schema: appConfigLinkJsonOutputSchema.schema, + definitions: appConfigLinkJsonOutputSchema.definitions, +}) + +export type AppConfigPullResult = InferJsonOutputSchema diff --git a/packages/cli/README.md b/packages/cli/README.md index d3269940464..a3d03d26ed4 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -609,14 +609,18 @@ Refresh an already-linked app configuration without prompts. ``` USAGE - $ shopify app config pull [--auth-alias ] [--client-id | -c ] [--json-schema] [--no-color] - [--path ] [--reset | ] [--verbose] + $ shopify app config pull [--auth-alias ] [--client-id | -c ] [-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] @@ -652,6 +656,161 @@ DESCRIPTION This 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. + + Output from `--json` conforms to the `AppConfigPullResult` 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": "AppConfigPullResult", + "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 use [config] [flags]` diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index 6bcb99e73e4..6a7a9dc9410 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -632,7 +632,7 @@ "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.", + "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.\n\nOutput from `--json` conforms to the `AppConfigPullResult` 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\": \"AppConfigPullResult\",\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 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": { "auth-alias": { @@ -665,6 +665,15 @@ "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" + }, "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 e22ef5fb339..a048affd518 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/pull.ts', 'packages/app/src/cli/commands/app/config/use.ts', 'packages/app/src/cli/commands/app/deploy.ts', 'packages/app/src/cli/commands/app/dev/clean.ts',