diff --git a/.changeset/typed-app-env-show.md b/.changeset/typed-app-env-show.md new file mode 100644 index 00000000000..ec7f07a1cbd --- /dev/null +++ b/.changeset/typed-app-env-show.md @@ -0,0 +1,4 @@ +--- +'@shopify/cli': minor +--- +Add typed JSON output to app env show. diff --git a/docs-shopify.dev/generated/generated_docs_data_v2.json b/docs-shopify.dev/generated/generated_docs_data_v2.json index ff8e3489503..6c5693a3992 100644 --- a/docs-shopify.dev/generated/generated_docs_data_v2.json +++ b/docs-shopify.dev/generated/generated_docs_data_v2.json @@ -1405,9 +1405,18 @@ "description": "The name of the app configuration.", "isOptional": true, "environmentValue": "SHOPIFY_FLAG_APP_CONFIG" + }, + { + "filePath": "docs-shopify.dev/commands/interfaces/app-env-show.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 appenvshow {\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 appenvshow {\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}" } }, "appexecute": { diff --git a/packages/app/src/cli/commands/app/env/show.test.ts b/packages/app/src/cli/commands/app/env/show.test.ts new file mode 100644 index 00000000000..a4748de931d --- /dev/null +++ b/packages/app/src/cli/commands/app/env/show.test.ts @@ -0,0 +1,77 @@ +import EnvShow from './show.js' +import {linkedAppContext} from '../../../services/app-context.js' +import {testAppLinked, testOrganizationApp} from '../../../models/app/app.test-data.js' +import {OrganizationSource} from '../../../models/organization.js' +import {appEnvShowJsonOutputSchema} from '../../../services/app/env/show/types.js' +import {Config} from '@oclif/core' +import {expect, test, vi} from 'vitest' +import * as context from '@shopify/cli-kit/node/context/local' +import {mockAndCaptureStandardStreams} from '@shopify/cli-kit/node/testing/output' +import {outputInfo} from '@shopify/cli-kit/node/output' +import {runWithCommandEventsForCommand} from '@shopify/cli-kit/node/command-events' + +vi.mock('../../../services/app-context.js') +vi.mock('../../../services/context.js') + +test.each([ + {secret: 'secret', scopes: 'read_products'}, + {secret: undefined, scopes: ''}, + {secret: '', scopes: ''}, +])('writes one JSON result with secret $secret and scopes $scopes', async ({secret, scopes}) => { + const app = testAppLinked() + app.configuration.access_scopes = {scopes} + const remoteApp = testOrganizationApp({apiSecretKeys: secret === undefined ? [] : [{secret}]}) + vi.mocked(linkedAppContext).mockImplementation(async () => { + outputInfo('Loaded app') + return { + app, + remoteApp, + organization: {id: '1', businessName: 'Example', source: OrganizationSource.BusinessPlatform}, + } as Awaited> + }) + const command = new EnvShow(['--json'], await Config.load()) + vi.spyOn(context, 'isUnitTest').mockReturnValue(false) + const streams = mockAndCaptureStandardStreams() + try { + await runWithCommandEventsForCommand(['--json'], () => command.run()) + expect(streams.stdout()).toBe( + `${JSON.stringify({SHOPIFY_API_KEY: remoteApp.apiKey, SHOPIFY_API_SECRET: secret, SCOPES: scopes}, null, 2)}\n`, + ) + expect(JSON.parse(streams.stderr())).toMatchObject({type: 'diagnostic', message: 'Loaded app'}) + expect(linkedAppContext).toHaveBeenCalledWith({ + directory: expect.any(String), + clientId: undefined, + forceRelink: false, + userProvidedConfigName: undefined, + }) + } finally { + streams.restore() + } +}) + +test('exposes the schema and JSON flag', () => { + expect(EnvShow.jsonOutputSchema).toBe(appEnvShowJsonOutputSchema) + expect(EnvShow.description).toContain('AppEnvShowResult') + expect(EnvShow.flags.json).toBeDefined() +}) + +test('propagates failures before writing any result', async () => { + vi.mocked(linkedAppContext).mockRejectedValue(new Error('Authentication failed')) + const command = new EnvShow(['--json'], await Config.load()) + vi.spyOn(context, 'isUnitTest').mockReturnValue(false) + const streams = mockAndCaptureStandardStreams() + try { + await expect(command.run()).rejects.toThrow('Authentication failed') + expect(streams.stdout()).toBe('') + } finally { + streams.restore() + } +}) + +test.each([ + {SHOPIFY_API_KEY: 1, SCOPES: ''}, + {SHOPIFY_API_KEY: 'key', SHOPIFY_API_SECRET: null, SCOPES: ''}, + {SHOPIFY_API_KEY: 'key', SCOPES: []}, +])('rejects malformed data %j', (value) => { + expect(() => appEnvShowJsonOutputSchema.validate(value)).toThrow() +}) diff --git a/packages/app/src/cli/commands/app/env/show.ts b/packages/app/src/cli/commands/app/env/show.ts index 73448ab9a95..91569289551 100644 --- a/packages/app/src/cli/commands/app/env/show.ts +++ b/packages/app/src/cli/commands/app/env/show.ts @@ -2,18 +2,24 @@ import {appFlags} from '../../../flags.js' import {linkedAppContext} from '../../../services/app-context.js' import {showEnv} from '../../../services/app/env/show.js' import AppLinkedCommand, {AppLinkedCommandOutput} from '../../../utilities/app-linked-command.js' -import {globalFlags} from '@shopify/cli-kit/node/cli' -import {outputResult} from '@shopify/cli-kit/node/output' +import {appEnvShowJsonOutputSchema} from '../../../services/app/env/show/types.js' +import {renderAppEnvShowResult} from '../../../services/app/env/show/result.js' +import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli' export default class EnvShow extends AppLinkedCommand { static summary = 'Display app and extensions environment variables.' static descriptionWithMarkdown = `Displays environment variables that can be used to deploy apps and app extensions.` + static get jsonOutputSchema() { + return appEnvShowJsonOutputSchema + } + static description = this.descriptionForHelp() static flags = { ...globalFlags, + ...jsonFlag, ...appFlags, } @@ -25,7 +31,8 @@ export default class EnvShow extends AppLinkedCommand { forceRelink: flags.reset, userProvidedConfigName: flags.config, }) - outputResult(await showEnv(app, remoteApp, organization)) + const result = await showEnv(app, remoteApp, organization) + renderAppEnvShowResult(result, flags.json ? 'json' : 'text') return {app} } } diff --git a/packages/app/src/cli/services/app/env/show.test.ts b/packages/app/src/cli/services/app/env/show.test.ts index 229d583cf54..1f0546dd07a 100644 --- a/packages/app/src/cli/services/app/env/show.test.ts +++ b/packages/app/src/cli/services/app/env/show.test.ts @@ -1,4 +1,4 @@ -import {showEnv} from './show.js' +import {showEnv, outputEnv} from './show.js' import {fetchOrganizations} from '../../dev/fetch.js' import {AppInterface} from '../../../models/app/app.js' import {testApp, testOrganizationApp} from '../../../models/app/app.test-data.js' @@ -6,7 +6,7 @@ import {OrganizationSource} from '../../../models/organization.js' import {selectOrganizationPrompt} from '@shopify/organizations' import {describe, expect, vi, test} from 'vitest' import * as file from '@shopify/cli-kit/node/fs' -import {stringifyMessage, unstyled} from '@shopify/cli-kit/node/output' +import {stringifyMessage, unstyled, outputContent, outputToken} from '@shopify/cli-kit/node/output' vi.mock('../../dev/fetch.js') vi.mock('@shopify/organizations') @@ -34,7 +34,7 @@ describe('env show', () => { vi.mocked(selectOrganizationPrompt).mockResolvedValue(organization) // When - const result = await showEnv(app, remoteApp, organization) + const result = await outputEnv(app, remoteApp, organization, 'text') // Then expect(file.writeFile).not.toHaveBeenCalled() @@ -63,3 +63,16 @@ function mockApp(): AppInterface { }, }) } + +test('returns environment facts and omits absent secrets when encoded', async () => { + const app = mockApp() + const remoteApp = testOrganizationApp({apiSecretKeys: []}) + const organization = {id: '123', businessName: 'test', source: OrganizationSource.BusinessPlatform} + await expect(showEnv(app, remoteApp, organization)).resolves.toEqual({ + SHOPIFY_API_KEY: remoteApp.apiKey, + SHOPIFY_API_SECRET: undefined, + SCOPES: 'my-scope', + }) + const legacy = outputContent`${outputToken.json({SHOPIFY_API_KEY: remoteApp.apiKey, SCOPES: 'my-scope'})}` + expect(stringifyMessage(await outputEnv(app, remoteApp, organization, 'json'))).toBe(stringifyMessage(legacy)) +}) diff --git a/packages/app/src/cli/services/app/env/show.ts b/packages/app/src/cli/services/app/env/show.ts index a2dbab01a07..3430c7ee256 100644 --- a/packages/app/src/cli/services/app/env/show.ts +++ b/packages/app/src/cli/services/app/env/show.ts @@ -1,3 +1,5 @@ +import {type AppEnvShowResult} from './show/types.js' +import {formatAppEnvShowResult} from './show/result.js' import {AppInterface, getAppScopes} from '../../../models/app/app.js' import {Organization, OrganizationApp} from '../../../models/organization.js' import {logMetadataForLoadedContext} from '../../context.js' @@ -9,8 +11,13 @@ export async function showEnv( app: AppInterface, remoteApp: OrganizationApp, organization: Organization, -): Promise { - return outputEnv(app, remoteApp, organization, 'text') +): Promise { + await logMetadataForLoadedContext(remoteApp, organization.source) + return { + SHOPIFY_API_KEY: remoteApp.apiKey, + SHOPIFY_API_SECRET: remoteApp.apiSecretKeys[0]?.secret, + SCOPES: getAppScopes(app.configuration), + } } export async function outputEnv( @@ -19,19 +26,7 @@ export async function outputEnv( organization: Organization, format: Format, ): Promise { - await logMetadataForLoadedContext(remoteApp, organization.source) - - if (format === 'json') { - return outputContent`${outputToken.json({ - SHOPIFY_API_KEY: remoteApp.apiKey, - SHOPIFY_API_SECRET: remoteApp.apiSecretKeys[0]?.secret, - SCOPES: getAppScopes(app.configuration), - })}` - } else { - return outputContent` - ${outputToken.green('SHOPIFY_API_KEY')}=${remoteApp.apiKey} - ${outputToken.green('SHOPIFY_API_SECRET')}=${remoteApp.apiSecretKeys[0]?.secret ?? ''} - ${outputToken.green('SCOPES')}=${getAppScopes(app.configuration)} - ` - } + // Compatibility adapter for app info --web-env. + const result = await showEnv(app, remoteApp, organization) + return format === 'json' ? outputContent`${outputToken.json(result)}` : formatAppEnvShowResult(result, 'text') } diff --git a/packages/app/src/cli/services/app/env/show/result.test.ts b/packages/app/src/cli/services/app/env/show/result.test.ts new file mode 100644 index 00000000000..8d6dc099655 --- /dev/null +++ b/packages/app/src/cli/services/app/env/show/result.test.ts @@ -0,0 +1,19 @@ +import {formatAppEnvShowResult} from './result.js' +import {appEnvShowJsonOutputSchema} from './types.js' +import {expect, test} from 'vitest' +import {outputContent, outputToken, stringifyMessage, unstyled} from '@shopify/cli-kit/node/output' + +test.each([ + {SHOPIFY_API_KEY: 'key', SHOPIFY_API_SECRET: 'secret', SCOPES: 'read_products'}, + {SHOPIFY_API_KEY: 'key', SCOPES: ''}, +])('matches the unstyled app info --web-env JSON for %j', (value) => { + const legacy = outputContent`${outputToken.json(value)}` + expect(stringifyMessage(formatAppEnvShowResult(value, 'json'))).toBe(unstyled(stringifyMessage(legacy))) + expect(appEnvShowJsonOutputSchema.encode(value)).toBe(JSON.stringify(value, null, 2)) +}) + +test('preserves text formatting with an absent secret and empty scopes', () => { + expect(unstyled(stringifyMessage(formatAppEnvShowResult({SHOPIFY_API_KEY: 'key', SCOPES: ''}, 'text')))).toBe( + '\n SHOPIFY_API_KEY=key\n SHOPIFY_API_SECRET=\n SCOPES=\n ', + ) +}) diff --git a/packages/app/src/cli/services/app/env/show/result.ts b/packages/app/src/cli/services/app/env/show/result.ts new file mode 100644 index 00000000000..b93a1a7bf49 --- /dev/null +++ b/packages/app/src/cli/services/app/env/show/result.ts @@ -0,0 +1,15 @@ +import {appEnvShowJsonOutputSchema, type AppEnvShowResult} from './types.js' +import {OutputMessage, outputContent, outputToken, outputResult} from '@shopify/cli-kit/node/output' + +export function formatAppEnvShowResult(result: AppEnvShowResult, format: 'json' | 'text'): OutputMessage { + if (format === 'json') return appEnvShowJsonOutputSchema.encode(result) + return outputContent` + ${outputToken.green('SHOPIFY_API_KEY')}=${result.SHOPIFY_API_KEY} + ${outputToken.green('SHOPIFY_API_SECRET')}=${result.SHOPIFY_API_SECRET ?? ''} + ${outputToken.green('SCOPES')}=${result.SCOPES} + ` +} + +export function renderAppEnvShowResult(result: AppEnvShowResult, format: 'json' | 'text'): void { + outputResult(formatAppEnvShowResult(result, format)) +} diff --git a/packages/app/src/cli/services/app/env/show/types.ts b/packages/app/src/cli/services/app/env/show/types.ts new file mode 100644 index 00000000000..45ea77ebbe1 --- /dev/null +++ b/packages/app/src/cli/services/app/env/show/types.ts @@ -0,0 +1,13 @@ +import {defineJsonOutputSchema, type InferJsonOutputSchema} from '@shopify/cli-kit/node/json-output-schema' +import {zod} from '@shopify/cli-kit/node/schema' + +export const appEnvShowJsonOutputSchema = defineJsonOutputSchema({ + name: 'AppEnvShowResult', + schema: zod.object({ + SHOPIFY_API_KEY: zod.string(), + SHOPIFY_API_SECRET: zod.string().optional(), + SCOPES: zod.string(), + }), +}) + +export type AppEnvShowResult = InferJsonOutputSchema diff --git a/packages/cli-kit/src/public/node/testing/output.test.ts b/packages/cli-kit/src/public/node/testing/output.test.ts new file mode 100644 index 00000000000..41046e63aa4 --- /dev/null +++ b/packages/cli-kit/src/public/node/testing/output.test.ts @@ -0,0 +1,73 @@ +import {mockAndCaptureStandardStreams} from './output.js' +import {describe, expect, test, vi} from 'vitest' + +describe('mockAndCaptureStandardStreams', () => { + test('captures stdout, stderr, and console warnings separately', () => { + const streams = mockAndCaptureStandardStreams() + + try { + expect(process.stdout.write('result\n')).toBe(true) + process.stderr.write('diagnostic\n') + // eslint-disable-next-line no-console + console.warn('Warning: %s', 'example') + + expect(streams.stdout()).toBe('result\n') + expect(streams.stderr()).toBe('diagnostic\nWarning: example\n') + } finally { + streams.restore() + } + }) + + test('preserves byte chunks and string encodings', () => { + const streams = mockAndCaptureStandardStreams() + + try { + const encoded = Buffer.from('€') + process.stdout.write(encoded.subarray(0, 1)) + process.stdout.write(new Uint8Array(encoded.subarray(1))) + process.stdout.write('21', 'hex') + + expect(streams.stdout()).toBe('€!') + expect(streams.stderr()).toBe('') + } finally { + streams.restore() + } + }) + + test('calls write callbacks asynchronously, including empty writes used to flush output', async () => { + const streams = mockAndCaptureStandardStreams() + const callback = vi.fn() + + try { + process.stdout.write('', callback) + expect(callback).not.toHaveBeenCalled() + await new Promise((resolve) => process.stderr.write('message', 'utf8', () => resolve())) + + expect(callback).toHaveBeenCalledOnce() + expect(streams.stdout()).toBe('') + expect(streams.stderr()).toBe('message') + } finally { + streams.restore() + } + }) + + test('restores the original writers and leaves captured output available', () => { + const stdoutWrite = process.stdout.write + const stderrWrite = process.stderr.write + // eslint-disable-next-line no-console + const consoleWarn = console.warn + const streams = mockAndCaptureStandardStreams() + + try { + process.stdout.write('captured') + } finally { + streams.restore() + } + + expect(process.stdout.write).toBe(stdoutWrite) + expect(process.stderr.write).toBe(stderrWrite) + // eslint-disable-next-line no-console + expect(console.warn).toBe(consoleWarn) + expect(streams.stdout()).toBe('captured') + }) +}) diff --git a/packages/cli-kit/src/public/node/testing/output.ts b/packages/cli-kit/src/public/node/testing/output.ts index 75fd042d41a..ec0afc140c2 100644 --- a/packages/cli-kit/src/public/node/testing/output.ts +++ b/packages/cli-kit/src/public/node/testing/output.ts @@ -1,4 +1,6 @@ import {collectedLogs, clearCollectedLogs} from '../output.js' +// eslint-disable-next-line n/prefer-global/console +import {Console} from 'node:console' interface OutputMock { output: () => string @@ -11,6 +13,63 @@ interface OutputMock { clear: () => void } +interface StandardStreamsMock { + stdout: () => string + stderr: () => string + restore: () => void +} + +/** + * Captures writes to stdout and stderr, including console warnings intercepted by Vitest. + * Call restore in a finally block. This replaces process globals and must not be used in concurrent tests. + * CLI output tests must disable SHOPIFY_UNIT_TEST and reset modules before loading the command. + * + * @returns Captured output and a function to restore the original writers. + */ +export function mockAndCaptureStandardStreams(): StandardStreamsMock { + const stdout = captureStream(process.stdout) + const stderr = captureStream(process.stderr) + // Vitest intercepts console.warn before it reaches stderr; use Node's console to exercise the writer. + // eslint-disable-next-line no-console + const originalWarn = console.warn + // eslint-disable-next-line no-console + console.warn = new Console(process.stdout, process.stderr).warn + + return { + stdout: stdout.output, + stderr: stderr.output, + restore: () => { + // eslint-disable-next-line no-console + console.warn = originalWarn + stdout.restore() + stderr.restore() + }, + } +} + +function captureStream(stream: NodeJS.WriteStream) { + const chunks: Buffer[] = [] + const originalWrite = stream.write + stream.write = ( + chunk: string | Uint8Array, + encodingOrCallback?: BufferEncoding | ((error?: Error | null) => void), + callback?: (error?: Error | null) => void, + ) => { + const encoding = typeof encodingOrCallback === 'string' ? encodingOrCallback : 'utf8' + chunks.push(typeof chunk === 'string' ? Buffer.from(chunk, encoding) : Buffer.from(chunk)) + const onWrite = typeof encodingOrCallback === 'function' ? encodingOrCallback : callback + if (onWrite) queueMicrotask(() => onWrite()) + return true + } + + return { + output: () => Buffer.concat(chunks).toString('utf8'), + restore: () => { + stream.write = originalWrite + }, + } +} + /** * Returns a set of functions to get the outputs ocurred during a test run. * diff --git a/packages/cli/README.md b/packages/cli/README.md index 38c5dc2c8ff..52ff152302c 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -906,14 +906,18 @@ Display app and extensions environment variables. ``` USAGE - $ shopify app env show [--auth-alias ] [--client-id | -c ] [--json-schema] [--no-color] - [--path ] [--reset | ] [--verbose] + $ shopify app env show [--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] @@ -946,6 +950,34 @@ DESCRIPTION Display app and extensions environment variables. Displays environment variables that can be used to deploy apps and app extensions. + + Output from `--json` conforms to the `AppEnvShowResult` schema. + + Use `--json-schema` to print the result, error, and event schemas. + + ```json + { + "type": "object", + "properties": { + "SHOPIFY_API_KEY": { + "type": "string" + }, + "SHOPIFY_API_SECRET": { + "type": "string" + }, + "SCOPES": { + "type": "string" + } + }, + "required": [ + "SHOPIFY_API_KEY", + "SCOPES" + ], + "additionalProperties": false, + "title": "AppEnvShowResult", + "$schema": "http://json-schema.org/draft-07/schema#" + } + ``` ``` ## `shopify app execute` diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index 1b88620390e..4106d9fdf51 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -1786,7 +1786,7 @@ "args": { }, "customPluginName": "@shopify/app", - "description": "Displays environment variables that can be used to deploy apps and app extensions.", + "description": "Displays environment variables that can be used to deploy apps and app extensions.\n\nOutput from `--json` conforms to the `AppEnvShowResult` schema.\n\nUse `--json-schema` to print the result, error, and event schemas.\n\n```json\n{\n \"type\": \"object\",\n \"properties\": {\n \"SHOPIFY_API_KEY\": {\n \"type\": \"string\"\n },\n \"SHOPIFY_API_SECRET\": {\n \"type\": \"string\"\n },\n \"SCOPES\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"SHOPIFY_API_KEY\",\n \"SCOPES\"\n ],\n \"additionalProperties\": false,\n \"title\": \"AppEnvShowResult\",\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n}\n```", "descriptionWithMarkdown": "Displays environment variables that can be used to deploy apps and app extensions.", "flags": { "auth-alias": { @@ -1819,6 +1819,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 7848d558f0f..a73b0a1b334 100644 --- a/packages/eslint-plugin-cli/rules/json-output-command-exceptions.js +++ b/packages/eslint-plugin-cli/rules/json-output-command-exceptions.js @@ -17,7 +17,6 @@ const commandExceptions = [ 'packages/app/src/cli/commands/app/doctor/instructions.ts', 'packages/app/src/cli/commands/app/doctor/submit.ts', 'packages/app/src/cli/commands/app/env/pull.ts', - 'packages/app/src/cli/commands/app/env/show.ts', 'packages/app/src/cli/commands/app/execute.ts', 'packages/app/src/cli/commands/app/function/build.ts', 'packages/app/src/cli/commands/app/function/info.ts',