diff --git a/.changeset/store-list-info-banner.md b/.changeset/store-list-info-banner.md new file mode 100644 index 00000000000..56de5ecfa5e --- /dev/null +++ b/.changeset/store-list-info-banner.md @@ -0,0 +1,5 @@ +--- +'@shopify/store': minor +--- + +Show `store list` context in an info banner, with the organization as a label/value row diff --git a/packages/store/src/cli/services/store/list/result.test.ts b/packages/store/src/cli/services/store/list/result.test.ts index cd84e69811b..e68b3291225 100644 --- a/packages/store/src/cli/services/store/list/result.test.ts +++ b/packages/store/src/cli/services/store/list/result.test.ts @@ -31,7 +31,7 @@ describe('writeStoreListResult', () => { 'text', ) - expect(output.info()).toContain('Organization: Acme (1234)') + expect(output.info()).toContain('Acme (1234)') expect(output.info()).toContain('Subdomain') expect(output.info()).toContain('my-shop') expect(output.info()).not.toContain('my-shop.myshopify.com') @@ -41,6 +41,45 @@ describe('writeStoreListResult', () => { expect(output.info()).toContain('shopify store auth list') }) + test('renders the organization row and the store auth hint in a single info banner above the table', () => { + const output = mockAndCaptureOutput() + + writeStoreListResult( + { + source: 'organization', + organization, + stores: [ + { + store: 'my-shop.myshopify.com', + createdAt: '2026-05-22T00:00:00Z', + organizationId: '1234', + organizationName: 'Acme', + name: 'My Shop', + type: 'dev', + }, + ], + }, + 'text', + ) + + expect(trimmedLines(output.info())).toMatchInlineSnapshot(` + "╭─ info ───────────────────────────────────────────────────────────────────────╮ + │ │ + │ Listing stores. │ + │ │ + │ Organization Acme (1234) │ + │ │ + │ To list stores authenticated directly with \`shopify store auth\`, run │ + │ \`shopify store auth list\`. │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────────╯ + + Subdomain Name Type Created + ───────── ─────── ──── ──────────── + my-shop My Shop Dev May 22, 2026" + `) + }) + test('renders the subdomain handle for non-myshopify hosts (local dev)', () => { const output = mockAndCaptureOutput() @@ -82,21 +121,34 @@ describe('writeStoreListResult', () => { expect(output.info()).toContain('shopify store auth list') }) - test('renders the selected organization empty state', () => { + test('renders the selected organization empty state in the same banner shape', () => { const output = mockAndCaptureOutput() writeStoreListResult({source: 'organization', organization, stores: []}, 'text') - expect(output.info()).toContain('No stores found in Acme.') + expect(trimmedLines(output.info())).toMatchInlineSnapshot(` + "╭─ info ───────────────────────────────────────────────────────────────────────╮ + │ │ + │ No stores found. │ + │ │ + │ Organization Acme (1234) │ + │ │ + │ To list stores authenticated directly with \`shopify store auth\`, run │ + │ \`shopify store auth list\`. │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────────╯ + " + `) }) - test('renders the fallback organization empty state when no organization is selected', () => { + test('omits the organization row from the empty state when no organization is selected', () => { const output = mockAndCaptureOutput() writeStoreListResult({source: 'organization', stores: []}, 'text') - expect(output.info()).toContain('No stores found in your Shopify organization.') + expect(output.info()).toContain('No stores found.') expect(output.info()).toContain('shopify store auth list') + expect(output.info()).not.toContain('Organization') }) test('emits a {stores, organization} JSON document on stdout', () => { @@ -182,3 +234,12 @@ describe('writeStoreListResult', () => { expect(jsonOutput.output()).toContain('"truncated": true') }) }) + +// The banner pads every line out to the terminal width, which is narrow in the test environment. +// Trimming keeps the snapshots readable and free of trailing whitespace. +function trimmedLines(output: string): string { + return output + .split('\n') + .map((line) => line.trimEnd()) + .join('\n') +} diff --git a/packages/store/src/cli/services/store/list/result.ts b/packages/store/src/cli/services/store/list/result.ts index 925dbba7fee..721d5007319 100644 --- a/packages/store/src/cli/services/store/list/result.ts +++ b/packages/store/src/cli/services/store/list/result.ts @@ -1,9 +1,18 @@ import {STORE_LIST_LIMIT} from './constants.js' -import {type ListStoresResult, type StoreListEntry} from './types.js' +import {type ListStoresResult, type StoreListEntry, type StoreListOrganization} from './types.js' import {extractSubdomain, formatShortDate} from '../display.js' import {storeTypeLabel} from '../store-type.js' -import {outputInfo, outputResult, outputWarn} from '@shopify/cli-kit/node/output' -import {renderTable} from '@shopify/cli-kit/node/ui' +import {outputResult, outputWarn} from '@shopify/cli-kit/node/output' +import {renderInfo, renderTable, type AlertCustomSection, type TokenItem} from '@shopify/cli-kit/node/ui' + +const STORE_AUTH_HINT: TokenItem = [ + 'To list stores authenticated directly with', + {command: 'shopify store auth'}, + {char: ','}, + 'run', + {command: 'shopify store auth list'}, + {char: '.'}, +] export function writeStoreListResult(result: ListStoresResult, format: 'text' | 'json'): void { // Human diagnostics always go to stderr so they never corrupt the JSON document on stdout, and so @@ -36,17 +45,34 @@ function truncationWarning(result: ListStoresResult): string { } function renderTextResult(result: ListStoresResult): void { - if (result.stores.length === 0) { - outputInfo(emptyStateMessage(result)) - return - } + renderInfo({ + headline: textResultHeadline(result), + customSections: [...organizationSections(result.organization), {body: STORE_AUTH_HINT}], + }) - if (result.organization) { - outputInfo(`Organization: ${result.organization.name} (${result.organization.id})`) + if (result.stores.length > 0) { + renderOrganizationTable(result.stores) } +} - renderOrganizationTable(result.stores) - outputInfo('To list stores authenticated directly with `shopify store auth`, run `shopify store auth list`.') +function textResultHeadline(result: ListStoresResult): string { + if (result.stores.length > 0) return 'Listing stores.' + // The notice explains on stderr why the session couldn't be resolved; this states the outcome. + if (result.notice) return 'No stores were returned for the current CLI session.' + return 'No stores found.' +} + +function organizationSections(organization: StoreListOrganization | undefined): AlertCustomSection[] { + if (!organization) return [] + + return [ + { + body: { + tabularData: [['Organization', `${organization.name} (${organization.id})`]], + firstColumnSubdued: true, + }, + }, + ] } function renderOrganizationTable(stores: StoreListEntry[]): void { @@ -66,26 +92,6 @@ function renderOrganizationTable(stores: StoreListEntry[]): void { }) } -function emptyStateMessage(result: ListStoresResult): string { - if (result.notice) { - return [ - 'No stores were returned for the current CLI session.', - '', - 'Run `shopify store auth list` to list stores authenticated directly with `shopify store auth`.', - ].join('\n') - } - - if (result.organization) { - return `No stores found in ${result.organization.name}.` - } - - return [ - 'No stores found in your Shopify organization.', - '', - 'Run `shopify store auth list` to list stores authenticated directly with `shopify store auth`.', - ].join('\n') -} - function subdomainFor(store: string): string { return extractSubdomain(store) ?? store }