From f1b82504aaccccfde266a9f2d3313febe2b7bbb8 Mon Sep 17 00:00:00 2001 From: Gonzalo Riestra Date: Tue, 6 Oct 2026 11:10:02 +0200 Subject: [PATCH 1/2] Recommend --file-name for conflicting app config link flags --- .changeset/config-link-flag-error.md | 5 +++ .../src/cli/commands/app/config/link.test.ts | 32 ++++++++++++++++++- .../app/src/cli/commands/app/config/link.ts | 17 +++++++++- packages/cli/README.md | 4 +-- packages/cli/oclif.manifest.json | 3 -- 5 files changed, 54 insertions(+), 7 deletions(-) create mode 100644 .changeset/config-link-flag-error.md diff --git a/.changeset/config-link-flag-error.md b/.changeset/config-link-flag-error.md new file mode 100644 index 00000000000..64e9d77f151 --- /dev/null +++ b/.changeset/config-link-flag-error.md @@ -0,0 +1,5 @@ +--- +'@shopify/app': patch +--- + +Recommend `--file-name` when `app config link` receives both `--config` and `--client-id` diff --git a/packages/app/src/cli/commands/app/config/link.test.ts b/packages/app/src/cli/commands/app/config/link.test.ts index d1c976be5fd..8c53f9095a1 100644 --- a/packages/app/src/cli/commands/app/config/link.test.ts +++ b/packages/app/src/cli/commands/app/config/link.test.ts @@ -2,9 +2,10 @@ import ConfigLink from './link.js' import link from '../../../services/app/config/link.js' import {linkedAppContext} from '../../../services/app-context.js' import {testAppLinked, testOrganizationApp} from '../../../models/app/app.test-data.js' +import {Config} from '@oclif/core' import {inTemporaryDirectory} from '@shopify/cli-kit/node/fs' import {terminalSupportsPrompting} from '@shopify/cli-kit/node/system' -import {beforeEach, describe, expect, test, vi} from 'vitest' +import {afterEach, beforeEach, describe, expect, test, vi} from 'vitest' vi.mock('../../../services/app/config/link.js') vi.mock('../../../services/app-context.js') @@ -15,6 +16,35 @@ describe('app config link command', () => { vi.mocked(link).mockReset() vi.mocked(linkedAppContext).mockReset() vi.mocked(terminalSupportsPrompting).mockReturnValue(true) + vi.stubEnv('SHOPIFY_FLAG_APP_CONFIG', undefined) + vi.stubEnv('SHOPIFY_FLAG_CLIENT_ID', undefined) + }) + + afterEach(() => { + vi.unstubAllEnvs() + }) + + test.each([ + {name: 'both flags', args: ['--config', 'staging', '--client-id', 'api-key']}, + {name: 'the short config flag', args: ['-c', 'staging', '--client-id', 'api-key']}, + {name: 'an empty config value', args: ['--config', '', '--client-id', 'api-key']}, + {name: 'config from the environment', args: ['--client-id', 'api-key'], config: 'staging'}, + {name: 'client ID from the environment', args: ['--config', 'staging'], clientId: 'api-key'}, + {name: 'both environment variables', args: [], config: 'staging', clientId: 'api-key'}, + ])('recommends --file-name when using $name', async ({args, config, clientId}) => { + vi.stubEnv('SHOPIFY_FLAG_APP_CONFIG', config) + vi.stubEnv('SHOPIFY_FLAG_CLIENT_ID', clientId) + + await inTemporaryDirectory(async (tmp) => { + const command = new ConfigLink(['--path', tmp, ...args], await Config.load(import.meta.url)) + await expect(command.run()).rejects.toMatchObject({ + message: "The --config and --client-id flags can't be used together.", + tryMessage: 'Use --file-name instead of --config to choose the configuration file to create or overwrite.', + }) + + expect(link).not.toHaveBeenCalled() + expect(linkedAppContext).not.toHaveBeenCalled() + }) }) test('accepts --client-id with --file-name to link a specific app to a specific config file', async () => { diff --git a/packages/app/src/cli/commands/app/config/link.ts b/packages/app/src/cli/commands/app/config/link.ts index ded25562bc5..c1cc2d6102d 100644 --- a/packages/app/src/cli/commands/app/config/link.ts +++ b/packages/app/src/cli/commands/app/config/link.ts @@ -4,6 +4,7 @@ import link, {LinkOptions} from '../../../services/app/config/link.js' import AppLinkedCommand, {AppLinkedCommandOutput} from '../../../utilities/app-linked-command.js' import {Flags} from '@oclif/core' import {globalFlags, requiredIfNonInteractive} from '@shopify/cli-kit/node/cli' +import {AbortError} from '@shopify/cli-kit/node/error' export default class ConfigLink extends AppLinkedCommand { static summary = 'Fetch your app configuration from the Developer Dashboard.' @@ -23,7 +24,14 @@ export default class ConfigLink extends AppLinkedCommand { env: 'SHOPIFY_FLAG_ORGANIZATION_ID', exclusive: ['client-id'], }), - 'client-id': requiredIfNonInteractive(appFlags['client-id']), + // Validate this conflict in run() so we can recommend --file-name. + 'client-id': requiredIfNonInteractive( + Flags.string({ + hidden: false, + description: 'The Client ID of your app.', + env: 'SHOPIFY_FLAG_CLIENT_ID', + }), + ), 'file-name': Flags.string({ hidden: false, description: 'The name of the app configuration file to create or overwrite.', @@ -41,6 +49,13 @@ export default class ConfigLink extends AppLinkedCommand { public async run(): Promise { const {flags} = await this.parse(ConfigLink) + if (flags.config !== undefined && flags['client-id'] !== undefined) { + throw new AbortError( + "The --config and --client-id flags can't be used together.", + 'Use --file-name instead of --config to choose the configuration file to create or overwrite.', + ) + } + const options: LinkOptions = { directory: flags.path, apiKey: flags['client-id'], diff --git a/packages/cli/README.md b/packages/cli/README.md index 13bffaee1a7..30c4b75d5e2 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -410,8 +410,8 @@ Fetch your app configuration from the Developer Dashboard. ``` USAGE - $ shopify app config link [--auth-alias ] [--client-id | -c ] [--force [--file-name | - ]] [--json-schema] [--no-color] [--no-input] [--path ] [--reset | ] [--verbose] + $ shopify app config link [--auth-alias ] [--client-id ] [--force [--file-name | -c ]] + [--json-schema] [--no-color] [--no-input] [--path ] [--reset | ] [--verbose] FLAGS -c, --config= diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index 20822604f7b..d3a0dc2995e 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -538,9 +538,6 @@ "client-id": { "description": "The Client ID of your app. Required if non interactive.", "env": "SHOPIFY_FLAG_CLIENT_ID", - "exclusive": [ - "config" - ], "hasDynamicHelp": false, "hidden": false, "multiple": false, From c70751ada302757c51bcf58b018cae7b7953c4cc Mon Sep 17 00:00:00 2001 From: Gonzalo Riestra Date: Tue, 6 Oct 2026 11:20:23 +0200 Subject: [PATCH 2/2] Clarify app config link flag descriptions --- .changeset/config-link-flag-error.md | 2 +- .../generated/generated_docs_data_v2.json | 8 ++++---- packages/app/src/cli/commands/app/config/link.ts | 13 +++++++++++-- packages/cli/README.md | 7 ++++--- packages/cli/oclif.manifest.json | 6 +++--- 5 files changed, 23 insertions(+), 13 deletions(-) diff --git a/.changeset/config-link-flag-error.md b/.changeset/config-link-flag-error.md index 64e9d77f151..a5b6025d08b 100644 --- a/.changeset/config-link-flag-error.md +++ b/.changeset/config-link-flag-error.md @@ -2,4 +2,4 @@ '@shopify/app': patch --- -Recommend `--file-name` when `app config link` receives both `--config` and `--client-id` +Clarify `app config link` flags and recommend `--file-name` when both `--config` and `--client-id` are provided diff --git a/docs-shopify.dev/generated/generated_docs_data_v2.json b/docs-shopify.dev/generated/generated_docs_data_v2.json index 9931a16c7bf..8efcf7e4d62 100644 --- a/docs-shopify.dev/generated/generated_docs_data_v2.json +++ b/docs-shopify.dev/generated/generated_docs_data_v2.json @@ -504,7 +504,7 @@ "syntaxKind": "PropertySignature", "name": "--client-id ", "value": "string", - "description": "The Client ID of your app. Required if non interactive.", + "description": "The Client ID of the remote app to link. Use --file-name to specify the destination configuration file. Required if non interactive.", "isOptional": true, "environmentValue": "SHOPIFY_FLAG_CLIENT_ID" }, @@ -513,7 +513,7 @@ "syntaxKind": "PropertySignature", "name": "--file-name ", "value": "string", - "description": "The name of the app configuration file to create or overwrite.", + "description": "The name of the app configuration file to create or overwrite. Requires --force to overwrite an existing file.", "isOptional": true, "environmentValue": "SHOPIFY_FLAG_APP_CONFIG_FILE_NAME" }, @@ -585,12 +585,12 @@ "syntaxKind": "PropertySignature", "name": "-c, --config ", "value": "string", - "description": "The name of the app configuration.", + "description": "The name of the local app configuration to read and overwrite. Use --file-name instead when specifying --client-id.", "isOptional": true, "environmentValue": "SHOPIFY_FLAG_APP_CONFIG" } ], - "value": "export interface appconfiglink {\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. Required if non interactive.\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 * The name of the app configuration file to create or overwrite.\n * @environment SHOPIFY_FLAG_APP_CONFIG_FILE_NAME\n */\n '--file-name '?: string\n\n /**\n * Overwrite an existing configuration file without prompting.\n * @environment SHOPIFY_FLAG_FORCE\n */\n '--force'?: ''\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 * 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 appconfiglink {\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 the remote app to link. Use --file-name to specify the destination configuration file. Required if non interactive.\n * @environment SHOPIFY_FLAG_CLIENT_ID\n */\n '--client-id '?: string\n\n /**\n * The name of the local app configuration to read and overwrite. Use --file-name instead when specifying --client-id.\n * @environment SHOPIFY_FLAG_APP_CONFIG\n */\n '-c, --config '?: string\n\n /**\n * The name of the app configuration file to create or overwrite. Requires --force to overwrite an existing file.\n * @environment SHOPIFY_FLAG_APP_CONFIG_FILE_NAME\n */\n '--file-name '?: string\n\n /**\n * Overwrite an existing configuration file without prompting.\n * @environment SHOPIFY_FLAG_FORCE\n */\n '--force'?: ''\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 * 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}" } }, "appconfigpull": { diff --git a/packages/app/src/cli/commands/app/config/link.ts b/packages/app/src/cli/commands/app/config/link.ts index c1cc2d6102d..75d24f612bc 100644 --- a/packages/app/src/cli/commands/app/config/link.ts +++ b/packages/app/src/cli/commands/app/config/link.ts @@ -19,6 +19,13 @@ export default class ConfigLink extends AppLinkedCommand { static flags = { ...globalFlags, ...appFlags, + config: Flags.string({ + hidden: false, + char: 'c', + description: + 'The name of the local app configuration to read and overwrite. Use --file-name instead when specifying --client-id.', + env: 'SHOPIFY_FLAG_APP_CONFIG', + }), 'organization-id': Flags.string({ hidden: true, env: 'SHOPIFY_FLAG_ORGANIZATION_ID', @@ -28,13 +35,15 @@ export default class ConfigLink extends AppLinkedCommand { 'client-id': requiredIfNonInteractive( Flags.string({ hidden: false, - description: 'The Client ID of your app.', + description: + 'The Client ID of the remote app to link. Use --file-name to specify the destination configuration file.', env: 'SHOPIFY_FLAG_CLIENT_ID', }), ), 'file-name': Flags.string({ hidden: false, - description: 'The name of the app configuration file to create or overwrite.', + description: + 'The name of the app configuration file to create or overwrite. Requires --force to overwrite an existing file.', env: 'SHOPIFY_FLAG_APP_CONFIG_FILE_NAME', exclusive: ['config'], }), diff --git a/packages/cli/README.md b/packages/cli/README.md index 30c4b75d5e2..629fa8ea8ca 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -415,7 +415,7 @@ USAGE FLAGS -c, --config= - The name of the app configuration. + The name of the local app configuration to read and overwrite. Use --file-name instead when specifying --client-id. [env: SHOPIFY_FLAG_APP_CONFIG] --auth-alias= @@ -423,11 +423,12 @@ FLAGS [env: SHOPIFY_FLAG_AUTH_ALIAS] --client-id= - The Client ID of your app. Required if non interactive. + The Client ID of the remote app to link. Use --file-name to specify the destination configuration file. Required if + non interactive. [env: SHOPIFY_FLAG_CLIENT_ID] --file-name= - The name of the app configuration file to create or overwrite. + The name of the app configuration file to create or overwrite. Requires --force to overwrite an existing file. [env: SHOPIFY_FLAG_APP_CONFIG_FILE_NAME] --force diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index d3a0dc2995e..a65831f96fc 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -536,7 +536,7 @@ "type": "option" }, "client-id": { - "description": "The Client ID of your app. Required if non interactive.", + "description": "The Client ID of the remote app to link. Use --file-name to specify the destination configuration file. Required if non interactive.", "env": "SHOPIFY_FLAG_CLIENT_ID", "hasDynamicHelp": false, "hidden": false, @@ -546,7 +546,7 @@ }, "config": { "char": "c", - "description": "The name of the app configuration.", + "description": "The name of the local app configuration to read and overwrite. Use --file-name instead when specifying --client-id.", "env": "SHOPIFY_FLAG_APP_CONFIG", "hasDynamicHelp": false, "hidden": false, @@ -555,7 +555,7 @@ "type": "option" }, "file-name": { - "description": "The name of the app configuration file to create or overwrite.", + "description": "The name of the app configuration file to create or overwrite. Requires --force to overwrite an existing file.", "env": "SHOPIFY_FLAG_APP_CONFIG_FILE_NAME", "exclusive": [ "config"