diff --git a/.changeset/brave-agents-login.md b/.changeset/brave-agents-login.md new file mode 100644 index 00000000000..8bc4d6a479d --- /dev/null +++ b/.changeset/brave-agents-login.md @@ -0,0 +1,6 @@ +--- +'@shopify/cli': patch +'@shopify/cli-kit': patch +--- + +Improve Shopify CLI login guidance for agent-driven authentication flows. diff --git a/docs/cli/auth.md b/docs/cli/auth.md new file mode 100644 index 00000000000..852923e4e12 --- /dev/null +++ b/docs/cli/auth.md @@ -0,0 +1,34 @@ +# Shopify CLI authentication + +Shopify CLI authenticates developers with Shopify through a device-code OAuth flow. This flow is designed to work in terminals, remote development environments, and agent-driven workflows. + +## Supported flow + +Shopify CLI currently supports user-driven device authentication: + +1. Check whether a session is already available with `shopify auth status` or `shopify auth status --json`. +2. Run a command that requires authentication, or run `shopify auth login` directly. +3. Shopify CLI prints a verification URL and user code, or opens the verification URL in your browser. +4. The user completes login in the browser. +5. Keep the CLI process running. It polls for completion and continues automatically after authentication succeeds. + +Agents should show the verification URL and user code to the user, ask the user to complete authentication in the browser, and wait for the CLI command to finish. + +## Commands + +- `shopify auth login`: Start an interactive Shopify account login. +- `shopify auth logout`: Clear the stored Shopify CLI session. +- `shopify auth status`: Check whether Shopify CLI has a usable Shopify account session. Use `--json` for machine-readable output. +- Commands that need authentication may start the same login flow automatically. + +## Non-interactive environments + +In CI or fully non-interactive environments, use credentials provided through the supported environment variables for the command you are running. Do not start an interactive browser login from CI. + +## Scopes + +Shopify CLI requests the scopes needed for CLI workflows, including access to Shopify Admin, Partners, Storefront Renderer, Business Platform, and App Management APIs. Individual commands may request additional scopes for the task being performed. + +## Support + +For issues with Shopify CLI authentication, see https://shopify.dev/docs/api/shopify-cli or contact Shopify support at https://help.shopify.com. diff --git a/packages/cli-kit/src/private/node/session/device-authorization.test.ts b/packages/cli-kit/src/private/node/session/device-authorization.test.ts index 32349ba0ee2..a12308fd892 100644 --- a/packages/cli-kit/src/private/node/session/device-authorization.test.ts +++ b/packages/cli-kit/src/private/node/session/device-authorization.test.ts @@ -12,6 +12,7 @@ import {isTTY} from '../../../public/node/ui.js' import {err, ok} from '../../../public/node/result.js' import {AbortError} from '../../../public/node/error.js' import {isCI} from '../../../public/node/system.js' +import {mockAndCaptureOutput} from '../../../public/node/testing/output.js' import {beforeEach, describe, expect, test, vi} from 'vitest' import {Response} from 'node-fetch' @@ -66,6 +67,27 @@ describe('requestDeviceAuthorization', () => { expect(got).toEqual(dataExpected) }) + test('prints explicit guidance for agents when the browser is not opened automatically', async () => { + // Given + const outputMock = mockAndCaptureOutput() + const response = new Response(JSON.stringify(data)) + vi.mocked(shopifyFetch).mockResolvedValue(response) + vi.mocked(identityFqdn).mockResolvedValue('fqdn.com') + vi.mocked(clientId).mockReturnValue('clientId') + vi.mocked(isTTY).mockReturnValue(false) + + // When + await requestDeviceAuthorization(['scope1', 'scope2']) + + // Then + expect(outputMock.output()).toContain('User verification code: user_code') + expect(outputMock.output()).toContain('Open this link to start the auth process: verification_uri_complete') + expect(outputMock.output()).toContain('Waiting for authentication to complete. Keep this command running.') + expect(outputMock.output()).toContain( + 'If you are an agent, show the URL and code to the user, ask them to complete login, then continue after this command finishes.', + ) + }) + test('when the response is not valid JSON, throw an error with context', async () => { // Given const response = new Response('not valid JSON') diff --git a/packages/cli-kit/src/private/node/session/device-authorization.ts b/packages/cli-kit/src/private/node/session/device-authorization.ts index 14e8e367a9f..c095664a7ad 100644 --- a/packages/cli-kit/src/private/node/session/device-authorization.ts +++ b/packages/cli-kit/src/private/node/session/device-authorization.ts @@ -81,20 +81,29 @@ export async function requestDeviceAuthorization(scopes: string[]): Promise { + const waitingMessage = () => { + outputInfo('Waiting for authentication to complete. Keep this command running.') + outputInfo( + 'If you are an agent, show the URL and code to the user, ask them to complete login, then continue after this command finishes.', + ) + } + + const manualLoginMessage = () => { outputInfo(outputContent`👉 Open this link to start the auth process: ${linkToken}`) + waitingMessage() } if (isCloudEnvironment() || !isTTY()) { - cloudMessage() + manualLoginMessage() } else { outputInfo('👉 Press any key to open the login page on your browser') await keypress() const opened = await openURL(jsonResult.verification_uri_complete) if (opened) { outputInfo(outputContent`Opened link to start the auth process: ${linkToken}`) + waitingMessage() } else { - cloudMessage() + manualLoginMessage() } } diff --git a/packages/cli/README.md b/packages/cli/README.md index 21dadd72067..c6e8e1e80ee 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -1048,7 +1048,7 @@ DESCRIPTION ## `shopify auth login` -Logs you in to your Shopify account. +Log in to a Shopify account. ``` USAGE @@ -1058,7 +1058,20 @@ FLAGS --alias= [env: SHOPIFY_FLAG_AUTH_ALIAS] Alias of the session you want to login to. DESCRIPTION - Logs you in to your Shopify account. + Log in to a Shopify account. + + Logs in to a Shopify account using a browser-based device authentication flow. + + If Shopify CLI prints a verification URL and user code, open the URL in a browser, complete login, and keep the + command running. The command continues automatically after authentication succeeds. + + When running from an agent, show the verification URL and user code to the user, ask them to complete login in the + browser, and wait for the command to finish. + +EXAMPLES + $ shopify auth login + + $ shopify auth login --alias my-account ``` ## `shopify auth logout` diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index 7f13cde58b9..d5e4405b94f 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -3042,8 +3042,13 @@ ], "args": { }, - "description": "Logs you in to your Shopify account.", + "description": "Logs in to a Shopify account using a browser-based device authentication flow.\n\nIf Shopify CLI prints a verification URL and user code, open the URL in a browser, complete login, and keep the command running. The command continues automatically after authentication succeeds.\n\nWhen running from an agent, show the verification URL and user code to the user, ask them to complete login in the browser, and wait for the command to finish.", + "descriptionWithMarkdown": "Logs in to a Shopify account using a browser-based device authentication flow.\n\nIf Shopify CLI prints a verification URL and user code, open the URL in a browser, complete login, and keep the command running. The command continues automatically after authentication succeeds.\n\nWhen running from an agent, show the verification URL and user code to the user, ask them to complete login in the browser, and wait for the command to finish.", "enableJsonFlag": false, + "examples": [ + "<%= config.bin %> <%= command.id %>", + "<%= config.bin %> <%= command.id %> --alias my-account" + ], "flags": { "alias": { "description": "Alias of the session you want to login to.", @@ -3061,7 +3066,8 @@ "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", - "strict": true + "strict": true, + "summary": "Log in to a Shopify account." }, "auth:logout": { "aliases": [ diff --git a/packages/cli/src/cli/commands/auth/login.ts b/packages/cli/src/cli/commands/auth/login.ts index 992b311dd26..abeba9abf4d 100644 --- a/packages/cli/src/cli/commands/auth/login.ts +++ b/packages/cli/src/cli/commands/auth/login.ts @@ -4,7 +4,17 @@ import {Flags} from '@oclif/core' import {outputCompleted} from '@shopify/cli-kit/node/output' export default class Login extends Command { - static description = 'Logs you in to your Shopify account.' + static summary = 'Log in to a Shopify account.' + + static descriptionWithMarkdown = `Logs in to a Shopify account using a browser-based device authentication flow. + +If Shopify CLI prints a verification URL and user code, open the URL in a browser, complete login, and keep the command running. The command continues automatically after authentication succeeds. + +When running from an agent, show the verification URL and user code to the user, ask them to complete login in the browser, and wait for the command to finish.` + + static description = this.descriptionWithoutMarkdown() + + static examples = ['<%= config.bin %> <%= command.id %>', '<%= config.bin %> <%= command.id %> --alias my-account'] static flags = { alias: Flags.string({