From 6971de59241c978c1552fc8b27ee593a68149c92 Mon Sep 17 00:00:00 2001 From: Nick Wesselman <27013789+nickwesselman@users.noreply.github.com> Date: Thu, 18 Dec 2025 09:02:13 -0500 Subject: [PATCH 1/2] remove STDIN support on execute commands --- .../app/src/cli/commands/app/bulk/execute.ts | 2 +- packages/app/src/cli/commands/app/execute.ts | 2 +- packages/app/src/cli/flags.ts | 10 ++-- .../utilities/execute-command-helpers.test.ts | 47 +------------------ .../cli/utilities/execute-command-helpers.ts | 18 ++----- packages/cli/oclif.manifest.json | 10 +--- 6 files changed, 16 insertions(+), 73 deletions(-) diff --git a/packages/app/src/cli/commands/app/bulk/execute.ts b/packages/app/src/cli/commands/app/bulk/execute.ts index 92857c6916..8ea3f21daf 100644 --- a/packages/app/src/cli/commands/app/bulk/execute.ts +++ b/packages/app/src/cli/commands/app/bulk/execute.ts @@ -21,7 +21,7 @@ export default class BulkExecute extends AppLinkedCommand { async run(): Promise { const {flags} = await this.parse(BulkExecute) - const {query, appContextResult, store} = await prepareExecuteContext(flags, 'bulk execute') + const {query, appContextResult, store} = await prepareExecuteContext(flags) await executeBulkOperation({ organization: appContextResult.organization, diff --git a/packages/app/src/cli/commands/app/execute.ts b/packages/app/src/cli/commands/app/execute.ts index 5a72509861..743e7cf565 100644 --- a/packages/app/src/cli/commands/app/execute.ts +++ b/packages/app/src/cli/commands/app/execute.ts @@ -20,7 +20,7 @@ export default class Execute extends AppLinkedCommand { async run(): Promise { const {flags} = await this.parse(Execute) - const {query, appContextResult, store} = await prepareExecuteContext(flags, 'execute') + const {query, appContextResult, store} = await prepareExecuteContext(flags) await executeOperation({ organization: appContextResult.organization, diff --git a/packages/app/src/cli/flags.ts b/packages/app/src/cli/flags.ts index 46a5ebb8cd..d9a9776380 100644 --- a/packages/app/src/cli/flags.ts +++ b/packages/app/src/cli/flags.ts @@ -38,16 +38,16 @@ export const appFlags = { export const bulkOperationFlags = { query: Flags.string({ char: 'q', - description: 'The GraphQL query or mutation to run as a bulk operation. If omitted, reads from standard input.', + description: 'The GraphQL query or mutation to run as a bulk operation.', env: 'SHOPIFY_FLAG_QUERY', required: false, - exclusive: ['query-file'], + exactlyOne: ['query', 'query-file'], }), 'query-file': Flags.string({ description: "Path to a file containing the GraphQL query or mutation. Can't be used with --query.", env: 'SHOPIFY_FLAG_QUERY_FILE', parse: async (input) => resolvePath(input), - exclusive: ['query'], + exactlyOne: ['query', 'query-file'], }), variables: Flags.string({ char: 'v', @@ -92,13 +92,13 @@ export const operationFlags = { description: 'The GraphQL query or mutation, as a string.', env: 'SHOPIFY_FLAG_QUERY', required: false, - exclusive: ['query-file'], + exactlyOne: ['query', 'query-file'], }), 'query-file': Flags.string({ description: "Path to a file containing the GraphQL query or mutation. Can't be used with --query.", env: 'SHOPIFY_FLAG_QUERY_FILE', parse: async (input) => resolvePath(input), - exclusive: ['query'], + exactlyOne: ['query', 'query-file'], }), variables: Flags.string({ char: 'v', diff --git a/packages/app/src/cli/utilities/execute-command-helpers.test.ts b/packages/app/src/cli/utilities/execute-command-helpers.test.ts index 005975e5bd..d4bf76e4ee 100644 --- a/packages/app/src/cli/utilities/execute-command-helpers.test.ts +++ b/packages/app/src/cli/utilities/execute-command-helpers.test.ts @@ -3,7 +3,6 @@ import {linkedAppContext} from '../services/app-context.js' import {storeContext} from '../services/store-context.js' import {validateSingleOperation} from '../services/graphql/common.js' import {readFile, fileExists} from '@shopify/cli-kit/node/fs' -import {readStdinString} from '@shopify/cli-kit/node/system' import {describe, test, expect, vi, beforeEach} from 'vitest' vi.mock('../services/app-context.js') @@ -116,48 +115,18 @@ describe('prepareExecuteContext', () => { beforeEach(() => { vi.mocked(linkedAppContext).mockResolvedValue(mockAppContextResult as any) vi.mocked(storeContext).mockResolvedValue(mockStore as any) - vi.mocked(readStdinString).mockResolvedValue('') }) test('uses query from flags when provided', async () => { const result = await prepareExecuteContext(mockFlags) expect(result.query).toBe(mockFlags.query) - expect(readStdinString).not.toHaveBeenCalled() }) - test('reads query from stdin when flag not provided', async () => { - const stdinQuery = 'query { products { edges { node { id } } } }' - vi.mocked(readStdinString).mockResolvedValue(stdinQuery) - - const flagsWithoutQuery = {...mockFlags, query: undefined} - const result = await prepareExecuteContext(flagsWithoutQuery) - - expect(readStdinString).toHaveBeenCalled() - expect(result.query).toBe(stdinQuery) - }) - - test('throws AbortError when no query provided via flag or stdin', async () => { - vi.mocked(readStdinString).mockResolvedValue('') - - const flagsWithoutQuery = {...mockFlags, query: undefined} - - await expect(prepareExecuteContext(flagsWithoutQuery)).rejects.toThrow('No query provided') - }) - - test('includes command name in error message', async () => { - vi.mocked(readStdinString).mockResolvedValue('') - + test('throws BugError when no query provided', async () => { const flagsWithoutQuery = {...mockFlags, query: undefined} - try { - await prepareExecuteContext(flagsWithoutQuery, 'bulk execute') - expect.fail('Should have thrown an error') - // eslint-disable-next-line no-catch-all/no-catch-all - } catch (error: any) { - expect(error.message).toContain('No query provided') - expect(error.tryMessage).toMatch(/shopify app bulk execute/) - } + await expect(prepareExecuteContext(flagsWithoutQuery)).rejects.toThrow('exactlyOne constraint') }) test('returns query, app context, and store', async () => { @@ -188,7 +157,6 @@ describe('prepareExecuteContext', () => { expect(fileExists).toHaveBeenCalledWith('/path/to/query.graphql') expect(readFile).toHaveBeenCalledWith('/path/to/query.graphql', {encoding: 'utf8'}) expect(result.query).toBe(queryFileContent) - expect(readStdinString).not.toHaveBeenCalled() }) test('throws AbortError when query file does not exist', async () => { @@ -200,17 +168,6 @@ describe('prepareExecuteContext', () => { expect(readFile).not.toHaveBeenCalled() }) - test('falls back to stdin when neither query nor query-file provided', async () => { - const stdinQuery = 'query { shop { name } }' - vi.mocked(readStdinString).mockResolvedValue(stdinQuery) - - const flagsWithoutQueryOrFile = {...mockFlags, query: undefined} - const result = await prepareExecuteContext(flagsWithoutQueryOrFile) - - expect(readStdinString).toHaveBeenCalled() - expect(result.query).toBe(stdinQuery) - }) - test('validates GraphQL query using validateSingleOperation', async () => { await prepareExecuteContext(mockFlags) diff --git a/packages/app/src/cli/utilities/execute-command-helpers.ts b/packages/app/src/cli/utilities/execute-command-helpers.ts index 26c43882f7..1d6780efb2 100644 --- a/packages/app/src/cli/utilities/execute-command-helpers.ts +++ b/packages/app/src/cli/utilities/execute-command-helpers.ts @@ -2,8 +2,7 @@ import {linkedAppContext, LoadedAppContextOutput} from '../services/app-context. import {storeContext} from '../services/store-context.js' import {validateSingleOperation} from '../services/graphql/common.js' import {OrganizationStore} from '../models/organization.js' -import {readStdinString} from '@shopify/cli-kit/node/system' -import {AbortError} from '@shopify/cli-kit/node/error' +import {AbortError, BugError} from '@shopify/cli-kit/node/error' import {readFile, fileExists} from '@shopify/cli-kit/node/fs' import {outputContent, outputToken} from '@shopify/cli-kit/node/output' @@ -55,16 +54,12 @@ export async function prepareAppStoreContext(flags: AppStoreContextFlags): Promi /** * Prepares the execution context for GraphQL operations. - * Handles query input from flag, file, or stdin, validates GraphQL syntax, and sets up app and store contexts. + * Handles query input from flag or file, validates GraphQL syntax, and sets up app and store contexts. * * @param flags - Command flags containing configuration options. - * @param commandName - Name of the command for error messages (e.g., 'execute', 'bulk execute'). * @returns Context object containing query, app context, and store information. */ -export async function prepareExecuteContext( - flags: ExecuteCommandFlags, - commandName = 'execute', -): Promise { +export async function prepareExecuteContext(flags: ExecuteCommandFlags): Promise { let query: string | undefined if (flags.query) { @@ -77,14 +72,11 @@ export async function prepareExecuteContext( ) } query = await readFile(queryFile, {encoding: 'utf8'}) - } else { - query = await readStdinString() } if (!query) { - throw new AbortError( - 'No query provided. Use the --query flag, --query-file flag, or pipe input via stdin.', - `Example: shopify app ${commandName} --query-file query.graphql`, + throw new BugError( + 'Query should have been provided via --query or --query-file flags due to exactlyOne constraint. This indicates the oclif flag validation failed.', ) } diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index 25db767f42..e9d87a9e9f 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -146,7 +146,7 @@ }, "query": { "char": "q", - "description": "The GraphQL query or mutation to run as a bulk operation. If omitted, reads from standard input.", + "description": "The GraphQL query or mutation to run as a bulk operation.", "env": "SHOPIFY_FLAG_QUERY", "exclusive": [ "query-file" @@ -160,9 +160,6 @@ "query-file": { "description": "Path to a file containing the GraphQL query or mutation. Can't be used with --query.", "env": "SHOPIFY_FLAG_QUERY_FILE", - "exclusive": [ - "query" - ], "hasDynamicHelp": false, "multiple": false, "name": "query-file", @@ -1212,9 +1209,6 @@ "query-file": { "description": "Path to a file containing the GraphQL query or mutation. Can't be used with --query.", "env": "SHOPIFY_FLAG_QUERY_FILE", - "exclusive": [ - "query" - ], "hasDynamicHelp": false, "multiple": false, "name": "query-file", @@ -7602,4 +7596,4 @@ } }, "version": "3.88.0" -} \ No newline at end of file +} From 6d211c78a7a86aa0188571c0d391f4a7648d0088 Mon Sep 17 00:00:00 2001 From: Nick Wesselman <27013789+nickwesselman@users.noreply.github.com> Date: Thu, 18 Dec 2025 09:10:16 -0500 Subject: [PATCH 2/2] refreshed manifests --- packages/cli/oclif.manifest.json | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index e9d87a9e9f..a465dc6a99 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -148,9 +148,6 @@ "char": "q", "description": "The GraphQL query or mutation to run as a bulk operation.", "env": "SHOPIFY_FLAG_QUERY", - "exclusive": [ - "query-file" - ], "hasDynamicHelp": false, "multiple": false, "name": "query", @@ -1197,9 +1194,6 @@ "char": "q", "description": "The GraphQL query or mutation, as a string.", "env": "SHOPIFY_FLAG_QUERY", - "exclusive": [ - "query-file" - ], "hasDynamicHelp": false, "multiple": false, "name": "query", @@ -7596,4 +7590,4 @@ } }, "version": "3.88.0" -} +} \ No newline at end of file