diff --git a/.changeset/typed-app-config-validate.md b/.changeset/typed-app-config-validate.md new file mode 100644 index 00000000000..d49ab14c3a9 --- /dev/null +++ b/.changeset/typed-app-config-validate.md @@ -0,0 +1,4 @@ +--- +'@shopify/cli': minor +--- +Expose the JSON output schema for app config validate. diff --git a/packages/app/src/cli/commands/app/config/validate.test.ts b/packages/app/src/cli/commands/app/config/validate.test.ts index aa7ef84271c..4bc3273c30a 100644 --- a/packages/app/src/cli/commands/app/config/validate.test.ts +++ b/packages/app/src/cli/commands/app/config/validate.test.ts @@ -1,4 +1,5 @@ import Validate from './validate.js' +import {appConfigValidateJsonOutputSchema} from '../../../services/validate/types.js' import {linkedAppContext} from '../../../services/app-context.js' import {validateApp} from '../../../services/validate.js' import {testAppLinked} from '../../../models/app/app.test-data.js' @@ -40,39 +41,39 @@ describe('app config validate command', () => { expect(Validate.flags['client-id']?.exclusive).toEqual(['config']) }) - test('calls validateApp with json: false by default', async () => { + test('returns validation facts and presents text by default', async () => { const app = testAppLinked() mockHealthyProject() vi.mocked(linkedAppContext).mockResolvedValue({app} as Awaited>) - vi.mocked(validateApp).mockResolvedValue() + vi.mocked(validateApp).mockResolvedValue({valid: true, issues: []}) await Validate.run([], import.meta.url) - expect(validateApp).toHaveBeenCalledWith(app, {json: false}) + expect(validateApp).toHaveBeenCalledWith(app) await expectValidationMetadataCalls({cmd_app_validate_json: false}) }) - test('calls validateApp with json: true when --json flag is passed', async () => { + test('encodes validation facts when --json is passed', async () => { const app = testAppLinked() mockHealthyProject() vi.mocked(linkedAppContext).mockResolvedValue({app} as Awaited>) - vi.mocked(validateApp).mockResolvedValue() + vi.mocked(validateApp).mockResolvedValue({valid: true, issues: []}) await Validate.run(['--json'], import.meta.url) - expect(validateApp).toHaveBeenCalledWith(app, {json: true}) + expect(validateApp).toHaveBeenCalledWith(app) await expectValidationMetadataCalls({cmd_app_validate_json: true}) }) - test('calls validateApp with json: true when -j flag is passed', async () => { + test('accepts the -j alias', async () => { const app = testAppLinked() mockHealthyProject() vi.mocked(linkedAppContext).mockResolvedValue({app} as Awaited>) - vi.mocked(validateApp).mockResolvedValue() + vi.mocked(validateApp).mockResolvedValue({valid: true, issues: []}) await Validate.run(['-j'], import.meta.url) - expect(validateApp).toHaveBeenCalledWith(app, {json: true}) + expect(validateApp).toHaveBeenCalledWith(app) await expectValidationMetadataCalls({cmd_app_validate_json: true}) }) @@ -83,7 +84,7 @@ describe('app config validate command', () => { file: new TomlFile('shopify.app.staging.toml', {}), } as any) vi.mocked(linkedAppContext).mockResolvedValue({app} as Awaited>) - vi.mocked(validateApp).mockResolvedValue() + vi.mocked(validateApp).mockResolvedValue({valid: true, issues: []}) await Validate.run(['--client-id', 'api-key'], import.meta.url) @@ -98,7 +99,7 @@ describe('app config validate command', () => { userProvidedConfigName: 'shopify.app.staging.toml', unsafeTolerateErrors: true, }) - expect(validateApp).toHaveBeenCalledWith(app, {json: false}) + expect(validateApp).toHaveBeenCalledWith(app) await expectValidationMetadataCalls({cmd_app_validate_json: false}) }) @@ -106,7 +107,7 @@ describe('app config validate command', () => { const app = testAppLinked() mockHealthyProject() vi.mocked(linkedAppContext).mockResolvedValue({app} as Awaited>) - vi.mocked(validateApp).mockResolvedValue() + vi.mocked(validateApp).mockResolvedValue({valid: true, issues: []}) await Validate.run([], import.meta.url) @@ -114,7 +115,7 @@ describe('app config validate command', () => { clientId: undefined, skipPrompts: false, }) - expect(validateApp).toHaveBeenCalledWith(app, {json: false}) + expect(validateApp).toHaveBeenCalledWith(app) await expectValidationMetadataCalls({cmd_app_validate_json: false}) }) @@ -214,3 +215,17 @@ describe('app config validate command', () => { ) }) }) + +test('exposes the validation schema and JSON flag', () => { + expect(Validate.jsonOutputSchema).toBe(appConfigValidateJsonOutputSchema) + expect(Validate.flags.json).toBeDefined() + expect(Validate.description).toContain('AppConfigValidateResult') +}) + +test('does not convert authentication failures into validation results', async () => { + mockHealthyProject() + vi.mocked(linkedAppContext).mockRejectedValue(new AbortError('Authentication failed')) + await expect(Validate.run(['--json'], import.meta.url)).rejects.toThrow() + expect(outputResult).not.toHaveBeenCalled() + await expectValidationMetadataCalls({cmd_app_validate_json: true}) +}) diff --git a/packages/app/src/cli/commands/app/config/validate.ts b/packages/app/src/cli/commands/app/config/validate.ts index 0a77f195952..1e260c8634c 100644 --- a/packages/app/src/cli/commands/app/config/validate.ts +++ b/packages/app/src/cli/commands/app/config/validate.ts @@ -1,4 +1,6 @@ import {appFlags} from '../../../flags.js' +import {appConfigValidateJsonOutputSchema} from '../../../services/validate/types.js' +import {renderAppConfigValidateResult} from '../../../services/validate/result.js' import {validateApp} from '../../../services/validate.js' import AppLinkedCommand, {AppLinkedCommandOutput} from '../../../utilities/app-linked-command.js' import {linkedAppContext} from '../../../services/app-context.js' @@ -25,6 +27,10 @@ export default class Validate extends AppLinkedCommand { static descriptionWithMarkdown = `Validates the selected app configuration file and all extension configurations against their schemas and reports any errors found.` + static get jsonOutputSchema() { + return appConfigValidateJsonOutputSchema + } + static description = this.descriptionForHelp() static flags = { @@ -48,7 +54,7 @@ export default class Validate extends AppLinkedCommand { if (err instanceof AbortError && flags.json) { await recordValidationFailure(1, 1) const message = unstyled(stringifyMessage(err.message)).trim() - outputResult(JSON.stringify({valid: false, issues: [{message}]}, null, 2)) + outputResult(appConfigValidateJsonOutputSchema.encode({valid: false, issues: [{message}]})) throw new AbortSilentError() } throw err @@ -65,7 +71,7 @@ export default class Validate extends AppLinkedCommand { if (err instanceof AbortError && flags.json) { await recordValidationFailure(1, 1) const message = unstyled(stringifyMessage(err.message)).trim() - outputResult(JSON.stringify({valid: false, issues: [{message}]}, null, 2)) + outputResult(appConfigValidateJsonOutputSchema.encode({valid: false, issues: [{message}]})) throw new AbortSilentError() } throw err @@ -77,7 +83,7 @@ export default class Validate extends AppLinkedCommand { const fileCount = new Set(configErrors.map((err) => err.path)).size await recordValidationFailure(issues.length, fileCount) if (flags.json) { - outputResult(JSON.stringify({valid: false, issues}, null, 2)) + outputResult(appConfigValidateJsonOutputSchema.encode({valid: false, issues})) throw new AbortSilentError() } renderError({ @@ -105,13 +111,14 @@ export default class Validate extends AppLinkedCommand { const isValidationError = message.startsWith('Validation errors in ') if (isValidationError && flags.json) { await recordValidationFailure(1, 1) - outputResult(JSON.stringify({valid: false, issues: [{message}]}, null, 2)) + outputResult(appConfigValidateJsonOutputSchema.encode({valid: false, issues: [{message}]})) throw new AbortSilentError() } throw err } - await validateApp(app, {json: flags.json}) + const result = await validateApp(app) + renderAppConfigValidateResult(result, app.configPath, flags.json ? 'json' : 'text') return {app} } diff --git a/packages/app/src/cli/services/validate.test.ts b/packages/app/src/cli/services/validate.test.ts index 4a7d0095f29..386639b1789 100644 --- a/packages/app/src/cli/services/validate.test.ts +++ b/packages/app/src/cli/services/validate.test.ts @@ -1,4 +1,5 @@ -import {validateApp} from './validate.js' +import {validateApp as validateAppData} from './validate.js' +import {renderAppConfigValidateResult} from './validate/result.js' import {testAppLinked} from '../models/app/app.test-data.js' import {AppErrors, formatConfigurationError} from '../models/app/loader.js' import metadata from '../metadata.js' @@ -225,3 +226,20 @@ describe('validateApp', () => { }) }) }) + +// Keep the existing presentation assertions at the new service/presenter boundary. +async function validateApp(app: Parameters[0], options = {json: false}) { + const result = await validateAppData(app) + renderAppConfigValidateResult(result, app.configPath, options.json ? 'json' : 'text') +} + +test('returns validation facts without presenting or throwing for an invalid app', async () => { + const app = testAppLinked() + app.errors.addError({file: app.configPath, message: 'Required', path: ['name'], code: 'invalid_type'}) + await expect(validateAppData(app)).resolves.toEqual({ + valid: false, + issues: [{file: app.configPath, message: 'Required', path: ['name'], code: 'invalid_type'}], + }) + expect(outputResult).not.toHaveBeenCalled() + expect(renderError).not.toHaveBeenCalled() +}) diff --git a/packages/app/src/cli/services/validate.ts b/packages/app/src/cli/services/validate.ts index 4aeaaa74d66..50b95bdd34b 100644 --- a/packages/app/src/cli/services/validate.ts +++ b/packages/app/src/cli/services/validate.ts @@ -1,14 +1,6 @@ +import {type AppConfigValidateResult} from './validate/types.js' import {AppLinkedInterface} from '../models/app/app.js' -import {formatConfigurationError} from '../models/app/loader.js' import metadata from '../metadata.js' -import {outputResult} from '@shopify/cli-kit/node/output' -import {renderError, renderSuccess} from '@shopify/cli-kit/node/ui' -import {AbortSilentError} from '@shopify/cli-kit/node/error' -import {basename} from '@shopify/cli-kit/node/path' - -interface ValidateAppOptions { - json: boolean -} async function recordValidationMetadata(valid: boolean, errors: {file: string}[]) { const fileCount = new Set(errors.map((error) => error.file)).size @@ -20,34 +12,11 @@ async function recordValidationMetadata(valid: boolean, errors: {file: string}[] })) } -export async function validateApp(app: AppLinkedInterface, options: ValidateAppOptions = {json: false}): Promise { - const appErrors = app.errors - - if (!appErrors || appErrors.isEmpty()) { - await recordValidationMetadata(true, []) - - if (options.json) { - outputResult(JSON.stringify({valid: true, issues: []}, null, 2)) - return - } - - renderSuccess({headline: `App configuration '${basename(app.configPath)}' is valid.`}) - return +export async function validateApp(app: AppLinkedInterface): Promise { + const errors = app.errors?.getErrors() ?? [] + await recordValidationMetadata(errors.length === 0, errors) + return { + valid: errors.length === 0, + issues: errors.map(({file, message, path, code}) => ({file, message, path, code})), } - - const errors = appErrors.getErrors() - await recordValidationMetadata(false, errors) - - if (options.json) { - const issues = errors.map(({file, message, path, code}) => ({file, message, path, code})) - outputResult(JSON.stringify({valid: false, issues}, null, 2)) - throw new AbortSilentError() - } - - renderError({ - headline: 'Validation errors found.', - body: errors.map((err) => `• ${formatConfigurationError(err)}`).join('\n'), - }) - - throw new AbortSilentError() } diff --git a/packages/app/src/cli/services/validate/result.test.ts b/packages/app/src/cli/services/validate/result.test.ts new file mode 100644 index 00000000000..79118a900d8 --- /dev/null +++ b/packages/app/src/cli/services/validate/result.test.ts @@ -0,0 +1,58 @@ +import {appConfigValidateJsonOutputSchema} from './types.js' +import {describe, expect, test, vi} from 'vitest' +import type * as Errors from '@shopify/cli-kit/node/error' +import type * as Output from '@shopify/cli-kit/node/output' +import type * as CommandEvents from '@shopify/cli-kit/node/command-events' +import type * as OutputTesting from '@shopify/cli-kit/node/testing/output' + +const cases = [ + {valid: true, issues: []}, + {valid: false, issues: [{message: 'No config found'}]}, + {valid: false, issues: [{file: '/app/shopify.app.toml', message: 'Invalid TOML'}]}, + {valid: false, issues: [{file: '/app/shopify.app.toml', message: 'Required', path: ['name'], code: 'invalid_type'}]}, +] + +describe('validation contract', () => { + test.each(cases)('preserves exact JSON for %j', (result) => { + expect(appConfigValidateJsonOutputSchema.encode(result)).toBe(JSON.stringify(result, null, 2)) + }) + + test.each([ + {valid: 'true', issues: []}, + {valid: true, issues: [{message: 123}]}, + {valid: false, issues: [{message: 'Required', path: [false]}]}, + {valid: false, issues: [{message: 'Required', code: null}]}, + ])('rejects malformed data %j', (result) => { + expect(() => appConfigValidateJsonOutputSchema.validate(result)).toThrow() + }) + + test.each(cases)('writes exactly one result and routes diagnostics separately for %j', async (result) => { + vi.stubEnv('SHOPIFY_UNIT_TEST', '0') + vi.resetModules() + const {mockAndCaptureStandardStreams} = await vi.importActual( + '@shopify/cli-kit/node/testing/output', + ) + const {runWithCommandEventsForCommand} = await vi.importActual( + '@shopify/cli-kit/node/command-events', + ) + const {outputInfo} = await vi.importActual('@shopify/cli-kit/node/output') + const {AbortSilentError} = await vi.importActual('@shopify/cli-kit/node/error') + const {renderAppConfigValidateResult} = await import('./result.js') + const streams = mockAndCaptureStandardStreams() + try { + const render = () => + runWithCommandEventsForCommand(['--json'], () => { + outputInfo('Checking configuration') + renderAppConfigValidateResult(result, '/app/shopify.app.toml', 'json') + }) + if (result.valid) render() + else expect(render).toThrow(AbortSilentError) + expect(streams.stdout()).toBe(`${JSON.stringify(result, null, 2)}\n`) + expect(JSON.parse(streams.stderr())).toMatchObject({type: 'diagnostic', message: 'Checking configuration'}) + } finally { + streams.restore() + vi.unstubAllEnvs() + vi.resetModules() + } + }) +}) diff --git a/packages/app/src/cli/services/validate/result.ts b/packages/app/src/cli/services/validate/result.ts new file mode 100644 index 00000000000..c3d4cb8fb6c --- /dev/null +++ b/packages/app/src/cli/services/validate/result.ts @@ -0,0 +1,26 @@ +import {appConfigValidateJsonOutputSchema, type AppConfigValidateResult} from './types.js' +import {formatConfigurationError} from '../../models/app/loader.js' +import {outputResult} from '@shopify/cli-kit/node/output' +import {renderError, renderSuccess} from '@shopify/cli-kit/node/ui' +import {AbortSilentError} from '@shopify/cli-kit/node/error' +import {basename} from '@shopify/cli-kit/node/path' + +export function renderAppConfigValidateResult( + result: AppConfigValidateResult, + configPath: string, + format: 'json' | 'text', +): void { + if (format === 'json') { + outputResult(appConfigValidateJsonOutputSchema.encode(result)) + } else if (result.valid) { + renderSuccess({headline: `App configuration '${basename(configPath)}' is valid.`}) + } else { + renderError({ + headline: 'Validation errors found.', + body: result.issues + .map((issue) => `• ${formatConfigurationError({...issue, file: issue.file ?? configPath})}`) + .join('\n'), + }) + } + if (!result.valid) throw new AbortSilentError() +} diff --git a/packages/app/src/cli/services/validate/types.ts b/packages/app/src/cli/services/validate/types.ts new file mode 100644 index 00000000000..2a21ac06f97 --- /dev/null +++ b/packages/app/src/cli/services/validate/types.ts @@ -0,0 +1,18 @@ +import {defineJsonOutputSchema, type InferJsonOutputSchema} from '@shopify/cli-kit/node/json-output-schema' +import {zod} from '@shopify/cli-kit/node/schema' + +// Keep field order aligned with the existing validation JSON producer. +const validationIssueSchema = zod.object({ + file: zod.string().optional(), + message: zod.string(), + path: zod.array(zod.union([zod.string(), zod.number()])).optional(), + code: zod.string().optional(), +}) + +export const appConfigValidateJsonOutputSchema = defineJsonOutputSchema({ + name: 'AppConfigValidateResult', + schema: zod.object({valid: zod.boolean(), issues: zod.array(validationIssueSchema)}), + definitions: {ValidationIssue: validationIssueSchema}, +}) + +export type AppConfigValidateResult = 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..5908572c63a 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -592,6 +592,63 @@ DESCRIPTION Validates the selected app configuration file and all extension configurations against their schemas and reports any errors found. + + Output from `--json` conforms to the `AppConfigValidateResult` schema. + + Use `--json-schema` to print the result, error, and event schemas. + + ```json + { + "type": "object", + "properties": { + "valid": { + "type": "boolean" + }, + "issues": { + "type": "array", + "items": { + "$ref": "#/definitions/ValidationIssue" + } + } + }, + "required": [ + "valid", + "issues" + ], + "additionalProperties": false, + "title": "AppConfigValidateResult", + "definitions": { + "ValidationIssue": { + "type": "object", + "properties": { + "file": { + "type": "string" + }, + "message": { + "type": "string" + }, + "path": { + "type": "array", + "items": { + "type": [ + "string", + "number" + ] + } + }, + "code": { + "type": "string" + } + }, + "required": [ + "message" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + } + ``` ``` ## `shopify app deploy` diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index 1b88620390e..0efd5ee17e9 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -804,7 +804,7 @@ "args": { }, "customPluginName": "@shopify/app", - "description": "Validates the selected app configuration file and all extension configurations against their schemas and reports any errors found.", + "description": "Validates the selected app configuration file and all extension configurations against their schemas and reports any errors found.\n\nOutput from `--json` conforms to the `AppConfigValidateResult` schema.\n\nUse `--json-schema` to print the result, error, and event schemas.\n\n```json\n{\n \"type\": \"object\",\n \"properties\": {\n \"valid\": {\n \"type\": \"boolean\"\n },\n \"issues\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/ValidationIssue\"\n }\n }\n },\n \"required\": [\n \"valid\",\n \"issues\"\n ],\n \"additionalProperties\": false,\n \"title\": \"AppConfigValidateResult\",\n \"definitions\": {\n \"ValidationIssue\": {\n \"type\": \"object\",\n \"properties\": {\n \"file\": {\n \"type\": \"string\"\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"path\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": [\n \"string\",\n \"number\"\n ]\n }\n },\n \"code\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"message\"\n ],\n \"additionalProperties\": false\n }\n },\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n}\n```", "descriptionWithMarkdown": "Validates the selected app configuration file and all extension configurations against their schemas and reports any errors found.", "flags": { "auth-alias": { 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..037ce1a3758 100644 --- a/packages/eslint-plugin-cli/rules/json-output-command-exceptions.js +++ b/packages/eslint-plugin-cli/rules/json-output-command-exceptions.js @@ -10,7 +10,6 @@ const commandExceptions = [ '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/config/validate.ts', 'packages/app/src/cli/commands/app/deploy.ts', 'packages/app/src/cli/commands/app/dev/clean.ts', 'packages/app/src/cli/commands/app/doctor.ts',