From fe5df53c2de263a714fbd1bb31511b75abe83392 Mon Sep 17 00:00:00 2001 From: Gonzalo Riestra Date: Fri, 28 Aug 2026 14:35:48 +0200 Subject: [PATCH 1/2] [Do not merge] Add JSON infrastructure sample command --- packages/cli/oclif.manifest.json | 43 +++++++++++++++++ .../commands/kitchen-sink/json-output.test.ts | 46 +++++++++++++++++++ .../cli/commands/kitchen-sink/json-output.ts | 41 +++++++++++++++++ .../services/kitchen-sink/json-output.test.ts | 20 ++++++++ .../cli/services/kitchen-sink/json-output.ts | 22 +++++++++ packages/cli/src/index.ts | 2 + 6 files changed, 174 insertions(+) create mode 100644 packages/cli/src/cli/commands/kitchen-sink/json-output.test.ts create mode 100644 packages/cli/src/cli/commands/kitchen-sink/json-output.ts create mode 100644 packages/cli/src/cli/services/kitchen-sink/json-output.test.ts create mode 100644 packages/cli/src/cli/services/kitchen-sink/json-output.ts diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index 66513bcce84..ad156bafddd 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -6343,6 +6343,49 @@ "pluginType": "core", "strict": true }, + "kitchen-sink:json-output": { + "aliases": [ + ], + "args": { + }, + "description": "Exercise command JSON output infrastructure.\n\nOutput from `--json` conforms to the `KitchenSinkJsonOutputResult` schema.\n\nUse `--json-schema` to print the schema directly:\n\n```ts\ninterface KitchenSinkJsonOutputResult {\n items: SampleItem[]\n}\n\ninterface SampleItem {\n id: number\n name: string\n}\n```", + "descriptionWithMarkdown": "Exercise command JSON output infrastructure.", + "enableJsonFlag": false, + "flags": { + "fail": { + "allowNo": false, + "description": "Fail with a sample error.", + "env": "SHOPIFY_FLAG_FAIL", + "name": "fail", + "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 output schema.", + "env": "SHOPIFY_FLAG_JSON_SCHEMA", + "name": "json-schema", + "type": "boolean" + } + }, + "hasDynamicHelp": false, + "hidden": true, + "hiddenAliases": [ + ], + "id": "kitchen-sink:json-output", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true + }, "kitchen-sink:prompts": { "aliases": [ ], diff --git a/packages/cli/src/cli/commands/kitchen-sink/json-output.test.ts b/packages/cli/src/cli/commands/kitchen-sink/json-output.test.ts new file mode 100644 index 00000000000..0ec5157fdf2 --- /dev/null +++ b/packages/cli/src/cli/commands/kitchen-sink/json-output.test.ts @@ -0,0 +1,46 @@ +import KitchenSinkJsonOutput from './json-output.js' +import {kitchenSinkJsonOutputSchema} from '../../services/kitchen-sink/json-output.js' +import {mockAndCaptureOutput} from '@shopify/cli-kit/node/testing/output' +import {Config} from '@oclif/core' +import {afterEach, describe, expect, test} from 'vitest' +import {fileURLToPath} from 'node:url' + +afterEach(() => { + mockAndCaptureOutput().clear() +}) + +describe('kitchen-sink json-output command', () => { + test('prints the validated JSON result', async () => { + const output = mockAndCaptureOutput() + await KitchenSinkJsonOutput.run(['--json'], import.meta.url) + + const sideEvents = output + .info() + .split('\n') + .slice(0, 3) + .map((line) => JSON.parse(line) as unknown) + expect(sideEvents).toMatchObject([ + {type: 'progress', message: 'Preparing the sample result'}, + {type: 'diagnostic', level: 'info', message: 'Preparing the sample result.'}, + {type: 'progress', message: 'Preparing the sample result', current: 1, total: 1}, + ]) + expect(output.info()).toContain(kitchenSinkJsonOutputSchema.encode({items: [{id: 1, name: 'Example'}]})) + }) + + test('prints a human-readable result', async () => { + const output = mockAndCaptureOutput() + await KitchenSinkJsonOutput.run([], import.meta.url) + + expect(output.info()).toContain('Prepared 1 item.') + }) + + test('exposes its JSON schema', () => { + expect(KitchenSinkJsonOutput.jsonOutputSchema).toBe(kitchenSinkJsonOutputSchema) + }) + + test('can exercise JSON error handling', async () => { + const config = await Config.load(fileURLToPath(import.meta.url)) + + await expect(new KitchenSinkJsonOutput(['--fail'], config).run()).rejects.toThrow('Sample command failure.') + }) +}) diff --git a/packages/cli/src/cli/commands/kitchen-sink/json-output.ts b/packages/cli/src/cli/commands/kitchen-sink/json-output.ts new file mode 100644 index 00000000000..22e22377ebf --- /dev/null +++ b/packages/cli/src/cli/commands/kitchen-sink/json-output.ts @@ -0,0 +1,41 @@ +import {createKitchenSinkJsonOutput, kitchenSinkJsonOutputSchema} from '../../services/kitchen-sink/json-output.js' +import Command from '@shopify/cli-kit/node/base-command' +import {jsonFlag} from '@shopify/cli-kit/node/cli' +import {AbortError} from '@shopify/cli-kit/node/error' +import {outputContent, outputResult, outputWarn} from '@shopify/cli-kit/node/output' +import {renderSingleTask} from '@shopify/cli-kit/node/ui' +import {Flags} from '@oclif/core' + +export default class KitchenSinkJsonOutput extends Command { + static descriptionWithMarkdown = 'Exercise command JSON output infrastructure.' + static description = this.descriptionWithoutMarkdown() + static hidden = true + + static flags = { + ...jsonFlag, + fail: Flags.boolean({ + description: 'Fail with a sample error.', + env: 'SHOPIFY_FLAG_FAIL', + default: false, + }), + } + + static get jsonOutputSchema() { + return kitchenSinkJsonOutputSchema + } + + async run(): Promise { + const {flags} = await this.parse(KitchenSinkJsonOutput) + const result = await renderSingleTask({ + title: outputContent`Preparing the sample result`, + task: async () => createKitchenSinkJsonOutput(), + }) + + if (flags.fail) { + outputWarn('Failing as requested.') + throw new AbortError('Sample command failure.') + } + + outputResult(flags.json ? kitchenSinkJsonOutputSchema.encode(result) : `Prepared ${result.items.length} item.`) + } +} diff --git a/packages/cli/src/cli/services/kitchen-sink/json-output.test.ts b/packages/cli/src/cli/services/kitchen-sink/json-output.test.ts new file mode 100644 index 00000000000..60ff362b5b9 --- /dev/null +++ b/packages/cli/src/cli/services/kitchen-sink/json-output.test.ts @@ -0,0 +1,20 @@ +import {createKitchenSinkJsonOutput, kitchenSinkJsonOutputSchema} from './json-output.js' +import {runWithCommandEvents} from '@shopify/cli-kit/node/command-events' +import {describe, expect, test, vi} from 'vitest' + +describe('kitchen sink JSON output service', () => { + test('returns a valid result and reports its diagnostic', () => { + const sink = vi.fn() + + const result = runWithCommandEvents({sink, outputMode: 'json'}, createKitchenSinkJsonOutput) + + expect(kitchenSinkJsonOutputSchema.validate(result)).toEqual({items: [{id: 1, name: 'Example'}]}) + expect(sink).toHaveBeenCalledOnce() + expect(sink).toHaveBeenCalledWith({ + type: 'diagnostic', + timestamp: expect.any(String), + level: 'info', + message: 'Preparing the sample result.', + }) + }) +}) diff --git a/packages/cli/src/cli/services/kitchen-sink/json-output.ts b/packages/cli/src/cli/services/kitchen-sink/json-output.ts new file mode 100644 index 00000000000..e21d3063531 --- /dev/null +++ b/packages/cli/src/cli/services/kitchen-sink/json-output.ts @@ -0,0 +1,22 @@ +import {defineJsonOutputSchema, type InferJsonOutputSchema} from '@shopify/cli-kit/node/json-output-schema' +import {zod} from '@shopify/cli-kit/node/schema' +import {outputInfo} from '@shopify/cli-kit/node/output' + +const SampleItemSchema = zod.object({ + id: zod.number(), + name: zod.string(), +}) + +export const kitchenSinkJsonOutputSchema = defineJsonOutputSchema({ + name: 'KitchenSinkJsonOutputResult', + schema: zod.object({items: zod.array(SampleItemSchema)}), + definitions: {SampleItem: SampleItemSchema}, +}) + +type KitchenSinkJsonOutputResult = InferJsonOutputSchema + +export function createKitchenSinkJsonOutput(): KitchenSinkJsonOutputResult { + outputInfo('Preparing the sample result.') + + return {items: [{id: 1, name: 'Example'}]} +} diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index a7a087bd4ce..82c1ab2ab6e 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -6,6 +6,7 @@ import Logout from './cli/commands/auth/logout.js' import Login from './cli/commands/auth/login.js' import CommandFlags from './cli/commands/debug/command-flags.js' import KitchenSinkAsync from './cli/commands/kitchen-sink/async.js' +import KitchenSinkJsonOutput from './cli/commands/kitchen-sink/json-output.js' import KitchenSinkPrompts from './cli/commands/kitchen-sink/prompts.js' import KitchenSinkStatic from './cli/commands/kitchen-sink/static.js' import KitchenSink from './cli/commands/kitchen-sink/index.js' @@ -151,6 +152,7 @@ export const COMMANDS: any = { 'debug:command-flags': CommandFlags, 'kitchen-sink': KitchenSink, 'kitchen-sink:async': KitchenSinkAsync, + 'kitchen-sink:json-output': KitchenSinkJsonOutput, 'kitchen-sink:prompts': KitchenSinkPrompts, 'kitchen-sink:static': KitchenSinkStatic, 'doctor-release': Doctor, From cf157152b744b5c70b85fcd6f1ee4b6887878935 Mon Sep 17 00:00:00 2001 From: Gonzalo Riestra Date: Fri, 28 Aug 2026 16:25:58 +0200 Subject: [PATCH 2/2] Refresh OCLIF manifest --- packages/cli/oclif.manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index ad156bafddd..9e4fd13a312 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -6370,7 +6370,7 @@ }, "json-schema": { "allowNo": false, - "description": "Print the command's JSON output schema.", + "description": "Print the command's JSON schemas.", "env": "SHOPIFY_FLAG_JSON_SCHEMA", "name": "json-schema", "type": "boolean"