Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/store-list-info-banner.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/store': minor
---

Show `store list` context in an info banner, with the organization as a label/value row
71 changes: 66 additions & 5 deletions packages/store/src/cli/services/store/list/result.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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()

Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very cool. Maybe worth broadening to the full test suite as a util in the future.

return output
.split('\n')
.map((line) => line.trimEnd())
.join('\n')
}
68 changes: 37 additions & 31 deletions packages/store/src/cli/services/store/list/result.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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
}
Loading