From de2c568f814438f5c3805eba1b012b8c544cda30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isaac=20Rold=C3=A1n?= Date: Tue, 6 Oct 2026 16:47:43 +0200 Subject: [PATCH] Add typed JSON output to auth logout --- .changeset/auth-logout-json.md | 5 + .../generated/generated_docs_data_v2.json | 38 +++++- packages/cli/README.md | 40 +++++- packages/cli/oclif.manifest.json | 35 ++++- .../src/cli/commands/auth/logout-json.test.ts | 121 ++++++++++++++++++ packages/cli/src/cli/commands/auth/logout.ts | 27 +++- .../rules/json-output-command-exceptions.js | 1 - 7 files changed, 260 insertions(+), 7 deletions(-) create mode 100644 .changeset/auth-logout-json.md create mode 100644 packages/cli/src/cli/commands/auth/logout-json.test.ts diff --git a/.changeset/auth-logout-json.md b/.changeset/auth-logout-json.md new file mode 100644 index 00000000000..4f1c71c72ea --- /dev/null +++ b/.changeset/auth-logout-json.md @@ -0,0 +1,5 @@ +--- +'@shopify/cli': minor +--- + +Add typed JSON output to `auth logout`. diff --git a/docs-shopify.dev/generated/generated_docs_data_v2.json b/docs-shopify.dev/generated/generated_docs_data_v2.json index ba48244cff6..0cff3da553e 100644 --- a/docs-shopify.dev/generated/generated_docs_data_v2.json +++ b/docs-shopify.dev/generated/generated_docs_data_v2.json @@ -4206,9 +4206,45 @@ "description": "Print the command's JSON schemas.", "isOptional": true, "environmentValue": "SHOPIFY_FLAG_JSON_SCHEMA" + }, + { + "filePath": "docs-shopify.dev/commands/interfaces/auth-logout.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/auth-logout.interface.ts", + "syntaxKind": "PropertySignature", + "name": "--no-input", + "value": "''", + "description": "Disable interactive prompts and browser authentication.", + "isOptional": true, + "environmentValue": "SHOPIFY_FLAG_NO_INPUT" + }, + { + "filePath": "docs-shopify.dev/commands/interfaces/auth-logout.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/auth-logout.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 authlogout {\n /**\n * Print the command's JSON schemas.\n * @environment SHOPIFY_FLAG_JSON_SCHEMA\n */\n '--json-schema'?: ''\n}" + "value": "export interface authlogout {\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 * Disable interactive prompts and browser authentication.\n * @environment SHOPIFY_FLAG_NO_INPUT\n */\n '--no-input'?: ''\n\n /**\n * Increase the verbosity of the output. May include sensitive data.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}" } }, "commands": { diff --git a/packages/cli/README.md b/packages/cli/README.md index b3624194936..5f19dfc54e9 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -3725,15 +3725,53 @@ Logs you out of the Shopify account or Partner account and store. ``` USAGE - $ shopify auth logout [--json-schema] + $ shopify auth logout [-j] [--json-schema] [--no-color] [--no-input] [--verbose] FLAGS + -j, --json + Output the result as JSON. Automatically disables color output. + [env: SHOPIFY_FLAG_JSON] + --json-schema Print the command's JSON schemas. [env: SHOPIFY_FLAG_JSON_SCHEMA] + --no-color + Disable color output. + [env: SHOPIFY_FLAG_NO_COLOR] + + --no-input + Disable interactive prompts and browser authentication. + [env: SHOPIFY_FLAG_NO_INPUT] + + --verbose + Increase the verbosity of the output. May include sensitive data. + [env: SHOPIFY_FLAG_VERBOSE] + DESCRIPTION Logs you out of the Shopify account or Partner account and store. + + Use `--json-schema` to print the result, error, and event schemas. + + Output from `--json` conforms to the `AuthLogoutResult` schema. + + ```json + { + "type": "object", + "properties": { + "status": { + "type": "string", + "const": "success" + } + }, + "required": [ + "status" + ], + "additionalProperties": false, + "title": "AuthLogoutResult", + "$schema": "http://json-schema.org/draft-07/schema#" + } + ``` ``` ## `shopify commands` diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index 495218cf0ca..9f7c12128cd 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -5571,15 +5571,48 @@ ], "args": { }, - "description": "Logs you out of the Shopify account or Partner account and store.", + "description": "Logs you out of the Shopify account or Partner account and store.\n\nUse `--json-schema` to print the result, error, and event schemas.\n\nOutput from `--json` conforms to the `AuthLogoutResult` schema.\n\n```json\n{\n \"type\": \"object\",\n \"properties\": {\n \"status\": {\n \"type\": \"string\",\n \"const\": \"success\"\n }\n },\n \"required\": [\n \"status\"\n ],\n \"additionalProperties\": false,\n \"title\": \"AuthLogoutResult\",\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n}\n```", + "descriptionWithMarkdown": "Logs you out of the Shopify account or Partner account and store.", "enableJsonFlag": false, "flags": { + "json": { + "allowNo": false, + "char": "j", + "description": "Output the result as JSON. Automatically disables color output.", + "env": "SHOPIFY_FLAG_JSON", + "hidden": false, + "name": "json", + "type": "boolean" + }, "json-schema": { "allowNo": false, "description": "Print the command's JSON schemas.", "env": "SHOPIFY_FLAG_JSON_SCHEMA", "name": "json-schema", "type": "boolean" + }, + "no-color": { + "allowNo": false, + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", + "type": "boolean" + }, + "no-input": { + "allowNo": false, + "description": "Disable interactive prompts and browser authentication.", + "env": "SHOPIFY_FLAG_NO_INPUT", + "name": "no-input", + "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, diff --git a/packages/cli/src/cli/commands/auth/logout-json.test.ts b/packages/cli/src/cli/commands/auth/logout-json.test.ts new file mode 100644 index 00000000000..8dc62caaee2 --- /dev/null +++ b/packages/cli/src/cli/commands/auth/logout-json.test.ts @@ -0,0 +1,121 @@ +import Logout from './logout.js' +import {logout} from '@shopify/cli-kit/node/session' +import {launchCLI} from '@shopify/cli-kit/node/cli-launcher' +import {ShopifyConfig} from '@shopify/cli-kit/node/custom-oclif-loader' +import {AbortError} from '@shopify/cli-kit/node/error' +import {outputInfo, unstyled} from '@shopify/cli-kit/node/output' +import {mockAndCaptureOutput, withCapturedStandardStreams} from '@shopify/cli-kit/node/testing/output' +import {beforeEach, afterEach, expect, test, vi} from 'vitest' + +vi.mock('@shopify/cli-kit/node/session') + +beforeEach(() => { + vi.stubEnv('CI', '1') + vi.stubEnv('SHOPIFY_CLI_NO_ANALYTICS', '1') + vi.spyOn(ShopifyConfig.prototype, 'runHook').mockResolvedValue({successes: [], failures: []}) +}) + +afterEach(() => { + vi.unstubAllEnvs() + mockAndCaptureOutput().clear() +}) + +test.each(['--json', '-j'])('writes one completed result through the launcher with %s', async (flag) => { + await withCapturedStandardStreams(async ({stdout, stderr}) => { + await launchCLI({ + moduleURL: import.meta.url, + argv: ['auth', 'logout', flag], + lazyCommandLoader: async () => Logout, + }) + + expect(logout).toHaveBeenCalledExactlyOnceWith() + expect(stdout()).toBe(`${JSON.stringify({status: 'success'}, null, 2)}\n`) + expect(stderr()).toBe('') + }) +}) + +test('supports the JSON environment flag', async () => { + vi.stubEnv('SHOPIFY_FLAG_JSON', '1') + + await withCapturedStandardStreams(async ({stdout, stderr}) => { + await Logout.run([], import.meta.url) + + expect(JSON.parse(stdout())).toEqual({status: 'success'}) + expect(stderr()).toBe('') + }) +}) + +test.each([false, true])('keeps no-input independent from JSON output: %s', async (json) => { + await withCapturedStandardStreams(async ({stdout, stderr}) => { + await Logout.run(['--no-input', ...(json ? ['--json'] : [])], import.meta.url) + + if (json) { + expect(JSON.parse(stdout())).toEqual({status: 'success'}) + expect(stderr()).toBe('') + } else { + expect(stdout()).toBe('') + expect(unstyled(stderr())).toBe('✅ Success! Logged out from all the accounts.\n') + } + }) +}) + +test('exposes the schema in help', () => { + expect(Logout.description).toContain('Output from `--json` conforms to the `AuthLogoutResult` schema.') +}) + +test.each([{}, {status: 'failed'}, {status: true}, {status: null}, {status: 'success', accessToken: 'secret'}])( + 'rejects an invalid result %j', + (value) => { + expect(() => Logout.jsonOutputSchema.validate(value)).toThrow() + }, +) + +test('discovers the schema without logging out', async () => { + await withCapturedStandardStreams(async ({stdout, stderr}) => { + await launchCLI({ + moduleURL: import.meta.url, + argv: ['auth', 'logout', '--json-schema'], + lazyCommandLoader: async () => Logout, + }) + + expect(JSON.parse(stdout()).definitions.Result).toMatchObject({ + type: 'object', + properties: {status: {type: 'string', const: 'success'}}, + required: ['status'], + additionalProperties: false, + }) + expect(logout).not.toHaveBeenCalled() + expect(stderr()).toBe('') + }) +}) + +test('writes diagnostics to stderr separately from the result', async () => { + vi.mocked(logout).mockImplementation(async () => { + outputInfo('Session cleanup diagnostic') + }) + + await withCapturedStandardStreams(async ({stdout, stderr}) => { + await Logout.run(['--json'], import.meta.url) + + expect(JSON.parse(stdout())).toEqual({status: 'success'}) + expect(JSON.parse(stderr())).toMatchObject({ + type: 'diagnostic', + level: 'info', + message: 'Session cleanup diagnostic', + }) + }) +}) + +test('writes only the fatal error and preserves the failure exit code', async () => { + vi.stubEnv('SHOPIFY_FLAG_JSON', '1') + const exit = vi.spyOn(process, 'exit').mockReturnValue(undefined as never) + vi.mocked(logout).mockRejectedValue(new AbortError('Cannot clear sessions.')) + + await withCapturedStandardStreams(async ({stdout, stderr}) => { + await Logout.run(['--json'], import.meta.url) + + expect(JSON.parse(stdout())).toEqual({error: {type: 'abort', message: 'Cannot clear sessions.'}}) + expect(stderr()).toBe('') + expect(exit).toHaveBeenCalledExactlyOnceWith(1) + }) +}) diff --git a/packages/cli/src/cli/commands/auth/logout.ts b/packages/cli/src/cli/commands/auth/logout.ts index de1ce42c00b..53a9484d794 100644 --- a/packages/cli/src/cli/commands/auth/logout.ts +++ b/packages/cli/src/cli/commands/auth/logout.ts @@ -1,12 +1,33 @@ import Command from '@shopify/cli-kit/node/base-command' -import {outputSuccess} from '@shopify/cli-kit/node/output' +import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli' +import {outputResult, outputSuccess} from '@shopify/cli-kit/node/output' import {logout} from '@shopify/cli-kit/node/session' +import {defineJsonOutputSchema} from '@shopify/cli-kit/node/json-output-schema' +import {zod} from '@shopify/cli-kit/node/schema' + +const authLogoutJsonOutputSchema = defineJsonOutputSchema({ + name: 'AuthLogoutResult', + schema: zod.object({status: zod.literal('success')}).strict(), +}) export default class Logout extends Command { - static description = 'Logs you out of the Shopify account or Partner account and store.' + static descriptionWithMarkdown = 'Logs you out of the Shopify account or Partner account and store.' + + static description = this.descriptionForHelp() + + static flags = {...globalFlags, ...jsonFlag} + + static get jsonOutputSchema() { + return authLogoutJsonOutputSchema + } async run(): Promise { + const {flags} = await this.parse(Logout) await logout() - outputSuccess('Logged out from all the accounts') + if (flags.json) { + outputResult(authLogoutJsonOutputSchema.encode({status: 'success'})) + } else { + outputSuccess('Logged out from all the accounts') + } } } 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 b300955e29d..347ad7b0f8f 100644 --- a/packages/eslint-plugin-cli/rules/json-output-command-exceptions.js +++ b/packages/eslint-plugin-cli/rules/json-output-command-exceptions.js @@ -33,7 +33,6 @@ const commandExceptions = [ 'packages/app/src/cli/commands/app/subscription-migrations/unschedule.ts', 'packages/app/src/cli/commands/app/webhook/trigger.ts', 'packages/cli/src/cli/commands/auth/login.ts', - 'packages/cli/src/cli/commands/auth/logout.ts', 'packages/cli/src/cli/commands/doc/fetch.ts', 'packages/cli/src/cli/commands/doc/search.ts', 'packages/cli/src/cli/commands/upgrade.ts',