From 5bdb86200742af7102356c6b13331fbded5b6036 Mon Sep 17 00:00:00 2001 From: Nick Wesselman <27013789+nickwesselman@users.noreply.github.com> Date: Wed, 2 Sep 2026 23:13:14 -0400 Subject: [PATCH 1/3] Show store list context in an info banner The organization line and the `shopify store auth list` hint were printed as bare stdout text, so the hint collided with the bottom of the table and the organization had no visual separation from the header row. Move both into a single info banner rendered above the table, matching the banner-then-table shape `listBulkOperations` already uses in this package. Co-Authored-By: Claude Opus 5 (1M context) Assisted-By: devx/4874be33-ff52-4bcc-beaa-5ff567199ec5 --- .changeset/store-list-info-banner.md | 5 ++ .../cli/services/store/list/result.test.ts | 46 ++++++++++++++++++- .../src/cli/services/store/list/result.ts | 21 +++++++-- 3 files changed, 66 insertions(+), 6 deletions(-) create mode 100644 .changeset/store-list-info-banner.md diff --git a/.changeset/store-list-info-banner.md b/.changeset/store-list-info-banner.md new file mode 100644 index 00000000000..84edcc5e1e7 --- /dev/null +++ b/.changeset/store-list-info-banner.md @@ -0,0 +1,5 @@ +--- +'@shopify/store': minor +--- + +Show the organization and `shopify store auth list` hint in an info banner in `store list` 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..06a35ba47c2 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('Listing stores in 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,50 @@ describe('writeStoreListResult', () => { expect(output.info()).toContain('shopify store auth list') }) + test('renders the organization 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', + ) + + // Terminal width in the test environment is quite narrow, so the table columns wrap. + const outputWithoutTrailingWhitespace = output + .info() + .split('\n') + .map((line) => line.trimEnd()) + .join('\n') + + expect(outputWithoutTrailingWhitespace).toMatchInlineSnapshot(` + "╭─ info ───────────────────────────────────────────────────────────────────────╮ + │ │ + │ Listing stores in 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() diff --git a/packages/store/src/cli/services/store/list/result.ts b/packages/store/src/cli/services/store/list/result.ts index 925dbba7fee..d9d0f53fef9 100644 --- a/packages/store/src/cli/services/store/list/result.ts +++ b/packages/store/src/cli/services/store/list/result.ts @@ -3,7 +3,7 @@ import {type ListStoresResult, type StoreListEntry} 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 {renderInfo, renderTable} from '@shopify/cli-kit/node/ui' 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 @@ -41,12 +41,23 @@ function renderTextResult(result: ListStoresResult): void { return } - if (result.organization) { - outputInfo(`Organization: ${result.organization.name} (${result.organization.id})`) - } + const headline = result.organization + ? `Listing stores in ${result.organization.name} (${result.organization.id}).` + : 'Listing stores.' + + renderInfo({ + headline, + body: [ + 'To list stores authenticated directly with', + {command: 'shopify store auth'}, + {char: ','}, + 'run', + {command: 'shopify store auth list'}, + {char: '.'}, + ], + }) renderOrganizationTable(result.stores) - outputInfo('To list stores authenticated directly with `shopify store auth`, run `shopify store auth list`.') } function renderOrganizationTable(stores: StoreListEntry[]): void { From 89a81afe26d94575641175a0683485db9497de65 Mon Sep 17 00:00:00 2001 From: Nick Wesselman <27013789+nickwesselman@users.noreply.github.com> Date: Wed, 2 Sep 2026 23:18:40 -0400 Subject: [PATCH 2/3] Show the organization as a label/value row and unify the empty state Move the organization out of the headline into a tabular customSection, matching the label/value shape used by `store info` and the dev store creation summary. Custom sections lay out with gap={1}, so the row and the auth hint separate cleanly. Route the empty states through the same banner. They previously printed as bare multi-line text, stated the organization three different ways, and phrased the auth hint differently from the populated path. Co-Authored-By: Claude Opus 5 (1M context) Assisted-By: devx/4874be33-ff52-4bcc-beaa-5ff567199ec5 --- .changeset/store-list-info-banner.md | 2 +- .../cli/services/store/list/result.test.ts | 47 +++++++---- .../src/cli/services/store/list/result.ts | 81 +++++++++---------- 3 files changed, 73 insertions(+), 57 deletions(-) diff --git a/.changeset/store-list-info-banner.md b/.changeset/store-list-info-banner.md index 84edcc5e1e7..56de5ecfa5e 100644 --- a/.changeset/store-list-info-banner.md +++ b/.changeset/store-list-info-banner.md @@ -2,4 +2,4 @@ '@shopify/store': minor --- -Show the organization and `shopify store auth list` hint in an info banner in `store list` +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 06a35ba47c2..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('Listing stores in 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,7 +41,7 @@ describe('writeStoreListResult', () => { expect(output.info()).toContain('shopify store auth list') }) - test('renders the organization and the store auth hint in a single info banner above the table', () => { + test('renders the organization row and the store auth hint in a single info banner above the table', () => { const output = mockAndCaptureOutput() writeStoreListResult( @@ -62,17 +62,12 @@ describe('writeStoreListResult', () => { 'text', ) - // Terminal width in the test environment is quite narrow, so the table columns wrap. - const outputWithoutTrailingWhitespace = output - .info() - .split('\n') - .map((line) => line.trimEnd()) - .join('\n') - - expect(outputWithoutTrailingWhitespace).toMatchInlineSnapshot(` + expect(trimmedLines(output.info())).toMatchInlineSnapshot(` "╭─ info ───────────────────────────────────────────────────────────────────────╮ │ │ - │ Listing stores in Acme (1234). │ + │ Listing stores. │ + │ │ + │ Organization Acme (1234) │ │ │ │ To list stores authenticated directly with \`shopify store auth\`, run │ │ \`shopify store auth list\`. │ @@ -126,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', () => { @@ -226,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 d9d0f53fef9..c709a63103e 100644 --- a/packages/store/src/cli/services/store/list/result.ts +++ b/packages/store/src/cli/services/store/list/result.ts @@ -1,9 +1,20 @@ 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 {renderInfo, 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' + +// Both listing paths point at the separate `shopify store auth` credential store, so the hint is +// the same whether or not the organization returned any stores. +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,28 +47,36 @@ 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.stores.length > 0) { + renderOrganizationTable(result.stores) } +} - const headline = result.organization - ? `Listing stores in ${result.organization.name} (${result.organization.id}).` - : 'Listing stores.' +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.' +} - renderInfo({ - headline, - body: [ - 'To list stores authenticated directly with', - {command: 'shopify store auth'}, - {char: ','}, - 'run', - {command: 'shopify store auth list'}, - {char: '.'}, - ], - }) +// The organization is only known once one has been selected, so the unresolved-session path +// renders the banner without this section. +function organizationSections(organization: StoreListOrganization | undefined): AlertCustomSection[] { + if (!organization) return [] - renderOrganizationTable(result.stores) + return [ + { + body: { + tabularData: [['Organization', `${organization.name} (${organization.id})`]], + firstColumnSubdued: true, + }, + }, + ] } function renderOrganizationTable(stores: StoreListEntry[]): void { @@ -77,26 +96,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 } From 52c69fe44da2ffec54ada4cbcabb60dd54fdea58 Mon Sep 17 00:00:00 2001 From: Ariel Caplan Date: Thu, 3 Sep 2026 12:22:02 +0300 Subject: [PATCH 3/3] Remove unnecessary comments --- packages/store/src/cli/services/store/list/result.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/store/src/cli/services/store/list/result.ts b/packages/store/src/cli/services/store/list/result.ts index c709a63103e..721d5007319 100644 --- a/packages/store/src/cli/services/store/list/result.ts +++ b/packages/store/src/cli/services/store/list/result.ts @@ -5,8 +5,6 @@ import {storeTypeLabel} from '../store-type.js' import {outputResult, outputWarn} from '@shopify/cli-kit/node/output' import {renderInfo, renderTable, type AlertCustomSection, type TokenItem} from '@shopify/cli-kit/node/ui' -// Both listing paths point at the separate `shopify store auth` credential store, so the hint is -// the same whether or not the organization returned any stores. const STORE_AUTH_HINT: TokenItem = [ 'To list stores authenticated directly with', {command: 'shopify store auth'}, @@ -64,8 +62,6 @@ function textResultHeadline(result: ListStoresResult): string { return 'No stores found.' } -// The organization is only known once one has been selected, so the unresolved-session path -// renders the banner without this section. function organizationSections(organization: StoreListOrganization | undefined): AlertCustomSection[] { if (!organization) return []