diff --git a/packages/app/src/cli/commands/app/init.ts b/packages/app/src/cli/commands/app/init.ts index d5c41db84b..f8d9b7b1fa 100644 --- a/packages/app/src/cli/commands/app/init.ts +++ b/packages/app/src/cli/commands/app/init.ts @@ -1,13 +1,13 @@ import initPrompt, {visibleTemplates} from '../../prompts/init/init.js' import initService from '../../services/init/init.js' -import {selectDeveloperPlatformClient} from '../../utilities/developer-platform-client.js' +import {DeveloperPlatformClient, selectDeveloperPlatformClient} from '../../utilities/developer-platform-client.js' import {appFromId, selectOrg} from '../../services/context.js' -import {selectOrCreateApp} from '../../services/dev/select-app.js' import AppCommand, {AppCommandOutput} from '../../utilities/app-command.js' import {validateFlavorValue, validateTemplateValue} from '../../services/init/validate.js' -import {OrganizationApp} from '../../models/organization.js' -import {loadApp} from '../../models/app/loader.js' -import {loadLocalExtensionsSpecifications} from '../../models/extensions/load-specifications.js' +import {MinimalOrganizationApp, Organization, OrganizationApp} from '../../models/organization.js' +import {appNamePrompt, createAsNewAppPrompt, selectAppPrompt} from '../../prompts/dev.js' +import {searchForAppsByNameFactory} from '../../services/dev/prompt-helpers.js' +import {linkedAppContext} from '../../services/app-context.js' import {Flags} from '@oclif/core' import {globalFlags} from '@shopify/cli-kit/node/cli' import {resolvePath, cwd} from '@shopify/cli-kit/node/path' @@ -15,8 +15,8 @@ import {addPublicMetadata} from '@shopify/cli-kit/node/metadata' import {installGlobalShopifyCLI} from '@shopify/cli-kit/node/is-global' import {generateRandomNameForSubdirectory} from '@shopify/cli-kit/node/fs' -import {renderText} from '@shopify/cli-kit/node/ui' import {inferPackageManager} from '@shopify/cli-kit/node/node-package-manager' +import {AbortError} from '@shopify/cli-kit/node/error' export default class Init extends AppCommand { static summary?: string | undefined = 'Create a new app project' @@ -75,25 +75,31 @@ export default class Init extends AppCommand { const inferredPackageManager = inferPackageManager(flags['package-manager']) const name = flags.name ?? (await generateRandomNameForSubdirectory({suffix: 'app', directory: flags.path})) - // Authenticate and select organization and app - const developerPlatformClient = selectDeveloperPlatformClient() + // Force user authentication before prompting. + let developerPlatformClient = selectDeveloperPlatformClient() + await developerPlatformClient.session() - let selectedApp: OrganizationApp + const promptAnswers = await initPrompt({ + template: flags.template, + flavor: flags.flavor, + }) + + let selectAppResult: SelectAppOrNewAppNameResult + let appName: string if (flags['client-id']) { // If a client-id is provided we don't need to prompt the user and can link directly to that app. - selectedApp = await appFromId({apiKey: flags['client-id'], developerPlatformClient}) + const selectedApp = await appFromId({apiKey: flags['client-id'], developerPlatformClient}) + appName = selectedApp.title + developerPlatformClient = selectedApp.developerPlatformClient ?? developerPlatformClient + selectAppResult = {result: 'existing', app: selectedApp} } else { - renderText({text: "\nWelcome. Let's get started by linking this new project to an app in your organization."}) const org = await selectOrg() + developerPlatformClient = selectDeveloperPlatformClient({organization: org}) const {organization, apps, hasMorePages} = await developerPlatformClient.orgAndApps(org.id) - selectedApp = await selectOrCreateApp(name, apps, hasMorePages, organization, developerPlatformClient) + selectAppResult = await selectAppOrNewAppName(name, apps, hasMorePages, organization, developerPlatformClient) + appName = selectAppResult.result === 'new' ? selectAppResult.name : selectAppResult.app.title } - const promptAnswers = await initPrompt({ - template: flags.template, - flavor: flags.flavor, - }) - if (promptAnswers.globalCLIResult.install) { await installGlobalShopifyCLI(inferredPackageManager) } @@ -103,30 +109,66 @@ export default class Init extends AppCommand { cmd_create_app_template_url: promptAnswers.template, })) - const platformClient = selectedApp.developerPlatformClient ?? developerPlatformClient - const result = await initService({ - name: selectedApp.title, - app: selectedApp, + name: appName, + selectedAppOrNameResult: selectAppResult, packageManager: inferredPackageManager, template: promptAnswers.template, local: flags.local, directory: flags.path, useGlobalCLI: promptAnswers.globalCLIResult.alreadyInstalled || promptAnswers.globalCLIResult.install, - developerPlatformClient: platformClient, + developerPlatformClient, postCloneActions: { removeLockfilesFromGitignore: promptAnswers.templateType !== 'custom', }, }) - const specifications = await loadLocalExtensionsSpecifications() - - const app = await loadApp({ - specifications, + const {app} = await linkedAppContext({ directory: result.outputDirectory, + clientId: undefined, + forceRelink: false, userProvidedConfigName: undefined, + unsafeReportMode: false, }) return {app} } } + +export type SelectAppOrNewAppNameResult = + | { + result: 'new' + name: string + org: Organization + } + | { + result: 'existing' + app: OrganizationApp + } + +/** + * This method returns either an existing app or a new app name and the data necessary to create it. + * But doesn't create the app yet, the app creation is deferred and is responsibility of the caller. + */ +async function selectAppOrNewAppName( + localAppName: string, + apps: MinimalOrganizationApp[], + hasMorePages: boolean, + org: Organization, + developerPlatformClient: DeveloperPlatformClient, +): Promise { + let createNewApp = apps.length === 0 + if (!createNewApp) { + createNewApp = await createAsNewAppPrompt() + } + if (createNewApp) { + const name = await appNamePrompt(localAppName) + return {result: 'new', name, org} + } else { + const app = await selectAppPrompt(searchForAppsByNameFactory(developerPlatformClient, org.id), apps, hasMorePages) + + const fullSelectedApp = await developerPlatformClient.appFromId(app) + if (!fullSelectedApp) throw new AbortError(`App with id ${app.id} not found`) + return {result: 'existing', app: fullSelectedApp} + } +} diff --git a/packages/app/src/cli/services/init/init.ts b/packages/app/src/cli/services/init/init.ts index 99fa9f8da4..7425125ac0 100644 --- a/packages/app/src/cli/services/init/init.ts +++ b/packages/app/src/cli/services/init/init.ts @@ -3,6 +3,8 @@ import cleanup from './template/cleanup.js' import link from '../app/config/link.js' import {OrganizationApp} from '../../models/organization.js' import {DeveloperPlatformClient} from '../../utilities/developer-platform-client.js' +import {loadApp} from '../../models/app/loader.js' +import {SelectAppOrNewAppNameResult} from '../../commands/app/init.js' import { findUpAndReadPackageJson, lockfiles, @@ -34,7 +36,7 @@ import {LocalStorage} from '@shopify/cli-kit/node/local-storage' interface InitOptions { name: string - app: OrganizationApp + selectedAppOrNameResult: SelectAppOrNewAppNameResult directory: string template: string packageManager: PackageManager @@ -193,13 +195,23 @@ async function init(options: InitOptions) { await moveFile(templateScaffoldDir, outputDirectory) }) - // Link the new project to the selected App + let app: OrganizationApp + if (options.selectedAppOrNameResult.result === 'new') { + // Load the local app to get the creation options. No need for specs since we only care about Creation Options. + const localApp = await loadApp({specifications: [], directory: outputDirectory, userProvidedConfigName: undefined}) + const org = options.selectedAppOrNameResult.org + app = await options.developerPlatformClient.createApp(org, options.name, localApp.creationDefaultOptions()) + } else { + app = options.selectedAppOrNameResult.app + } + + // Link the new project to the selected/created App await link( { directory: outputDirectory, - apiKey: options.app.apiKey, - appId: options.app.id, - organizationId: options.app.organizationId, + apiKey: app.apiKey, + appId: app.id, + organizationId: app.organizationId, configName: 'shopify.app.toml', developerPlatformClient: options.developerPlatformClient, },