diff --git a/packages/cli-kit/package.json b/packages/cli-kit/package.json index ce1ae34883..09b988f2a8 100644 --- a/packages/cli-kit/package.json +++ b/packages/cli-kit/package.json @@ -90,7 +90,6 @@ "static": [ "@oclif/core", "./context/utilities.js", - "../../private/node/demo-recorder.js", "../../private/node/conf-store.js", "url" ] diff --git a/packages/cli-kit/src/private/node/demo-recorder.ts b/packages/cli-kit/src/private/node/demo-recorder.ts deleted file mode 100644 index f06e43bb15..0000000000 --- a/packages/cli-kit/src/private/node/demo-recorder.ts +++ /dev/null @@ -1,171 +0,0 @@ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ -import {isTruthy} from '../../public/node/context/utilities.js' - -interface Event { - type: string - properties: {[key: string]: unknown} - // Only used within this recorder for tracking concurrency timeline - concurrencyStart?: number -} - -interface ConcurrencyStep { - timestamp: number - endMessage: string -} - -interface ConcurrencyProcess { - prefix: string - steps: ConcurrencyStep[] -} - -class DemoRecorder { - recorded: Event[] - sleepStart: number - command: string - - constructor() { - this.recorded = [] - this.sleepStart = Date.now() - this.command = ['shopify', ...process.argv.slice(2)].join(' ') - } - - addEvent({type, properties}: Event) { - if (type === 'taskbar') { - this.resetSleep() - } else { - this.addSleep() - } - this.recorded.push({type, properties: JSON.parse(JSON.stringify(properties))}) - this.sleepStart = Date.now() - } - - recordedEventsJson() { - return JSON.stringify( - { - command: this.command, - steps: this.withFormattedConcurrent(this.recorded), - }, - null, - 2, - ) - } - - addSleep() { - const duration = (Date.now() - this.sleepStart) / 1000 - this.sleepStart = Date.now() - if (duration > 0.1) { - this.recorded.push({type: 'sleep', properties: {duration}}) - } - } - - resetSleep() { - this.sleepStart = Date.now() - } - - addOrUpdateConcurrentOutput({prefix, index, output}: {prefix: string; index: number; output: string}) { - let last = this.recorded[this.recorded.length - 1] - if (last?.type === 'concurrent') { - // Don't sleep between concurrent lines - this.resetSleep() - } else { - const eventProperties: Event = { - type: 'concurrent', - properties: {processes: [], concurrencyStart: Date.now()}, - } - this.addEvent(eventProperties) - last = this.recorded[this.recorded.length - 1] - } - const {processes} = last!.properties as {processes: ConcurrencyProcess[]} - while (processes.length <= index) { - processes.push({prefix: '', steps: []}) - } - processes[index]!.prefix = prefix - processes[index]!.steps.push({timestamp: Date.now(), endMessage: output}) - } - - withFormattedConcurrent(recorded: Event[]) { - return recorded.map((event) => { - if (event.type === 'concurrent') { - const {processes, footer, concurrencyStart} = event.properties as { - processes: ConcurrencyProcess[] - footer?: string - concurrencyStart: number - } - const formatted = processes.map(({prefix, steps}: {prefix: string; steps: ConcurrencyStep[]}) => { - let mostRecentTimestamp = concurrencyStart - const formattedSteps = steps.map(({timestamp, endMessage}) => { - const duration = (timestamp - mostRecentTimestamp) / 1000 - mostRecentTimestamp = timestamp - return {duration, endMessage} - }) - return {prefix, steps: formattedSteps} - }) - return {type: 'concurrent', properties: {footer, processes: formatted}} - } - return event - }) - } -} - -class NoopDemoRecorder { - addEvent(_event: Event) {} - - recordedEventsJson() { - return JSON.stringify({steps: []}, null, 2) - } - - addSleep() {} - resetSleep() {} - - addOrUpdateConcurrentOutput(..._args: unknown[]) {} -} - -let _instance: { - addEvent: (event: Event) => void - recordedEventsJson: () => string - resetSleep: () => void - addSleep: () => void - addOrUpdateConcurrentOutput: ({prefix, index, output}: {prefix: string; index: number; output: string}) => void -} - -function ensureInstance() { - if (!_instance) { - if (isRecording()) { - _instance = new DemoRecorder() - } else { - _instance = new NoopDemoRecorder() - } - } -} - -export function initDemoRecorder() { - ensureInstance() -} - -export function recordUIEvent(event: Event) { - ensureInstance() - _instance.addEvent(event) -} - -export function resetRecordedSleep() { - ensureInstance() - _instance.resetSleep() -} - -export function printEventsJson(): void { - if (isRecording()) { - ensureInstance() - _instance.addSleep() - // eslint-disable-next-line no-console - console.log(_instance.recordedEventsJson()) - } -} - -export function addOrUpdateConcurrentUIEventOutput(data: {prefix: string; index: number; output: string}) { - ensureInstance() - _instance.addOrUpdateConcurrentOutput(data) -} - -function isRecording() { - return isTruthy(process.env.RECORD_DEMO) -} diff --git a/packages/cli-kit/src/private/node/ui.tsx b/packages/cli-kit/src/private/node/ui.tsx index d4d0441d7b..4e85f3abf7 100644 --- a/packages/cli-kit/src/private/node/ui.tsx +++ b/packages/cli-kit/src/private/node/ui.tsx @@ -19,7 +19,7 @@ export function renderOnce( if (output) { if (isUnitTest()) collectLog(logLevel, output) - outputWhereAppropriate(logLevel, logger, output, {skipUIEvent: true}) + outputWhereAppropriate(logLevel, logger, output) } unmount() diff --git a/packages/cli-kit/src/private/node/ui/alert.tsx b/packages/cli-kit/src/private/node/ui/alert.tsx index 6ace3671a1..a70946e583 100644 --- a/packages/cli-kit/src/private/node/ui/alert.tsx +++ b/packages/cli-kit/src/private/node/ui/alert.tsx @@ -1,7 +1,6 @@ import {Alert, AlertProps} from './components/Alert.js' import {renderOnce} from '../ui.js' import {consoleError, consoleLog, consoleWarn, Logger, LogLevel} from '../../../public/node/output.js' -import {recordUIEvent} from '../demo-recorder.js' import React from 'react' import {RenderOptions} from 'ink' @@ -35,8 +34,7 @@ export function alert({ renderOptions, }: AlertOptions) { // eslint-disable-next-line prefer-rest-params - const {type: alertType, ...eventProps} = arguments[0] - recordUIEvent({type, properties: eventProps}) + const {type: alertType, ..._eventProps} = arguments[0] return renderOnce( = ({ const index = addPrefix(prefix, prefixes) const lines = shouldStripAnsi ? stripAnsi(log).split(/\n/) : log.split(/\n/) - addOrUpdateConcurrentUIEventOutput({prefix, index, output: lines.join('\n')}) setProcessOutput((previousProcessOutput) => [ ...previousProcessOutput, { diff --git a/packages/cli-kit/src/public/node/cli.ts b/packages/cli-kit/src/public/node/cli.ts index 54439dafa4..919174b4f7 100644 --- a/packages/cli-kit/src/public/node/cli.ts +++ b/packages/cli-kit/src/public/node/cli.ts @@ -1,5 +1,4 @@ import {isTruthy} from './context/utilities.js' -import {printEventsJson} from '../../private/node/demo-recorder.js' import {cacheClear} from '../../private/node/conf-store.js' import {Flags} from '@oclif/core' import {fileURLToPath} from 'url' @@ -94,7 +93,6 @@ export async function runCLI(options: RunCLIOptions): Promise { await oclif.default.run(undefined, config) await oclif.default.flush() - printEventsJson() // eslint-disable-next-line no-catch-all/no-catch-all } catch (error) { await errorHandler(error as Error) diff --git a/packages/cli-kit/src/public/node/error-handler.ts b/packages/cli-kit/src/public/node/error-handler.ts index c4b2a5e37c..2b11157ba9 100644 --- a/packages/cli-kit/src/public/node/error-handler.ts +++ b/packages/cli-kit/src/public/node/error-handler.ts @@ -13,7 +13,6 @@ import { import {getEnvironmentData} from '../../private/node/analytics.js' import {outputDebug, outputInfo} from '../../public/node/output.js' import {bugsnagApiKey, reportingRateLimit} from '../../private/node/constants.js' -import {printEventsJson} from '../../private/node/demo-recorder.js' import {CLI_KIT_VERSION} from '../common/version.js' import {runWithRateLimit} from '../../private/node/conf-store.js' import {settings, Interfaces} from '@oclif/core' @@ -30,14 +29,13 @@ export async function errorHandler( outputInfo(`✨ ${error.message}`) } } else if (error instanceof AbortSilentError) { - printEventsJson() + /* empty */ } else { return errorMapper(error) .then((error) => { return handler(error) }) .then((mappedError) => { - printEventsJson() return reportError(mappedError, config) }) } diff --git a/packages/cli-kit/src/public/node/hooks/prerun.ts b/packages/cli-kit/src/public/node/hooks/prerun.ts index 367fab4e05..ea3ed20a57 100644 --- a/packages/cli-kit/src/public/node/hooks/prerun.ts +++ b/packages/cli-kit/src/public/node/hooks/prerun.ts @@ -4,7 +4,6 @@ import {startAnalytics} from '../../../private/node/analytics.js' import {outputDebug, outputWarn} from '../../../public/node/output.js' import {getOutputUpdateCLIReminder} from '../../../public/node/upgrade.js' import Command from '../../../public/node/base-command.js' -import {initDemoRecorder} from '../../../private/node/demo-recorder.js' import {runAtMinimumInterval} from '../../../private/node/conf-store.js' import {Hook} from '@oclif/core' @@ -15,7 +14,6 @@ export declare interface CommandContent { } // This hook is called before each command run. More info: https://oclif.io/docs/hooks export const hook: Hook.Prerun = async (options) => { - initDemoRecorder() const commandContent = parseCommandContent({ id: options.Command.id, aliases: options.Command.aliases, diff --git a/packages/cli-kit/src/public/node/output.ts b/packages/cli-kit/src/public/node/output.ts index cf2f0a1f82..4f1ea228b2 100644 --- a/packages/cli-kit/src/public/node/output.ts +++ b/packages/cli-kit/src/public/node/output.ts @@ -20,7 +20,6 @@ import { RawContentToken, SubHeadingContentToken, } from '../../private/node/content-tokens.js' -import {recordUIEvent} from '../../private/node/demo-recorder.js' import {tokenItemToString} from '../../private/node/ui/components/TokenizedText.js' import stripAnsi from 'strip-ansi' import {Writable} from 'stream' @@ -393,31 +392,20 @@ export function consoleWarn(message: string): void { console.warn(withOrWithoutStyle(message)) } -interface OutputWhereAppropriateOptions { - skipUIEvent?: boolean -} - /** * Writes a message to the appropiated logger. * * @param logLevel - The log level to use to determine if the message should be output. * @param logger - The logger to use to output the message. * @param message - The message to output. - * @param options - Additional options. */ -export function outputWhereAppropriate( - logLevel: LogLevel, - logger: Logger, - message: string, - options: OutputWhereAppropriateOptions = {skipUIEvent: false}, -): void { +export function outputWhereAppropriate(logLevel: LogLevel, logger: Logger, message: string): void { if (shouldOutput(logLevel)) { if (logger instanceof Writable) { logger.write(message) } else { logger(message, logLevel) } - if (!options.skipUIEvent) recordUIEvent({type: 'output', properties: {content: message}}) } } diff --git a/packages/cli-kit/src/public/node/tree-kill.ts b/packages/cli-kit/src/public/node/tree-kill.ts index 595879c0ea..021ffb398e 100644 --- a/packages/cli-kit/src/public/node/tree-kill.ts +++ b/packages/cli-kit/src/public/node/tree-kill.ts @@ -4,7 +4,6 @@ /* eslint-disable no-restricted-imports */ import {outputDebug} from './output.js' -import {printEventsJson} from '../../private/node/demo-recorder.js' import {exec, spawn} from 'child_process' interface ProcessTree { @@ -32,7 +31,6 @@ export function treeKill( ((error?: Error) => { if (error) outputDebug(`Failed to kill process ${pid}: ${error}`) }) - printEventsJson() adaptedTreeKill(pid, killSignal, killRoot, after) } diff --git a/packages/cli-kit/src/public/node/ui.tsx b/packages/cli-kit/src/public/node/ui.tsx index e0dfcc4a90..072f77fe80 100644 --- a/packages/cli-kit/src/public/node/ui.tsx +++ b/packages/cli-kit/src/public/node/ui.tsx @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable tsdoc/syntax */ -import {AbortError, AbortSilentError, FatalError as Fatal, FatalErrorType} from './error.js' +import {AbortError, AbortSilentError, FatalError as Fatal} from './error.js' import { collectLog, consoleError, @@ -40,7 +40,6 @@ import {Tasks, Task} from '../../private/node/ui/components/Tasks.js' import {TextPrompt, TextPromptProps} from '../../private/node/ui/components/TextPrompt.js' import {AutocompletePromptProps, AutocompletePrompt} from '../../private/node/ui/components/AutocompletePrompt.js' import {InfoTableSection} from '../../private/node/ui/components/Prompts/InfoTable.js' -import {recordUIEvent, resetRecordedSleep} from '../../private/node/demo-recorder.js' import {InfoMessageProps} from '../../private/node/ui/components/Prompts/InfoMessage.js' import React from 'react' import {Key as InkKey, RenderOptions} from 'ink' @@ -248,11 +247,6 @@ interface RenderFatalErrorOptions { */ // eslint-disable-next-line max-params export function renderFatalError(error: Fatal, {renderOptions}: RenderFatalErrorOptions = {}) { - recordUIEvent({ - type: 'fatalError', - properties: {...error, errorType: error.type === FatalErrorType.Bug ? 'bug' : 'abort'}, - }) - return renderOnce(, {logLevel: 'error', logger: consoleError, renderOptions}) } @@ -299,29 +293,21 @@ export async function renderSelectPrompt( ): Promise { throwInNonTTY({message: props.message, stdin: renderOptions?.stdin}, uiDebugOptions) - if (!isConfirmationPrompt) { - recordUIEvent({type: 'selectPrompt', properties: {renderOptions, ...props}}) - } - return runWithTimer('cmd_all_timing_prompts_ms')(async () => { let selectedValue: T - try { - await render( - { - selectedValue = value - }} - />, - { - ...renderOptions, - exitOnCtrlC: false, - }, - ) - return selectedValue! - } finally { - resetRecordedSleep() - } + await render( + { + selectedValue = value + }} + />, + { + ...renderOptions, + exitOnCtrlC: false, + }, + ) + return selectedValue! }) } @@ -362,9 +348,6 @@ export async function renderConfirmationPrompt({ abortSignal, infoMessage, }: RenderConfirmationPromptOptions): Promise { - // eslint-disable-next-line prefer-rest-params - recordUIEvent({type: 'confirmationPrompt', properties: arguments[0]}) - const choices = [ { label: confirmationMessage, @@ -440,9 +423,6 @@ export async function renderAutocompletePrompt( ): Promise { throwInNonTTY({message: props.message, stdin: renderOptions?.stdin}, uiDebugOptions) - // eslint-disable-next-line prefer-rest-params - recordUIEvent({type: 'autocompletePrompt', properties: arguments[0]}) - const newProps = { search(term: string) { const lowerTerm = term.toLowerCase() @@ -457,23 +437,19 @@ export async function renderAutocompletePrompt( return runWithTimer('cmd_all_timing_prompts_ms')(async () => { let selectedValue: T - try { - await render( - { - selectedValue = value - }} - />, - { - ...renderOptions, - exitOnCtrlC: false, - }, - ) - return selectedValue! - } finally { - resetRecordedSleep() - } + await render( + { + selectedValue = value + }} + />, + { + ...renderOptions, + exitOnCtrlC: false, + }, + ) + return selectedValue! }) } @@ -491,9 +467,6 @@ interface RenderTableOptions extends TableProps { * 3 John Smith jon@smith.com */ export function renderTable({renderOptions, ...props}: RenderTableOptions) { - // eslint-disable-next-line prefer-rest-params - recordUIEvent({type: 'table', properties: arguments[0]}) - return renderOnce(, {renderOptions}) } @@ -509,26 +482,13 @@ interface RenderTasksOptions { */ // eslint-disable-next-line max-params export async function renderTasks(tasks: Task[], {renderOptions}: RenderTasksOptions = {}) { - recordUIEvent({ - type: 'taskbar', - properties: { - // Rather than timing exactly, pretend each step takes 2 seconds. This - // should be easy to tweak manually. - steps: tasks.map((task) => { - return {title: task.title, duration: 2} - }), - }, - }) - // eslint-disable-next-line max-params return new Promise((resolve, reject) => { render(, { ...renderOptions, exitOnCtrlC: false, }) - .then(() => { - resetRecordedSleep() - }) + .then(() => {}) .catch(reject) }) } @@ -552,28 +512,21 @@ export async function renderTextPrompt( ): Promise { throwInNonTTY({message: props.message, stdin: renderOptions?.stdin}, uiDebugOptions) - // eslint-disable-next-line prefer-rest-params - recordUIEvent({type: 'textPrompt', properties: arguments[0]}) - return runWithTimer('cmd_all_timing_prompts_ms')(async () => { let enteredText = '' - try { - await render( - { - enteredText = value - }} - />, - { - ...renderOptions, - exitOnCtrlC: false, - }, - ) - return enteredText - } finally { - resetRecordedSleep() - } + await render( + { + enteredText = value + }} + />, + { + ...renderOptions, + exitOnCtrlC: false, + }, + ) + return enteredText }) } @@ -611,28 +564,21 @@ export async function renderDangerousConfirmationPrompt( ): Promise { throwInNonTTY({message: props.message, stdin: renderOptions?.stdin}, uiDebugOptions) - // eslint-disable-next-line prefer-rest-params - recordUIEvent({type: 'dangerousConfirmationPrompt', properties: arguments[0]}) - return runWithTimer('cmd_all_timing_prompts_ms')(async () => { let confirmed: boolean - try { - await render( - { - confirmed = value - }} - />, - { - ...renderOptions, - exitOnCtrlC: false, - }, - ) - return confirmed! - } finally { - resetRecordedSleep() - } + await render( + { + confirmed = value + }} + />, + { + ...renderOptions, + exitOnCtrlC: false, + }, + ) + return confirmed! }) } diff --git a/packages/cli/assets/demo-catalog.json b/packages/cli/assets/demo-catalog.json deleted file mode 100644 index 4d365ceb69..0000000000 --- a/packages/cli/assets/demo-catalog.json +++ /dev/null @@ -1,655 +0,0 @@ -{ - "command": "shopify demo", - "steps": [ - { - "type": "output", - "title": "Simple output", - "description": "This step demonstrates a simple output.", - "properties": { - "content": "\nThis is simple output." - } - }, - { - "type": "output", - "title": "Formatted output", - "description": "Output including bold and underlined text", - "properties": { - "content": "\nThis output includes \u001b[1mbold\u001b[22m and \u001b[4munderlined\u001b[24m text." - } - }, - { - "type": "output", - "title": "Formatted output with colors", - "description": "Output including colored text", - "properties": { - "content": "\nHere is a rainbow of colored output:\n• \u001b[31mred\u001b[39m\n• \u001b[32mgreen\u001b[39m\n• \u001b[33myellow\u001b[39m\n• \u001b[34mblue\u001b[39m\n• \u001b[35mmagenta\u001b[39m\n• \u001b[36mcyan\u001b[39m\n• \u001b[90mdim\u001b[39m\n• \u001b[91mbrightRed\u001b[39m\n• \u001b[92mbrightGreen\u001b[39m\n• \u001b[93mbrightYellow\u001b[39m\n• \u001b[94mbrightBlue\u001b[39m\n• \u001b[95mbrightMagenta\u001b[39m\n• \u001b[96mbrightCyan\u001b[39m\n• \u001b[97mwhite\u001b[39m\n" - } - }, - { - "type": "warning", - "title": "Warning to update CLI", - "description": "Show a warning message indicating the CLI is out of date.", - "properties": { - "headline": "CLI update available", - "body": ["Run", {"command": "npm run shopify upgrade"}, {"char": "."}] - } - }, - { - "type": "success", - "title": "CLI updated", - "description": "Show a success message indicating the CLI has been updated.", - "properties": { - "headline": "CLI updated.", - "body": "You are now running version 3.47." - } - }, - { - "type": "success", - "title": "App created successfully", - "description": "This step shows an info message after the app is created.", - "properties": { - "headline": [{"userInput": "my-app"}, "initialized and ready to build."], - "body": "Congratulations! You're on your way to success with the Shopify Partners program.", - "nextSteps": [ - [ - "Run", - { - "command": "cd verification-app" - } - ], - [ - "To preview your project, run", - { - "command": "npm app dev" - } - ], - [ - "To add extensions, run", - { - "command": "npm generate extension" - } - ] - ], - "reference": [ - [ - "Run", - { - "command": "npm shopify help" - } - ], - { - "link": { - "label": "Dev docs", - "url": "https://shopify.dev" - } - } - ], - "customSections": [ - { - "title": "Custom section", - "body": { - "list": { - "items": [{"link": {"label": "Item 1", "url": "https://shopify.com"}}, "Item 2"] - } - } - }, - { - "title": "Custom section 2", - "body": { - "list": { - "items": ["Item 1", "Item 2", "Item 3"] - } - } - } - ] - } - }, - { - "type": "success", - "title": "Successful deployment", - "description": "Show a success message after the app is deployed.", - "properties": { - "headline": "Deployment successful.", - "body": "Your extensions have been uploaded to your Shopify Partners Dashboard.", - "nextSteps": [ - { - "link": { - "label": "See your deployment and set it live", - "url": "https://partners.shopify.com/1797046/apps/4523695/deployments" - } - } - ] - } - }, - { - "type": "warning", - "title": "Access scope update required", - "description": "Show a warning message indicating the app's access scopes need to be updated.", - "properties": { - "headline": "Required access scope update.", - "body": "The deadline for re-selecting your app scopes is May 1, 2022.", - "reference": [ - { - "link": { - "label": "Dev docs", - "url": "https://shopify.dev/app/scopes" - } - } - ] - } - }, - { - "type": "table", - "title": "Table of available themes", - "description": "This step demonstrates a table output showing a list of available themes.", - "properties": { - "rows": [ - { - "ID": "1", - "Name": "Debut", - "Role": "main" - }, - { - "ID": "2", - "Name": "Simple", - "Role": "unpublished" - } - ], - "columns": { - "ID": { - "header": "ID", - "color": "red" - }, - "Name": { - "header": "Name" - }, - "Role": { - "header": "Role", - "color": "dim" - } - } - } - }, - { - "type": "table", - "title": "Table of apps and earned income", - "description": "A table output showing a list of apps and their earned income. Uses non-string values.", - "properties": { - "rows": [ - { - "App": "My App", - "Earned": 1000 - }, - { - "App": "My Other App", - "Earned": 2000 - } - ], - "columns": { - "App": { - "header": "App" - }, - "Earned": { - "header": "Earned", - "color": "green" - } - } - } - }, - { - "type": "fatalError", - "title": "BugError: Something went wrong", - "description": "Show a fatal error message indicating something unexpected went wrong.", - "properties": { - "errorType": "bug", - "message": "Something went wrong", - "tryMessage": "Run this command again." - } - }, - { - "type": "fatalError", - "title": "AbortError: No Organization found", - "description": "Show a fatal error message indicating no organization was found.", - "properties": { - "errorType": "abort", - "message": "No Organization found", - "tryMessage": "Run this command again after creating and confirming your organization.", - "nextSteps": [ - [ - "Have you", - { - "link": { - "label": "created a Shopify Partners organization", - "url": "https://partners.shopify.com/signup" - } - }, - { - "char": "?" - } - ], - "Have you confirmed your accounts from the emails you received?", - [ - "Need to connect to a different App or organization? Run the command again with", - { - "command": "--reset" - } - ] - ] - } - }, - { - "type": "selectPrompt", - "title": "Choose an extension type", - "description": "Select prompt for choosing an extension type, using groups and custom keys.", - "properties": { - "message": "Choose an extension type:", - "choices": [ - { - "label": "Checkout UI extension", - "value": "checkout-ui-extension", - "key": "c", - "group": "Checkout extensions" - }, - { - "label": "Checkout post-purchase UI extension", - "value": "checkout-post-purchase-extension", - "key": "p", - "group": "Checkout extensions" - }, - { - "label": "Discount function", - "value": "discount-function", - "key": "d", - "group": "Functions" - }, - { - "label": "Theme app extension", - "value": "theme-app-extension", - "key": "t", - "group": "Theme extensions" - } - ] - } - }, - { - "type": "selectPrompt", - "title": "Choose a plan", - "description": "Select prompt for choosing a plan, including an infoTable with plan information.", - "properties": { - "message": "Choose a plan", - "choices": [ - { - "label": "Free", - "value": "free" - }, - { - "label": "Basic", - "value": "basic" - }, - { - "label": "Pro", - "value": "pro" - }, - { - "label": "Enterprise", - "value": "enterprise" - } - ], - "infoTable": [ - { - "header": "Free Plan", - "helperText": "The Free Plan is a good fit for merchants who are just starting out.", - "items": [ - "Price: $0.00 USD", - [ - "Features:", - {"bold": "Minimal"}, - "features" - ] - ] - }, - { - "header": "Basic", - "helperText": "The Basic Plan includes the most important features for a growing business.", - "items": [ - "Price: $9.99 USD", - [ - "Features:", - {"bold": "Some"}, - "features" - ] - ] - }, - { - "header": "Pro", - "helperText": "The Pro Plan includes all the features you need to scale your business.", - "items": [ - "Price: $19.99 USD", - [ - "Features:", - {"bold": "Most"}, - "features" - ] - ] - }, - { - "header": "Enterprise", - "helperText": "The Enterprise Plan allows you to customize our product to fit your business needs.", - "items": [ - "Price: Contact us", - [ - "Features:", - {"bold": "All"}, - "features", - {"subdued": "(for a price 😉)"} - ] - ] - } - ] - } - }, - { - "type": "textPrompt", - "title": "App name prompt with default value", - "description": "A text prompt for entering an app name.", - "properties": { - "message": "Enter an app name", - "defaultValue": "expansive-commerce-app", - "allowEmpty": false, - "password": false - } - }, - { - "type": "textPrompt", - "title": "Required text prompt without a default value", - "description": "A text prompt for entering a value without a default value. Some input is required.", - "properties": { - "message": "How are you feeling today?", - "allowEmpty": false, - "password": false - } - }, - { - "type": "textPrompt", - "title": "Optional text prompt without a default value", - "description": "A text prompt for entering a value without a default value. The prompt can be skipped.", - "properties": { - "message": "To change the default behavior to release by default, enter the following exactly: \u001b[1mdefault-to-release\u001b[0m.\n\nOtherwise, press enter to create a version without releasing", - "allowEmpty": true, - "password": false - } - }, - { - "type": "textPrompt", - "title": "App secret prompt", - "description": "A text prompt for entering secret information.", - "properties": { - "message": "Enter your app secret", - "allowEmpty": false, - "password": true - } - }, - { - "type": "autocompletePrompt", - "title": "Autocomplete select an app", - "description": "An autocomplete prompt for selecting an app from many options", - "properties": { - "message": "Which app do you want to connect to?", - "choices": [ - {"label": "Expansive commerce app", "value": "expansive-commerce-app"}, - {"label": "Profitable ecosystem app", "value": "profitable-ecosystem-app"}, - {"label": "Sustainable business app", "value": "sustainable-business-app"}, - {"label": "Amortizable account app", "value": "amortizable-account-app"}, - {"label": "Branded shipping app", "value": "branded-shipping-app"}, - {"label": "Efficient inventory app", "value": "efficient-inventory-app"}, - {"label": "Streamlined payroll app", "value": "streamlined-payroll-app"}, - {"label": "Innovative marketing app", "value": "innovative-marketing-app"}, - {"label": "Integrated CRM app", "value": "integrated-crm-app"}, - {"label": "Secure payment app", "value": "secure-payment-app"}, - {"label": "Automated billing app", "value": "automated-billing-app"}, - {"label": "Customizable storefront app", "value": "customizable-storefront-app"}, - {"label": "User-friendly analytics app", "value": "user-friendly-analytics-app"}, - {"label": "Responsive customer service app", "value": "responsive-customer-service-app"}, - {"label": "Flexible scheduling app", "value": "flexible-scheduling-app"}, - {"label": "Seamless checkout app", "value": "seamless-checkout-app"}, - {"label": "Dynamic pricing app", "value": "dynamic-pricing-app"}, - {"label": "Interactive product app", "value": "interactive-product-app"}, - {"label": "Collaborative project management app", "value": "collaborative-project-management-app"}, - {"label": "Real-time inventory tracking app", "value": "real-time-inventory-tracking-app"}, - {"label": "Integrated shipping app", "value": "integrated-shipping-app"}, - {"label": "Effortless returns app", "value": "effortless-returns-app"}, - {"label": "Intuitive dashboard app", "value": "intuitive-dashboard-app"}, - {"label": "Powerful reporting app", "value": "powerful-reporting-app"}, - {"label": "Automated fulfillment app", "value": "automated-fulfillment-app"}, - {"label": "Customizable branding app", "value": "customizable-branding-app"}, - {"label": "Seamless integration app", "value": "seamless-integration-app"}, - {"label": "Secure data management app", "value": "secure-data-management-app"}, - {"label": "Intelligent upselling app", "value": "intelligent-upselling-app"}, - {"label": "Virtual appointment app", "value": "virtual-appointment-app"}, - {"label": "Personalized recommendations app", "value": "personalized-recommendations-app"}, - {"label": "Interactive FAQ app", "value": "interactive-faq-app"}, - {"label": "Automated email marketing app", "value": "automated-email-marketing-app"}, - {"label": "Dynamic discount app", "value": "dynamic-discount-app"}, - {"label": "Collaborative design app", "value": "collaborative-design-app"}, - {"label": "Real-time chat app", "value": "real-time-chat-app"}, - {"label": "Integrated accounting app", "value": "integrated-accounting-app"}, - {"label": "Effortless onboarding app", "value": "effortless-onboarding-app"}, - {"label": "Intuitive search app", "value": "intuitive-search-app"}, - {"label": "Powerful SEO app", "value": "powerful-seo-app"}, - {"label": "Automated inventory management app", "value": "automated-inventory-management-app"}, - {"label": "Customizable checkout app", "value": "customizable-checkout-app"}, - {"label": "Seamless order tracking app", "value": "seamless-order-tracking-app"}, - {"label": "Secure login app", "value": "secure-login-app"}, - {"label": "Intelligent recommendations app", "value": "intelligent-recommendations-app"}, - {"label": "Virtual try-on app", "value": "virtual-try-on-app"}, - {"label": "Personalized content app", "value": "personalized-content-app"}, - {"label": "Interactive product tour app", "value": "interactive-product-tour-app"}, - {"label": "Automated customer service app", "value": "automated-customer-service-app"}, - {"label": "Dynamic upselling app", "value": "dynamic-upselling-app"} - ] - } - }, - { - "type": "confirmationPrompt", - "title": "Confirm adding themes to store", - "description": "Confirmation prompt for adding themes to a store. Includes an info table of themes.", - "properties": { - "message": "Add the following themes to the store?", - "infoTable": { - "": [ - [ - "first theme", - { - "subdued": "(#1)" - } - ], - [ - "second theme", - { - "subdued": "(#2)" - } - ] - ] - }, - "confirmationMessage": "Yes, add them", - "cancellationMessage": "Cancel" - } - }, - { - "type": "confirmationPrompt", - "title": "Dangerous prompt: Confirm deleting themes from the store", - "description": "Confirmation prompt for deleting themes from a store. Includes an info table of themes, defaults to false", - "properties": { - "message": "Delete the following themes from the store?", - "infoTable": { - "": [ - [ - "first theme", - { - "subdued": "(#1)" - } - ], - [ - "second theme", - { - "subdued": "(#2)" - } - ] - ] - }, - "confirmationMessage": "Yes, delete them", - "cancellationMessage": "Cancel", - "defaultValue": false - } - }, - { - "type": "confirmationPrompt", - "title": "Confirm file changes", - "description": "Confirmation prompt for changing a file, including a git diff of the changes", - "properties": { - "message": [ - "Commit the following changes to", - {"filePath": ".env"}, - {"char": "?"} - ], - "gitDiff": { - "baselineContent": "PUBLIC_STOREFRONT_ID=\"12345678\"\nPUBLIC_STOREFRONT_TOKEN=\"abcdefgh\"\n", - "updatedContent": "PUBLIC_STOREFRONT_ID=\"87654321\"\nPUBLIC_STOREFRONT_TOKEN=\"abcdefgh\"\nPRIVATE_API_KEY=\"apik_5101520\"\n" - }, - "confirmationMessage": "Yes, accept these changes", - "cancellationMessage": "Cancel" - } - }, - { - "type": "sleep", - "title": "Sleep 1 second", - "description": "Sleep to simulate waiting at the terminal with no visual activity", - "properties": { - "duration": 1 - } - }, - { - "type": "taskbar", - "title": "App creation progress", - "description": "Taskbar showing progress through the steps of the app creation process.", - "properties": { - "steps": [ - { - "title": "Creating app", - "duration": 2 - }, - { - "title": "Setting up environment", - "duration": 1 - }, - { - "title": "Installing dependencies", - "duration": 3 - } - ] - } - }, - { - "type": "concurrent", - "title": "Concurrent processes", - "description": "Simple example of concurrent processes with a server and file watcher", - "properties": { - "processes": [ - { - "prefix": "Server", - "steps": [ - { - "startMessage": "Starting server...", - "duration": 2, - "endMessage": "Server started at http://localhost:3000" - } - ] - }, - { - "prefix": "Watcher", - "steps": [ - { - "startMessage": "Starting file watcher...", - "duration": 1, - "endMessage": "File watcher started" - } - ] - } - ], - "footer": { - "shortcuts": [ - { - "key": "p", - "action": "preview in your browser" - }, - { - "key": "q", - "action": "quit" - }, - { - "key": "r", - "action": "restart" - } - ], - "subTitle": "Preview URL: https://shopify.com" - } - } - }, - { - "type": "concurrent", - "title": "Realistic concurrent processes: App dev", - "description": "Realistic example of concurrent processes during app dev", - "properties": { - "footer": { - "shortcuts": [ - { - "key": "p", - "action": "preview in your browser" - }, - { - "key": "q", - "action": "quit" - } - ], - "subTitle": "Preview URL: https://thunder-gmbh-desperate-lo.trycloudflare.com/extensions/dev-console" - }, - "processes": [ - { - "prefix": "frontend", - "steps": [ - { - "duration": 0, - "endMessage": "\n> dev\n> vite\n" - }, - { - "duration": 0.593, - "endMessage": "\n vite v2.9.15 dev server running at:\n" - }, - { - "duration": 0.002, - "endMessage": " > Local: http://localhost:64382/\n > Network: http://127.0.2.2:64382/\n > Network: http://127.0.2.3:64382/\n > Network: http://192.168.1.112:64382/\n > Network: http://172.16.0.2:64382/\n\n ready in 140ms.\n" - } - ] - }, - { - "prefix": "backend", - "steps": [ - { - "duration": 0.003, - "endMessage": "\n> dev\n> cross-env NODE_ENV=development nodemon index.js --ignore ./frontend\n" - }, - { - "duration": 0.595, - "endMessage": "[nodemon] 2.0.22\n[nodemon] to restart at any time, enter `rs`\n[nodemon] watching path(s): *.*\n[nodemon] watching extensions: js,mjs,json\n[nodemon] starting `node index.js`" - }, - { - "duration": 0.223, - "endMessage": "[shopify-api/INFO] version 6.2.0, environment Node v20.0.0" - } - ] - } - ] - } - } - ] -} diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index 8d59f55cd6..4b70501d0d 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -2297,123 +2297,6 @@ "pluginType": "core", "strict": true }, - "demo": { - "aliases": [ - ], - "args": { - }, - "description": "Demo a command design defined in a file", - "enableJsonFlag": false, - "flags": { - "file": { - "default": ".", - "description": "The name of the demo file.", - "env": "SHOPIFY_FLAG_PATH", - "hasDynamicHelp": false, - "hidden": false, - "multiple": false, - "name": "file", - "type": "option" - }, - "path": { - "default": ".", - "description": "The directory where the demo file is located. Defaults to the current directory.", - "env": "SHOPIFY_FLAG_PATH", - "hasDynamicHelp": false, - "hidden": false, - "multiple": false, - "name": "path", - "type": "option" - } - }, - "hasDynamicHelp": false, - "hidden": true, - "hiddenAliases": [ - ], - "id": "demo", - "pluginAlias": "@shopify/cli", - "pluginName": "@shopify/cli", - "pluginType": "core", - "strict": true - }, - "demo:catalog": { - "aliases": [ - ], - "args": { - }, - "description": "Browse the catalog for steps", - "enableJsonFlag": false, - "flags": { - }, - "hasDynamicHelp": false, - "hidden": true, - "hiddenAliases": [ - ], - "id": "demo:catalog", - "pluginAlias": "@shopify/cli", - "pluginName": "@shopify/cli", - "pluginType": "core", - "strict": true - }, - "demo:generate-file": { - "aliases": [ - ], - "args": { - }, - "description": "Create a command design file", - "enableJsonFlag": false, - "flags": { - "file": { - "description": "The name of the demo file.", - "env": "SHOPIFY_FLAG_FILENAME", - "hasDynamicHelp": false, - "hidden": false, - "multiple": false, - "name": "file", - "required": true, - "type": "option" - }, - "path": { - "default": ".", - "description": "The directory for generating the demo file.", - "env": "SHOPIFY_FLAG_PATH", - "hasDynamicHelp": false, - "hidden": false, - "multiple": false, - "name": "path", - "type": "option" - } - }, - "hasDynamicHelp": false, - "hidden": true, - "hiddenAliases": [ - ], - "id": "demo:generate-file", - "pluginAlias": "@shopify/cli", - "pluginName": "@shopify/cli", - "pluginType": "core", - "strict": true, - "summary": "Creates a JSON file alongside a JSON schema that will validate it" - }, - "demo:print-ai-prompt": { - "aliases": [ - ], - "args": { - }, - "description": "Prints the prompts for a chat-enabled LLM to generate a demo", - "enableJsonFlag": false, - "flags": { - }, - "hasDynamicHelp": false, - "hidden": true, - "hiddenAliases": [ - ], - "id": "demo:print-ai-prompt", - "pluginAlias": "@shopify/cli", - "pluginName": "@shopify/cli", - "pluginType": "core", - "strict": true - }, "demo:watcher": { "aliases": [ ], diff --git a/packages/cli/package.json b/packages/cli/package.json index f683b58728..eb8e73f6ee 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -118,8 +118,7 @@ "@typescript-eslint/eslint-plugin": "7.13.1", "@vitest/coverage-istanbul": "^1.6.0", "esbuild-plugin-copy": "^2.1.1", - "espree": "9.6.1", - "zod-to-json-schema": "3.21.4" + "espree": "9.6.1" }, "engines": { "node": "^18.20.0 || >=20.10.0" diff --git a/packages/cli/src/cli/commands/demo/catalog.ts b/packages/cli/src/cli/commands/demo/catalog.ts deleted file mode 100644 index a35c219b8a..0000000000 --- a/packages/cli/src/cli/commands/demo/catalog.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ -import {demo, DemoStep} from '../../services/demo.js' -import Command from '@shopify/cli-kit/node/base-command' -import {readFile} from '@shopify/cli-kit/node/fs' -import {joinPath} from '@shopify/cli-kit/node/path' -import {outputInfo} from '@shopify/cli-kit/node/output' -import {renderAutocompletePrompt} from '@shopify/cli-kit/node/ui' -import {fileURLToPath} from 'url' - -export default class Catalog extends Command { - static description = 'Browse the catalog for steps' - static hidden = true - - async run(): Promise { - const catalogFile = joinPath(fileURLToPath(import.meta.url), '../../../../../assets/demo-catalog.json') - const {steps} = JSON.parse(await readFile(catalogFile)) as {steps: DemoStep[]} - const stepSelection = await renderAutocompletePrompt({ - message: 'Step to display', - choices: steps.map(({title, type}) => { - return { - label: title!, - value: title!, - group: type, - } - }), - }) - const selectedStep = steps.find(({title}) => title === stepSelection)! - outputInfo('The step looks like this:') - await demo({steps: [selectedStep]}) - outputInfo('JSON for this step:') - outputInfo(JSON.stringify(selectedStep, null, 2)) - } -} diff --git a/packages/cli/src/cli/commands/demo/generate-file.ts b/packages/cli/src/cli/commands/demo/generate-file.ts deleted file mode 100644 index 9e6b8a6345..0000000000 --- a/packages/cli/src/cli/commands/demo/generate-file.ts +++ /dev/null @@ -1,96 +0,0 @@ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ -import {demoStepsSchema, DemoStep} from '../../services/demo.js' -import zodToJsonSchema from 'zod-to-json-schema' -import {Flags} from '@oclif/core' -import Command from '@shopify/cli-kit/node/base-command' -import {AbortError} from '@shopify/cli-kit/node/error' -import {mkdir, fileExists, readFile, writeFile} from '@shopify/cli-kit/node/fs' -import {outputContent, outputSuccess, outputToken} from '@shopify/cli-kit/node/output' -import {resolvePath, joinPath, cwd} from '@shopify/cli-kit/node/path' -import {renderAutocompletePrompt} from '@shopify/cli-kit/node/ui' -import {createRequire} from 'module' - -const schemaFilename = 'demo-schema.json' - -export default class GenerateFile extends Command { - static description = 'Create a command design file' - static summary = 'Creates a JSON file alongside a JSON schema that will validate it' - static hidden = true - - static flags = { - path: Flags.string({ - hidden: false, - description: 'The directory for generating the demo file.', - env: 'SHOPIFY_FLAG_PATH', - parse: async (input) => resolvePath(input), - default: async () => cwd(), - }), - file: Flags.string({ - hidden: false, - description: 'The name of the demo file.', - env: 'SHOPIFY_FLAG_FILENAME', - required: true, - validate: (input: string) => { - if (input === schemaFilename) { - return `The demo file can't be named ${schemaFilename}, as this is used for the schema file.` - } - return true - }, - }), - } - - async run(): Promise { - const {flags} = await this.parse(GenerateFile) - await mkdir(flags.path) - const demoFilePath = joinPath(flags.path, flags.file) - if (await fileExists(demoFilePath)) { - throw new AbortError(`The file ${demoFilePath} already exists.`) - } - const demoSchemaPath = joinPath(flags.path, schemaFilename) - const jsonSchema = zodToJsonSchema.default(demoStepsSchema, 'demo-steps') - await Promise.all([ - writeFile(demoSchemaPath, JSON.stringify(jsonSchema, null, 2)), - writeFile( - demoFilePath, - JSON.stringify( - { - $schema: `./${schemaFilename}`, - steps: await selectSteps(), - }, - null, - 2, - ), - ), - ]) - outputSuccess(outputContent`Created ${outputToken.path(demoFilePath)} and ${outputToken.path(demoSchemaPath)}`) - } -} - -async function selectSteps(): Promise { - const require = createRequire(import.meta.url) - const catalogFile = require.resolve('@shopify/cli/assets/demo-catalog.json') - const {steps} = JSON.parse(await readFile(catalogFile)) as {steps: DemoStep[]} - const selectedSteps: DemoStep[] = [] - while (true) { - // eslint-disable-next-line no-await-in-loop - const stepSelection = await renderAutocompletePrompt({ - message: 'Add a step to the demo file', - choices: [ - { - label: "I'm done", - value: 'done', - }, - ...steps.map(({title, type}) => { - return { - label: title!, - value: title!, - group: type, - } - }), - ], - }) - if (stepSelection === 'done') break - selectedSteps.push(steps.find(({title}) => title === stepSelection)!) - } - return selectedSteps -} diff --git a/packages/cli/src/cli/commands/demo/index.ts b/packages/cli/src/cli/commands/demo/index.ts deleted file mode 100644 index 2d27c34e4c..0000000000 --- a/packages/cli/src/cli/commands/demo/index.ts +++ /dev/null @@ -1,34 +0,0 @@ -import {demo} from '../../services/demo.js' -import {Flags} from '@oclif/core' -import Command from '@shopify/cli-kit/node/base-command' -import {readFile} from '@shopify/cli-kit/node/fs' -import {resolvePath, cwd} from '@shopify/cli-kit/node/path' - -export default class Demo extends Command { - static description = 'Demo a command design defined in a file' - static hidden = true - - static flags = { - path: Flags.string({ - hidden: false, - description: 'The directory where the demo file is located. Defaults to the current directory.', - env: 'SHOPIFY_FLAG_PATH', - parse: async (input) => resolvePath(input), - default: async () => cwd(), - }), - file: Flags.string({ - hidden: false, - description: 'The name of the demo file.', - env: 'SHOPIFY_FLAG_PATH', - parse: async (input) => resolvePath(input), - default: async () => cwd(), - }), - } - - async run(): Promise { - const {flags} = await this.parse(Demo) - const contents = await readFile(flags.file) - const design = JSON.parse(contents) - await demo(design) - } -} diff --git a/packages/cli/src/cli/commands/demo/print-ai-prompt.ts b/packages/cli/src/cli/commands/demo/print-ai-prompt.ts deleted file mode 100644 index d7c6a85664..0000000000 --- a/packages/cli/src/cli/commands/demo/print-ai-prompt.ts +++ /dev/null @@ -1,40 +0,0 @@ -import {demoStepsSchema} from '../../services/demo.js' -import zodToJsonSchema from 'zod-to-json-schema' -import Command from '@shopify/cli-kit/node/base-command' -import {renderInfo} from '@shopify/cli-kit/node/ui' -import {outputInfo} from '@shopify/cli-kit/node/output' - -export default class PrintAIPrompt extends Command { - static description = 'Prints the prompts for a chat-enabled LLM to generate a demo' - static hidden = true - - async run(): Promise { - const jsonSchema = zodToJsonSchema.default(demoStepsSchema, 'demo-steps') - const printable = JSON.stringify(jsonSchema) - renderInfo({body: 'Copy and paste the following into the system prompt.'}) - outputInfo(`You are creating a demo for a Shopify CLI command, using a strictly typed JSON file. -The file defines the steps that will be executed during the demo. - -The JSON schema for this file is: -\`\`\`json -${printable} -\`\`\` -`) - renderInfo({body: 'Then, fill out the following and paste it into the chat box.'}) - - outputInfo(`Generate a human-readable JSON file which will be used to create the demo. The JSON file must be typed according to the JSON schema. - -The purpose of the command is: {A short description of the command.} - -The demo should perform the following steps: - -{ - List the steps for the command, like: - 1. Prompt for this - 2. Autocomplete that -} - -============================================================ -`) - } -} diff --git a/packages/cli/src/cli/services/demo.test.ts b/packages/cli/src/cli/services/demo.test.ts deleted file mode 100644 index e360292cc5..0000000000 --- a/packages/cli/src/cli/services/demo.test.ts +++ /dev/null @@ -1,13 +0,0 @@ -import {demoStepsSchema} from './demo.js' -import {readFile} from '@shopify/cli-kit/node/fs' -import {dirname, joinPath} from '@shopify/cli-kit/node/path' -import {describe, expect, test} from 'vitest' -import {fileURLToPath} from 'url' - -describe('demoStepsSchema', () => { - test('validates catalog', async () => { - const filePath = joinPath(fileURLToPath(dirname(import.meta.url)), '../../../assets/demo-catalog.json') - const catalogContents = demoStepsSchema.parse(JSON.parse(await readFile(filePath))) - expect(catalogContents.command).toEqual('shopify demo') - }) -}) diff --git a/packages/cli/src/cli/services/demo.ts b/packages/cli/src/cli/services/demo.ts deleted file mode 100644 index 1449498e47..0000000000 --- a/packages/cli/src/cli/services/demo.ts +++ /dev/null @@ -1,398 +0,0 @@ -import {AbortError, BugError} from '@shopify/cli-kit/node/error' -import {outputInfo} from '@shopify/cli-kit/node/output' -import {sleep} from '@shopify/cli-kit/node/system' -import { - renderFatalError, - renderInfo, - renderSuccess, - renderTable, - renderConcurrent, - renderTasks, - renderWarning, - renderAutocompletePrompt, - renderConfirmationPrompt, - renderSelectPrompt, - renderTextPrompt, -} from '@shopify/cli-kit/node/ui' -import {zod} from '@shopify/cli-kit/node/schema' -import {Writable} from 'stream' - -function oneOrMore(singular: zod.ZodType) { - return zod.union([singular, zod.array(singular)]) -} -const scalar = zod.union([zod.string(), zod.number(), zod.boolean(), zod.null(), zod.undefined()]) -const linkSchema = zod.object({label: zod.string(), url: zod.string()}) -const inlineTokenSchema = zod.union([ - zod.string(), - zod.object({command: zod.string()}), - zod.object({link: linkSchema}), - zod.object({char: zod.string().length(1)}), - zod.object({userInput: zod.string()}), - zod.object({subdued: zod.string()}), - zod.object({filePath: zod.string()}), - zod.object({bold: zod.string()}), -]) -const headlineTokenSchema = oneOrMore( - zod.union([ - zod.string(), - zod.object({command: zod.string()}), - zod.object({char: zod.string().length(1)}), - zod.object({userInput: zod.string()}), - zod.object({subdued: zod.string()}), - zod.object({filePath: zod.string()}), - ]), -) -// type InlineToken = zod.infer -const inlineTokenItemSchema = oneOrMore(inlineTokenSchema) -// type InlineTokenItem = zod.infer -const listSchema = zod.object({ - list: zod.object({ - title: zod.string().optional(), - items: zod.array(inlineTokenItemSchema), - ordered: zod.boolean().optional(), - }), -}) -const tokenItemSchema = oneOrMore(zod.union([inlineTokenSchema, listSchema])) - -const tableSchema = zod.object({ - rows: zod.array(zod.object({}).catchall(scalar)), - columns: zod.object({}).catchall( - zod.object({ - header: zod.string().optional(), - color: zod.string().optional(), - }), - ), -}) -const infoTableSchema = zod.union([ - zod.object({}).catchall(zod.array(inlineTokenItemSchema)), - zod.array( - zod.object({ - color: zod.string().optional(), - header: zod.string(), - helperText: zod.string().optional(), - bullet: zod.string().optional(), - items: zod.array(inlineTokenItemSchema), - }), - ), -]) - -const abstractDemoStepSchema = zod.object({ - type: zod.string(), - properties: zod.object({}), - // optional properties for documentation purposes - title: zod.string().optional(), - description: zod.string().optional(), -}) - -const outputStepSchema = abstractDemoStepSchema.extend({ - type: zod.literal('output'), - properties: zod.object({ - content: zod.string(), - }), -}) -type OutputStep = zod.infer - -const renderStepPropertiesSchema = zod.object({ - headline: headlineTokenSchema.optional(), - body: tokenItemSchema.optional(), - nextSteps: zod.array(inlineTokenItemSchema).optional(), - reference: zod.array(inlineTokenItemSchema).optional(), - link: linkSchema.optional(), - customSections: zod - .array( - zod.object({ - title: zod.string().optional(), - body: tokenItemSchema, - }), - ) - .optional(), - orderedNextSteps: zod.boolean().optional(), -}) -const renderInfoStepSchema = abstractDemoStepSchema.extend({ - type: zod.literal('info'), - properties: renderStepPropertiesSchema, -}) -type RenderInfoStep = zod.infer -const renderSuccessStepSchema = abstractDemoStepSchema.extend({ - type: zod.literal('success'), - properties: renderStepPropertiesSchema, -}) -type RenderSuccessStep = zod.infer -const renderWarningStepSchema = abstractDemoStepSchema.extend({ - type: zod.literal('warning'), - properties: renderStepPropertiesSchema, -}) -type RenderWarningStep = zod.infer - -const renderFatalErrorStepSchema = abstractDemoStepSchema.extend({ - type: zod.literal('fatalError'), - properties: zod.object({ - errorType: zod.union([zod.literal('abort'), zod.literal('bug')]), - message: zod.string(), - tryMessage: zod.string().optional(), - nextSteps: zod.array(inlineTokenItemSchema).optional(), - }), -}) -type RenderFatalErrorStep = zod.infer - -const renderTableStepSchema = abstractDemoStepSchema.extend({ - type: zod.literal('table'), - properties: tableSchema, -}) -type RenderTableStep = zod.infer - -const renderAutoCompletePromptStepSchema = abstractDemoStepSchema.extend({ - type: zod.literal('autocompletePrompt'), - properties: zod.object({ - message: zod.string(), - choices: zod.array( - zod.object({ - label: zod.string(), - value: zod.string(), - }), - ), - }), -}) -type RenderAutocompletePromptStep = zod.infer - -const renderConfirmationPromptStepSchema = abstractDemoStepSchema.extend({ - type: zod.literal('confirmationPrompt'), - properties: zod.object({ - message: headlineTokenSchema, - infoTable: infoTableSchema.optional(), - defaultValue: zod.boolean().optional(), - confirmationMessage: zod.string(), - cancellationMessage: zod.string(), - }), -}) -type RenderConfirmationPromptStep = zod.infer - -const renderSelectPromptStepSchema = abstractDemoStepSchema.extend({ - type: zod.literal('selectPrompt'), - properties: zod.object({ - message: headlineTokenSchema, - choices: zod.array( - zod.object({ - label: zod.string(), - value: zod.string(), - key: zod.string().length(1).optional(), - group: zod.string().optional(), - disabled: zod.boolean().optional(), - }), - ), - defaultValue: zod.string().optional(), - infoTable: infoTableSchema.optional(), - }), -}) -type RenderSelectPromptStep = zod.infer - -const renderTextPromptStepSchema = abstractDemoStepSchema.extend({ - type: zod.literal('textPrompt'), - properties: zod.object({ - message: zod.string(), - defaultValue: zod.string().optional(), - password: zod.boolean().optional(), - allowEmpty: zod.boolean().optional(), - }), -}) -type RenderTextPromptStep = zod.infer - -const sleepStepSchema = abstractDemoStepSchema.extend({ - type: zod.literal('sleep'), - properties: zod.object({ - duration: zod.number(), - }), -}) -type SleepStep = zod.infer - -const taskbarStepSchema = abstractDemoStepSchema.extend({ - type: zod.literal('taskbar'), - properties: zod.object({ - steps: zod.array( - zod.object({ - title: zod.string(), - duration: zod.number(), - }), - ), - }), -}) -type TaskbarStep = zod.infer - -const renderConcurrentPropertiesSchema = zod.object({ - processes: zod.array( - zod.object({ - prefix: zod.string(), - steps: zod.array( - zod.object({ - startMessage: zod.string().optional(), - duration: zod.number(), - endMessage: zod.string().optional(), - }), - ), - }), - ), -}) -type RenderConcurrentProperties = zod.infer -const renderConcurrentStepSchema = abstractDemoStepSchema.extend({ - type: zod.literal('concurrent'), - properties: renderConcurrentPropertiesSchema, -}) -type RenderConcurrentStep = zod.infer - -export type DemoStep = - | OutputStep - | RenderInfoStep - | RenderSuccessStep - | RenderWarningStep - | RenderTableStep - | RenderFatalErrorStep - | RenderAutocompletePromptStep - | RenderConfirmationPromptStep - | RenderSelectPromptStep - | RenderTextPromptStep - | SleepStep - | TaskbarStep - | RenderConcurrentStep - -const demoStepSchema = zod.discriminatedUnion('type', [ - outputStepSchema, - renderInfoStepSchema, - renderSuccessStepSchema, - renderWarningStepSchema, - renderTableStepSchema, - renderFatalErrorStepSchema, - renderAutoCompletePromptStepSchema, - renderConfirmationPromptStepSchema, - renderSelectPromptStepSchema, - renderTextPromptStepSchema, - sleepStepSchema, - taskbarStepSchema, - renderConcurrentStepSchema, -]) -export const demoStepsSchema = zod.object({ - $schema: zod.string().optional(), - command: zod.string().optional(), - steps: zod.array(demoStepSchema), -}) -type DemoSteps = zod.infer - -export async function demo(stepsJsonData: DemoSteps) { - const {steps, command} = demoStepsSchema.parse(stepsJsonData) - const executors = steps.map(executorForStep) - - await simulateTyping(command) - for (const executor of executors) { - // eslint-disable-next-line no-await-in-loop - await executor() - } -} - -async function simulateTyping(text?: string) { - if (!text) return - - // eslint-disable-next-line no-console - console.clear() - process.stdout.write('$ ') - const chars = text.split('') - while (chars.length > 0) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const char = chars.shift()! - process.stdout.write(char) - // eslint-disable-next-line no-await-in-loop - await sleep(0.1 + Math.random() / 10) - } - process.stdout.write('\n') - await sleep(1 + Math.random() / 10) -} - -function executorForStep(step: DemoStep): () => Promise { - switch (step.type) { - case 'output': - return async () => { - outputInfo(step.properties.content) - } - case 'sleep': - return async () => { - await sleep(step.properties.duration) - } - case 'taskbar': - return taskbarExecutor(step.properties.steps) - case 'concurrent': - return concurrentExecutor(step.properties) - case 'info': - return async () => { - renderInfo(step.properties) - } - case 'success': - return async () => { - renderSuccess(step.properties) - } - case 'warning': - return async () => { - renderWarning(step.properties) - } - case 'fatalError': - return async () => { - const {errorType, message, nextSteps, tryMessage} = step.properties - if (errorType === 'abort') { - renderFatalError(new AbortError(message, tryMessage, nextSteps)) - } else { - renderFatalError(new BugError(message, tryMessage)) - } - } - case 'table': - return async () => { - renderTable(step.properties as Parameters[0]) - } - case 'autocompletePrompt': - return async () => { - await renderAutocompletePrompt(step.properties) - } - case 'confirmationPrompt': - return async () => { - await renderConfirmationPrompt(step.properties as Parameters[0]) - } - case 'selectPrompt': - return async () => { - await renderSelectPrompt(step.properties as Parameters[0]) - } - case 'textPrompt': - return async () => { - await renderTextPrompt(step.properties) - } - default: - throw new Error(`Unknown step type: ${(step as DemoStep).type}`) - } -} - -function taskbarExecutor(steps: {title: string; duration: number}[]) { - return async () => { - const tasks = steps.map(({title, duration}) => { - return { - title, - task: async () => sleep(duration), - } - }) - await renderTasks(tasks) - } -} - -function concurrentExecutor({processes}: RenderConcurrentProperties) { - return async () => { - const concurrentProcesses = processes.map(({prefix, steps}) => { - return { - prefix, - action: async (stdout: Writable) => { - for (const step of steps) { - const {startMessage, duration, endMessage} = step - if (startMessage) stdout.write(startMessage) - // eslint-disable-next-line no-await-in-loop - await sleep(duration) - if (endMessage) stdout.write(endMessage) - } - }, - } - }) - await renderConcurrent({processes: concurrentProcesses}) - } -} diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 5dab8c5f2c..3d36993ec5 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -4,10 +4,6 @@ import Search from './cli/commands/search.js' import Upgrade from './cli/commands/upgrade.js' import Logout from './cli/commands/auth/logout.js' import CommandFlags from './cli/commands/debug/command-flags.js' -import Demo from './cli/commands/demo/index.js' -import Catalog from './cli/commands/demo/catalog.js' -import GenerateFile from './cli/commands/demo/generate-file.js' -import PrintAIPrompt from './cli/commands/demo/print-ai-prompt.js' import KitchenSinkAsync from './cli/commands/kitchen-sink/async.js' import KitchenSinkPrompts from './cli/commands/kitchen-sink/prompts.js' import KitchenSinkStatic from './cli/commands/kitchen-sink/static.js' @@ -23,7 +19,7 @@ import {commands as AppCommands} from '@shopify/app' import {commands as PluginCommandsCommands} from '@oclif/plugin-commands' import {commands as PluginPluginsCommands} from '@oclif/plugin-plugins' import {DidYouMeanCommands} from '@shopify/plugin-did-you-mean' -import {runCLI, useLocalCLIIfDetected} from '@shopify/cli-kit/node/cli' +import {runCLI} from '@shopify/cli-kit/node/cli' import {renderFatalError} from '@shopify/cli-kit/node/ui' import {FatalError} from '@shopify/cli-kit/node/error' import fs from 'fs' @@ -70,14 +66,6 @@ interface RunShopifyCLIOptions { } async function runShopifyCLI({development}: RunShopifyCLIOptions) { - if (!development) { - // If we run a local CLI instead, don't run the global one again after! - const ranLocalInstead = await useLocalCLIIfDetected(import.meta.url) - if (ranLocalInstead) { - return - } - } - await runCLI({ moduleURL: import.meta.url, development, @@ -140,10 +128,6 @@ export const COMMANDS: any = { help: HelpCommand, 'auth:logout': Logout, 'debug:command-flags': CommandFlags, - demo: Demo, - 'demo:catalog': Catalog, - 'demo:generate-file': GenerateFile, - 'demo:print-ai-prompt': PrintAIPrompt, 'kitchen-sink': KitchenSink, 'kitchen-sink:async': KitchenSinkAsync, 'kitchen-sink:prompts': KitchenSinkPrompts, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2070c00fc0..f04e554ccc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -279,7 +279,7 @@ importers: version: link:../app '@shopify/cli-hydrogen': specifier: 9.0.2 - version: 9.0.2(@graphql-codegen/cli@5.0.2)(react-dom@17.0.2)(react@17.0.2) + version: 9.0.2(react-dom@18.2.0)(react@18.2.0) '@shopify/cli-kit': specifier: 3.70.0 version: link:../cli-kit @@ -307,9 +307,6 @@ importers: espree: specifier: 9.6.1 version: 9.6.1 - zod-to-json-schema: - specifier: 3.21.4 - version: 3.21.4(zod@3.22.3) packages/cli-kit: dependencies: @@ -4807,6 +4804,7 @@ packages: /@humanwhocodes/config-array@0.11.14: resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead dependencies: '@humanwhocodes/object-schema': 2.0.3 debug: 4.3.4(supports-color@8.1.1) @@ -4820,6 +4818,7 @@ packages: /@humanwhocodes/object-schema@2.0.3: resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead /@iarna/toml@2.2.5: resolution: {integrity: sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==} @@ -6021,7 +6020,7 @@ packages: requiresBuild: true optional: true - /@shopify/cli-hydrogen@9.0.2(@graphql-codegen/cli@5.0.2)(react-dom@17.0.2)(react@17.0.2): + /@shopify/cli-hydrogen@9.0.2(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-KRlBmgik6fdzIw8z9Xj31swE2dIgTFavCSTS/tonumlu2RX5DrT2M+OTjiQv8s6wLJn5a4Nn6/+5Ds/hKGaKEQ==} engines: {node: '>=18.0.0'} hasBin: true @@ -6047,7 +6046,6 @@ packages: optional: true dependencies: '@ast-grep/napi': 0.11.0 - '@graphql-codegen/cli': 5.0.2(@types/node@18.19.3)(graphql@16.8.1)(typescript@5.2.2) '@oclif/core': 3.26.5 '@shopify/cli-kit': link:packages/cli-kit '@shopify/oxygen-cli': 4.5.3(@oclif/core@3.26.5)(@shopify/cli-kit@packages+cli-kit) @@ -6065,7 +6063,7 @@ packages: tar-fs: 2.1.1 tempy: 3.0.0 ts-morph: 20.0.0 - use-resize-observer: 9.1.0(react-dom@17.0.2)(react@17.0.2) + use-resize-observer: 9.1.0(react-dom@18.2.0)(react@18.2.0) transitivePeerDependencies: - react - react-dom @@ -7520,13 +7518,13 @@ packages: dependencies: '@vitest/utils': 1.6.0 p-limit: 5.0.0 - pathe: 1.1.1 + pathe: 1.1.2 /@vitest/snapshot@1.6.0: resolution: {integrity: sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==} dependencies: magic-string: 0.30.10 - pathe: 1.1.1 + pathe: 1.1.2 pretty-format: 29.7.0 /@vitest/spy@1.6.0: @@ -10126,6 +10124,7 @@ packages: /eslint@8.57.0: resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) @@ -11300,6 +11299,7 @@ packages: /immutable@4.3.5: resolution: {integrity: sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==} + dev: true /import-fresh@3.3.0: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} @@ -13783,7 +13783,6 @@ packages: loose-envify: 1.4.0 react: 18.2.0 scheduler: 0.23.2 - dev: false /react-fast-compare@3.2.2: resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} @@ -13901,7 +13900,6 @@ packages: engines: {node: '>=0.10.0'} dependencies: loose-envify: 1.4.0 - dev: false /read-pkg-up@7.0.1: resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} @@ -14212,6 +14210,7 @@ packages: /rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true dependencies: glob: 7.2.3 @@ -14291,6 +14290,7 @@ packages: chokidar: 3.5.3 immutable: 4.3.5 source-map-js: 1.2.0 + dev: true /saxes@6.0.0: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} @@ -14309,7 +14309,6 @@ packages: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} dependencies: loose-envify: 1.4.0 - dev: false /scuid@1.1.0: resolution: {integrity: sha512-MuCAyrGZcTLfQoH2XoBlQ8C6bzwN88XT/0slOGz0pn8+gIP85BOAfYa44ZXQUTOwRwPU0QvgU+V+OSajl/59Xg==} @@ -15606,15 +15605,15 @@ packages: resolution: {integrity: sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ==} dev: true - /use-resize-observer@9.1.0(react-dom@17.0.2)(react@17.0.2): + /use-resize-observer@9.1.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-R25VqO9Wb3asSD4eqtcxk8sJalvIOYBqS8MNZlpDSQ4l4xMQxC/J7Id9HoTqPq8FwULIn0PVW+OAqF2dyYbjow==} peerDependencies: react: 16.8.0 - 18 react-dom: 16.8.0 - 18 dependencies: '@juggle/resize-observer': 3.4.0 - react: 17.0.2 - react-dom: 17.0.2(react@17.0.2) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) dev: true /util-arity@1.1.0: @@ -15700,9 +15699,9 @@ packages: dependencies: cac: 6.7.14 debug: 4.3.4(supports-color@8.1.1) - pathe: 1.1.1 + pathe: 1.1.2 picocolors: 1.0.0 - vite: 5.3.1(@types/node@18.19.3)(sass@1.76.0) + vite: 5.3.1(@types/node@18.19.3) transitivePeerDependencies: - '@types/node' - less @@ -15713,6 +15712,41 @@ packages: - supports-color - terser + /vite@5.3.1(@types/node@18.19.3): + resolution: {integrity: sha512-XBmSKRLXLxiaPYamLv3/hnP/KXDai1NDexN0FpkTaZXTfycHvkRHoenpgl/fvuK/kPbB6xAgoyiryAhQNxYmAQ==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + '@types/node': 18.19.3 + esbuild: 0.21.5 + postcss: 8.4.38 + rollup: 4.18.0 + optionalDependencies: + fsevents: 2.3.3 + /vite@5.3.1(@types/node@18.19.3)(sass@1.76.0): resolution: {integrity: sha512-XBmSKRLXLxiaPYamLv3/hnP/KXDai1NDexN0FpkTaZXTfycHvkRHoenpgl/fvuK/kPbB6xAgoyiryAhQNxYmAQ==} engines: {node: ^18.0.0 || >=20.0.0} @@ -15748,6 +15782,7 @@ packages: sass: 1.76.0 optionalDependencies: fsevents: 2.3.3 + dev: true /vitest@1.6.0(@types/node@18.19.3): resolution: {integrity: sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==} @@ -15786,13 +15821,13 @@ packages: execa: 8.0.1 local-pkg: 0.5.0 magic-string: 0.30.10 - pathe: 1.1.1 + pathe: 1.1.2 picocolors: 1.0.0 std-env: 3.7.0 strip-literal: 2.1.0 tinybench: 2.8.0 tinypool: 0.8.4 - vite: 5.3.1(@types/node@18.19.3)(sass@1.76.0) + vite: 5.3.1(@types/node@18.19.3) vite-node: 1.6.0(@types/node@18.19.3) why-is-node-running: 2.2.2 transitivePeerDependencies: @@ -16220,14 +16255,6 @@ packages: readable-stream: 3.6.2 dev: false - /zod-to-json-schema@3.21.4(zod@3.22.3): - resolution: {integrity: sha512-fjUZh4nQ1s6HMccgIeE0VP4QG/YRGPmyjO9sAh890aQKPEk3nqbfUXhMFaC+Dr5KvYBm8BCyvfpZf2jY9aGSsw==} - peerDependencies: - zod: ^3.21.4 - dependencies: - zod: 3.22.3 - dev: true - /zod-validation-error@3.2.0(zod@3.23.5): resolution: {integrity: sha512-cYlPR6zuyrgmu2wRTdumEAJGuwI7eHVHGT+VyneAQxmRAKtGRL1/7pjz4wfLhz4J05f5qoSZc3rGacswgyTjjw==} engines: {node: '>=18.0.0'} @@ -16239,6 +16266,7 @@ packages: /zod@3.22.3: resolution: {integrity: sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==} + dev: false /zod@3.23.5: resolution: {integrity: sha512-fkwiq0VIQTksNNA131rDOsVJcns0pfVUjHzLrNBiF/O/Xxb5lQyEXkhZWcJ7npWsYlvs+h0jFWXXy4X46Em1JA==}