diff --git a/.changeset/light-windows-sit.md b/.changeset/light-windows-sit.md new file mode 100644 index 00000000000..07598bb80a8 --- /dev/null +++ b/.changeset/light-windows-sit.md @@ -0,0 +1,5 @@ +--- +'@shopify/app': minor +--- + +Give `app info` a facelift and correct a few display bugs diff --git a/.changeset/red-brooms-lick.md b/.changeset/red-brooms-lick.md new file mode 100644 index 00000000000..d4fdc36ea7a --- /dev/null +++ b/.changeset/red-brooms-lick.md @@ -0,0 +1,5 @@ +--- +'@shopify/cli-kit': minor +--- + +Add tabular data display component to UI kit diff --git a/packages/app/src/cli/commands/app/info.ts b/packages/app/src/cli/commands/app/info.ts index 5de3daa4b58..5178ea7b0f2 100644 --- a/packages/app/src/cli/commands/app/info.ts +++ b/packages/app/src/cli/commands/app/info.ts @@ -5,6 +5,7 @@ import {linkedAppContext} from '../../services/app-context.js' import {Flags} from '@oclif/core' import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli' import {outputInfo} from '@shopify/cli-kit/node/output' +import {renderInfo} from '@shopify/cli-kit/node/ui' export default class AppInfo extends AppCommand { static summary = 'Print basic information about your app and extensions.' @@ -40,14 +41,17 @@ export default class AppInfo extends AppCommand { userProvidedConfigName: flags.config, unsafeReportMode: true, }) - outputInfo( - await info(app, remoteApp, { - format: (flags.json ? 'json' : 'text') as Format, - webEnv: flags['web-env'], - configName: flags.config, - developerPlatformClient, - }), - ) + const results = await info(app, remoteApp, { + format: (flags.json ? 'json' : 'text') as Format, + webEnv: flags['web-env'], + configName: flags.config, + developerPlatformClient, + }) + if (typeof results === 'string' || 'value' in results) { + outputInfo(results) + } else { + renderInfo({customSections: results}) + } if (app.errors) process.exit(2) return {app} diff --git a/packages/app/src/cli/services/info.test.ts b/packages/app/src/cli/services/info.test.ts index 4ccea66b5a2..634ad861001 100644 --- a/packages/app/src/cli/services/info.test.ts +++ b/packages/app/src/cli/services/info.test.ts @@ -12,11 +12,12 @@ import { import {AppErrors} from '../models/app/loader.js' import {DeveloperPlatformClient} from '../utilities/developer-platform-client.js' import {describe, expect, vi, test} from 'vitest' -import {checkForNewVersion} from '@shopify/cli-kit/node/node-package-manager' import {joinPath} from '@shopify/cli-kit/node/path' -import {TokenizedString, stringifyMessage, unstyled} from '@shopify/cli-kit/node/output' +import {OutputMessage, TokenizedString, stringifyMessage, unstyled} from '@shopify/cli-kit/node/output' import {inTemporaryDirectory, writeFileSync} from '@shopify/cli-kit/node/fs' -import {CLI_KIT_VERSION} from '@shopify/cli-kit/common/version' +import {InlineToken, renderInfo} from '@shopify/cli-kit/node/ui' + +type CustomSection = Exclude[0]['customSections'], undefined>[number] vi.mock('../prompts/dev.js') vi.mock('@shopify/cli-kit/node/node-package-manager') @@ -80,34 +81,6 @@ function infoOptions(): InfoOptions { describe('info', () => { const remoteApp = testOrganizationApp() - test('returns update shopify cli reminder when last version is greater than current version', async () => { - await inTemporaryDirectory(async (tmp) => { - // Given - const latestVersion = '2.2.3' - const app = mockApp({directory: tmp}) - vi.mocked(checkForNewVersion).mockResolvedValue(latestVersion) - - // When - const result = stringifyMessage(await info(app, remoteApp, infoOptions())) - // Then - expect(unstyled(result)).toMatch(`Shopify CLI ${CLI_KIT_VERSION}`) - }) - }) - - test('returns update shopify cli reminder when last version lower or equals to current version', async () => { - await inTemporaryDirectory(async (tmp) => { - // Given - const app = mockApp({directory: tmp}) - vi.mocked(checkForNewVersion).mockResolvedValue(undefined) - - // When - const result = stringifyMessage(await info(app, remoteApp, infoOptions())) - // Then - expect(unstyled(result)).toMatch(`Shopify CLI ${CLI_KIT_VERSION}`) - expect(unstyled(result)).not.toMatch('CLI reminder') - }) - }) - test('returns the web environment as a text when webEnv is true', async () => { await inTemporaryDirectory(async (tmp) => { // Given @@ -116,7 +89,7 @@ describe('info', () => { vi.mocked(selectOrganizationPrompt).mockResolvedValue(ORG1) // When - const result = await info(app, remoteApp, {...infoOptions(), webEnv: true}) + const result = (await info(app, remoteApp, {...infoOptions(), webEnv: true})) as OutputMessage // Then expect(unstyled(stringifyMessage(result))).toMatchInlineSnapshot(` @@ -136,7 +109,7 @@ describe('info', () => { vi.mocked(selectOrganizationPrompt).mockResolvedValue(ORG1) // When - const result = await info(app, remoteApp, {...infoOptions(), format: 'json', webEnv: true}) + const result = (await info(app, remoteApp, {...infoOptions(), format: 'json', webEnv: true})) as OutputMessage // Then expect(unstyled(stringifyMessage(result))).toMatchInlineSnapshot(` @@ -184,18 +157,28 @@ describe('info', () => { vi.mocked(selectOrganizationPrompt).mockResolvedValue(ORG1) // When - const result = await info(app, remoteApp, infoOptions()) + const result = (await info(app, remoteApp, infoOptions())) as CustomSection[] + const uiData = tabularDataSectionFromInfo(result, 'ui_extension_external') + const checkoutData = tabularDataSectionFromInfo(result, 'checkout_ui_extension_external') // Then - expect(result).toContain('Extensions with errors') + // Doesn't use the type as part of the title - expect(result).not.toContain('📂 ui_extension') - // Shows handle in title - expect(result).toContain('📂 handle-for-extension-1') + expect(JSON.stringify(uiData)).not.toContain('📂 ui_extension') + + // Shows handle as title + const uiExtensionTitle = uiData[0]![0] + expect(uiExtensionTitle).toBe('📂 handle-for-extension-1') + // Displays errors + const uiExtensionErrorsRow = errorRow(uiData) + expect(uiExtensionErrorsRow[1]).toStrictEqual({error: 'Mock error with ui_extension'}) + // Shows default handle derived from name when no handle is present - expect(result).toContain('📂 extension-2') - expect(result).toContain('! Mock error with ui_extension') - expect(result).toContain('! Mock error with checkout_ui_extension') + const checkoutExtensionTitle = checkoutData[0]![0] + expect(checkoutExtensionTitle).toBe('📂 extension-2') + // Displays errors + const checkoutExtensionErrorsRow = errorRow(checkoutData) + expect(checkoutExtensionErrorsRow[1]).toStrictEqual({error: 'Mock error with checkout_ui_extension'}) }) }) @@ -222,11 +205,14 @@ describe('info', () => { vi.mocked(selectOrganizationPrompt).mockResolvedValue(ORG1) // When - const result = await info(app, remoteApp, infoOptions()) + const result = (await info(app, remoteApp, infoOptions())) as CustomSection[] + const uiExtensionsData = tabularDataSectionFromInfo(result, 'ui_extension_external') + const relevantExtension = extensionTitleRow(uiExtensionsData, 'handle-for-extension-1') + const irrelevantExtension = extensionTitleRow(uiExtensionsData, 'point_of_sale') // Then - expect(result).toContain('📂 handle-for-extension-1') - expect(result).not.toContain('📂 point_of_sale') + expect(relevantExtension).toBeDefined() + expect(irrelevantExtension).not.toBeDefined() }) }) @@ -293,3 +279,22 @@ function mockApp({ ...(app ? app : {}), }) } + +function tabularDataSectionFromInfo(info: CustomSection[], title: string): InlineToken[][] { + const section = info.find((section) => section.title === title) + if (!section) throw new Error(`Section ${title} not found`) + if (!(typeof section.body === 'object' && 'tabularData' in section.body)) { + throw new Error(`Expected to be a table: ${JSON.stringify(section.body)}`) + } + return section.body.tabularData +} + +function errorRow(data: InlineToken[][]): InlineToken[] { + const row = data.find((row: InlineToken[]) => typeof row[0] === 'object' && 'error' in row[0])! + if (!row) throw new Error('Error row not found') + return row +} + +function extensionTitleRow(data: InlineToken[][], title: string): InlineToken[] | undefined { + return data.find((row) => typeof row[0] === 'string' && row[0].match(new RegExp(title))) +} diff --git a/packages/app/src/cli/services/info.ts b/packages/app/src/cli/services/info.ts index 9ec1f0057c9..4e0aa2b4308 100644 --- a/packages/app/src/cli/services/info.ts +++ b/packages/app/src/cli/services/info.ts @@ -6,11 +6,19 @@ import {configurationFileNames} from '../constants.js' import {ExtensionInstance} from '../models/extensions/extension-instance.js' import {OrganizationApp} from '../models/organization.js' import {platformAndArch} from '@shopify/cli-kit/node/os' -import {linesToColumns} from '@shopify/cli-kit/common/string' import {basename, relativePath} from '@shopify/cli-kit/node/path' -import {OutputMessage, outputContent, outputToken, formatSection, stringifyMessage} from '@shopify/cli-kit/node/output' +import { + OutputMessage, + formatPackageManagerCommand, + outputContent, + shouldDisplayColors, + stringifyMessage, +} from '@shopify/cli-kit/node/output' +import {InlineToken, renderInfo} from '@shopify/cli-kit/node/ui' import {CLI_KIT_VERSION} from '@shopify/cli-kit/common/version' +type CustomSection = Exclude[0]['customSections'], undefined>[number] + export type Format = 'json' | 'text' export interface InfoOptions { format: Format @@ -19,16 +27,12 @@ export interface InfoOptions { webEnv: boolean developerPlatformClient: DeveloperPlatformClient } -interface Configurable { - type: string - externalType: string -} export async function info( app: AppLinkedInterface, remoteApp: OrganizationApp, options: InfoOptions, -): Promise { +): Promise { if (options.webEnv) { return infoWeb(app, remoteApp, options) } else { @@ -48,7 +52,7 @@ async function infoApp( app: AppLinkedInterface, remoteApp: OrganizationApp, options: InfoOptions, -): Promise { +): Promise { if (options.format === 'json') { const extensionsInfo = withPurgedSchemas(app.allExtensions.filter((ext) => ext.isReturnedAsInfo())) let appWithSupportedExtensions = { @@ -103,8 +107,9 @@ function withPurgedSchemas(extensions: object[]): object[] { }) } -const UNKNOWN_TEXT = outputContent`${outputToken.italic('unknown')}`.value -const NOT_CONFIGURED_TEXT = outputContent`${outputToken.italic('Not yet configured')}`.value +const UNKNOWN_TEXT = 'unknown' +const NOT_CONFIGURED_TOKEN: InlineToken = {subdued: 'Not yet configured'} +const NOT_LOADED_TEXT = 'NOT LOADED' class AppInfo { private readonly app: AppLinkedInterface @@ -117,157 +122,165 @@ class AppInfo { this.options = options } - async output(): Promise { - const sections: [string, string][] = [ - await this.devConfigsSection(), + async output(): Promise { + return [ + ...(await this.devConfigsSection()), this.projectSettingsSection(), - await this.appComponentsSection(), + ...(await this.appComponentsSection()), await this.systemInfoSection(), ] - return sections.map((sectionContents: [string, string]) => formatSection(...sectionContents)).join('\n\n') } - async devConfigsSection(): Promise<[string, string]> { - const title = `Current app configuration` - const postscript = outputContent`💡 To change these, run ${outputToken.packagejsonScript( - this.app.packageManager, - 'dev', - '--reset', - )}`.value - - let updateUrls = NOT_CONFIGURED_TEXT + async devConfigsSection(): Promise { + let updateUrls = NOT_CONFIGURED_TOKEN if (this.app.configuration.build?.automatically_update_urls_on_dev !== undefined) { updateUrls = this.app.configuration.build.automatically_update_urls_on_dev ? 'Yes' : 'No' } - let partnersAccountInfo = ['Partners account', 'unknown'] + let userAccountInfo: [string, string] = ['User', 'unknown'] const retrievedAccountInfo = await this.options.developerPlatformClient.accountInfo() if (isServiceAccount(retrievedAccountInfo)) { - partnersAccountInfo = ['Service account', retrievedAccountInfo.orgName] + userAccountInfo = ['Service account', retrievedAccountInfo.orgName] } else if (isUserAccount(retrievedAccountInfo)) { - partnersAccountInfo = ['Partners account', retrievedAccountInfo.email] + userAccountInfo[1] = retrievedAccountInfo.email } - const lines = [ - ['Configuration file', basename(this.app.configuration.path) || configurationFileNames.app], - ['App name', this.remoteApp.title || NOT_CONFIGURED_TEXT], - ['Client ID', this.remoteApp.apiKey || NOT_CONFIGURED_TEXT], - ['Access scopes', getAppScopes(this.app.configuration)], - ['Dev store', this.app.configuration.build?.dev_store_url || NOT_CONFIGURED_TEXT], - ['Update URLs', updateUrls], - partnersAccountInfo, + return [ + this.tableSection( + 'Current app configuration', + [ + ['Configuration file', {filePath: basename(this.app.configuration.path) || configurationFileNames.app}], + ['App name', this.remoteApp.title ? {userInput: this.remoteApp.title} : NOT_CONFIGURED_TOKEN], + ['Client ID', this.remoteApp.apiKey || NOT_CONFIGURED_TOKEN], + ['Access scopes', getAppScopes(this.app.configuration)], + ['Dev store', this.app.configuration.build?.dev_store_url ?? NOT_CONFIGURED_TOKEN], + ['Update URLs', updateUrls], + userAccountInfo, + ], + {isFirstItem: true}, + ), + { + body: [ + '💡 To change these, run', + {command: formatPackageManagerCommand(this.app.packageManager, 'dev', '--reset')}, + ], + }, ] - return [title, `${linesToColumns(lines)}\n\n${postscript}`] } - projectSettingsSection(): [string, string] { - const title = 'Your Project' - const lines = [['Root location', this.app.directory]] - return [title, linesToColumns(lines)] + projectSettingsSection(): CustomSection { + return this.tableSection('Your Project', [['Root location', {filePath: this.app.directory}]]) } - async appComponentsSection(): Promise<[string, string]> { - const title = 'Directory Components' - - let body = this.webComponentsSection() - - function augmentWithExtensions( - extensions: TExtension[], - outputFormatter: (extension: TExtension) => string, - ) { - const types = new Set(extensions.map((ext) => ext.type)) - types.forEach((extensionType: string) => { - const relevantExtensions = extensions.filter((extension: TExtension) => extension.type === extensionType) - if (relevantExtensions[0]) { - body += `\n\n${outputContent`${outputToken.subheading(relevantExtensions[0].externalType)}`.value}` - relevantExtensions.forEach((extension: TExtension) => { - body += outputFormatter(extension) - }) - } - }) - } - - const supportedExtensions = this.app.allExtensions.filter((ext) => ext.isReturnedAsInfo()) - augmentWithExtensions(supportedExtensions, this.extensionSubSection.bind(this)) - - if (this.app.errors?.isEmpty() === false) { - body += `\n\n${outputContent`${outputToken.subheading('Extensions with errors')}`.value}` - supportedExtensions.forEach((extension) => { - body += this.invalidExtensionSubSection(extension) - }) - } - return [title, body] + async appComponentsSection(): Promise { + const webComponentsSection = this.webComponentsSection() + return [ + { + title: '\nDirectory components'.toUpperCase(), + body: '', + }, + ...(webComponentsSection ? [webComponentsSection] : []), + ...this.extensionsSections(), + ] } - webComponentsSection(): string { + webComponentsSection(): CustomSection | undefined { const errors: OutputMessage[] = [] - const subtitle = outputContent`${outputToken.subheading('web')}`.value - const toplevel = ['📂 web', ''] - const sublevels: [string, string][] = [] + const sublevels: InlineToken[][] = [] + if (!this.app.webs[0]) return this.app.webs.forEach((web) => { if (web.configuration) { if (web.configuration.name) { const {name, roles} = web.configuration - sublevels.push([` 📂 ${name} (${roles.join(',')})`, relativePath(this.app.directory, web.directory)]) + const pathToWeb = relativePath(this.app.directory, web.directory) + sublevels.push([` 📂 ${name}`, {filePath: pathToWeb || '/'}]) + if (roles.length > 0) { + sublevels.push([' roles', roles.join(', ')]) + } } else { web.configuration.roles.forEach((role) => { - sublevels.push([` 📂 ${role}`, relativePath(this.app.directory, web.directory)]) + sublevels.push([` 📂 ${role}`, {filePath: relativePath(this.app.directory, web.directory)}]) }) } } else { - sublevels.push([` 📂 ${UNKNOWN_TEXT}`, relativePath(this.app.directory, web.directory)]) + sublevels.push([{subdued: ` 📂 ${UNKNOWN_TEXT}`}, {filePath: relativePath(this.app.directory, web.directory)}]) } if (this.app.errors) { const error = this.app.errors.getError(`${web.directory}/${configurationFileNames.web}`) if (error) errors.push(error) } }) - let errorContent = `\n${errors.map((error) => this.formattedError(error)).join('\n')}` - if (errorContent.trim() === '') errorContent = '' - return `${subtitle}\n${linesToColumns([toplevel, ...sublevels])}${errorContent}` + return this.subtableSection('web', [ + ['📂 web', ''], + ...sublevels, + ...errors.map((error): InlineToken[] => [{error: 'error'}, {error: this.formattedError(error)}]), + ]) } - extensionSubSection(extension: ExtensionInstance): string { + extensionsSections(): CustomSection[] { + const extensions = this.app.allExtensions.filter((ext) => ext.isReturnedAsInfo()) + const types = Array.from(new Set(extensions.map((ext) => ext.type))) + return types + .map((extensionType: string): CustomSection | undefined => { + const relevantExtensions = extensions.filter((extension: ExtensionInstance) => extension.type === extensionType) + if (relevantExtensions[0]) { + return this.subtableSection( + relevantExtensions[0].externalType, + relevantExtensions.map((ext) => this.extensionSubSection(ext)).flat(), + ) + } + }) + .filter((section: CustomSection | undefined) => section !== undefined) + } + + extensionSubSection(extension: ExtensionInstance): InlineToken[][] { const config = extension.configuration - const details = [ - [`📂 ${extension.handle}`, relativePath(this.app.directory, extension.directory)], - [' config file', relativePath(extension.directory, extension.configurationPath)], + const details: InlineToken[][] = [ + [`📂 ${extension.handle || NOT_LOADED_TEXT}`, {filePath: relativePath(this.app.directory, extension.directory)}], + [' config file', {filePath: relativePath(extension.directory, extension.configurationPath)}], ] if (config && config.metafields?.length) { details.push([' metafields', `${config.metafields.length}`]) } - - return `\n${linesToColumns(details)}` - } - - invalidExtensionSubSection(extension: ExtensionInstance): string { const error = this.app.errors?.getError(extension.configurationPath) - if (!error) return '' - const details = [ - [`📂 ${extension.handle}`, relativePath(this.app.directory, extension.directory)], - [' config file', relativePath(extension.directory, extension.configurationPath)], - ] - const formattedError = this.formattedError(error) - return `\n${linesToColumns(details)}\n${formattedError}` + if (error) { + details.push([{error: ' error'}, {error: this.formattedError(error)}]) + } + + return details } formattedError(str: OutputMessage): string { - const [errorFirstLine, ...errorRemainingLines] = stringifyMessage(str).split('\n') - const errorLines = [`! ${errorFirstLine}`, ...errorRemainingLines.map((line) => ` ${line}`)] - return outputContent`${outputToken.errorText(errorLines.join('\n'))}`.value + // Some errors have newlines at the beginning for no apparent reason + const rawErrorMessage = stringifyMessage(str).trim() + if (shouldDisplayColors()) return rawErrorMessage + const [errorFirstLine, ...errorRemainingLines] = stringifyMessage(str).trim().split('\n') + return [`! ${errorFirstLine}`, ...errorRemainingLines.map((line) => ` ${line}`)].join('\n') } - async systemInfoSection(): Promise<[string, string]> { - const title = 'Tooling and System' + async systemInfoSection(): Promise { const {platform, arch} = platformAndArch() - const lines: string[][] = [ + return this.tableSection('Tooling and System', [ ['Shopify CLI', CLI_KIT_VERSION], ['Package manager', this.app.packageManager], ['OS', `${platform}-${arch}`], - ['Shell', process.env.SHELL || 'unknown'], + ['Shell', process.env.SHELL ?? 'unknown'], ['Node version', process.version], - ] - return [title, linesToColumns(lines)] + ]) + } + + tableSection(title: string, rows: InlineToken[][], {isFirstItem = false} = {}): CustomSection { + return { + title: `${isFirstItem ? '' : '\n'}${title.toUpperCase()}\n`, + body: {tabularData: rows, firstColumnSubdued: true}, + } + } + + subtableSection(title: string, rows: InlineToken[][]): CustomSection { + return { + title, + body: {tabularData: rows, firstColumnSubdued: true}, + } } } diff --git a/packages/cli-kit/src/private/node/ui/components/Alert.tsx b/packages/cli-kit/src/private/node/ui/components/Alert.tsx index f732f33b833..fc573cd55dc 100644 --- a/packages/cli-kit/src/private/node/ui/components/Alert.tsx +++ b/packages/cli-kit/src/private/node/ui/components/Alert.tsx @@ -2,12 +2,13 @@ import {Banner, BannerType} from './Banner.js' import {Link} from './Link.js' import {List} from './List.js' import {BoldToken, InlineToken, LinkToken, TokenItem, TokenizedText} from './TokenizedText.js' +import {TabularData, TabularDataProps} from './TabularData.js' import {Box, Text} from 'ink' import React, {FunctionComponent} from 'react' export interface CustomSection { title?: string - body: TokenItem + body: TabularDataProps | TokenItem } export interface AlertProps { @@ -57,7 +58,11 @@ const Alert: FunctionComponent = ({ {customSections.map((section, index) => ( {section.title ? {section.title} : null} - + {typeof section.body === 'object' && 'tabularData' in section.body ? ( + + ) : ( + + )} ))} diff --git a/packages/cli-kit/src/private/node/ui/components/FatalError.tsx b/packages/cli-kit/src/private/node/ui/components/FatalError.tsx index c5e65dafcbe..b632ac0d177 100644 --- a/packages/cli-kit/src/private/node/ui/components/FatalError.tsx +++ b/packages/cli-kit/src/private/node/ui/components/FatalError.tsx @@ -2,6 +2,7 @@ import {Banner} from './Banner.js' import {TokenizedText} from './TokenizedText.js' import {Command} from './Command.js' import {List} from './List.js' +import {TabularData} from './TabularData.js' import {BugError, cleanSingleStackTracePath, ExternalError, FatalError as Fatal} from '../../../../public/node/error.js' import {Box, Text} from 'ink' import React, {FunctionComponent} from 'react' @@ -58,7 +59,11 @@ const FatalError: FunctionComponent = ({error}) => { {error.customSections.map((section, index) => ( {section.title ? {section.title} : null} - + {typeof section.body === 'object' && 'tabularData' in section.body ? ( + + ) : ( + + )} ))} diff --git a/packages/cli-kit/src/private/node/ui/components/TabularData.tsx b/packages/cli-kit/src/private/node/ui/components/TabularData.tsx new file mode 100644 index 00000000000..a228308af54 --- /dev/null +++ b/packages/cli-kit/src/private/node/ui/components/TabularData.tsx @@ -0,0 +1,36 @@ +import {InlineToken, TokenizedText, tokenItemToString} from './TokenizedText.js' +import {unstyled} from '../../../../public/node/output.js' +import {Box} from 'ink' +import React, {FunctionComponent} from 'react' + +export interface TabularDataProps { + tabularData: InlineToken[][] + firstColumnSubdued?: boolean +} + +const TabularData: FunctionComponent = ({tabularData: data, firstColumnSubdued}) => { + const columnWidths: number[] = data.reduce((acc, row) => { + row.forEach((cell, index) => { + acc[index] = Math.max(acc[index] ?? 0, unstyled(tokenItemToString(cell)).length) + }) + return acc + }, []) + + return ( + + {data.map((row, index) => ( + + {row.map((cell, index) => ( + + + + ))} + + ))} + + ) +} + +export {TabularData} diff --git a/packages/cli/src/cli/services/kitchen-sink/static.ts b/packages/cli/src/cli/services/kitchen-sink/static.ts index c6b342f8207..32a01e8c1e6 100644 --- a/packages/cli/src/cli/services/kitchen-sink/static.ts +++ b/packages/cli/src/cli/services/kitchen-sink/static.ts @@ -34,6 +34,22 @@ export async function staticService() { ], }) + renderInfo({ + headline: 'About your app', + customSections: [ + { + body: { + tabularData: [ + ['Configuration file', {filePath: 'shopify.app.scalable-transaction-app.toml'}], + ['App name', {userInput: 'scalable-transaction-app'}], + ['Access scopes', 'read_products,write_products'], + ], + firstColumnSubdued: true, + }, + }, + ], + }) + renderInfo({ headline: [{userInput: 'my-app'}, 'initialized and ready to build.'], nextSteps: [