@@ -13,9 +13,13 @@ import { Https } from '../utils/https';
1313import { installDependencies } from '../utils/install-dependencies' ;
1414import { getAvailableLocales } from '../utils/localization' ;
1515import { login , storeCredentials } from '../utils/login' ;
16+ import { promptForInfrastructureProject } from '../utils/prompt-infrastructure-project' ;
17+ import { setupNativeHosting } from '../utils/setup-native-hosting' ;
1618import { Telemetry } from '../utils/telemetry/telemetry' ;
1719import { writeEnv } from '../utils/write-env' ;
1820
21+ type HostingMode = 'self-hosted' | 'commerce' ;
22+
1923interface Channel {
2024 id : number ;
2125 name : string ;
@@ -271,7 +275,7 @@ async function setupProject(options: {
271275 } ;
272276
273277 await input ( {
274- message : 'What is the name of your project?' ,
278+ message : 'What do you want to name your project directory ?' ,
275279 default : 'my-catalyst-app' ,
276280 validate : validateProjectName ,
277281 } ) ;
@@ -283,6 +287,130 @@ async function setupProject(options: {
283287 return { projectName, projectDir } ;
284288}
285289
290+ type HostingResolution =
291+ | { mode : 'self-hosted' }
292+ | { mode : 'commerce' ; projectUuid : string ; accessToken : string } ;
293+
294+ async function resolveProjectsAccess (
295+ cliApi : CliApi ,
296+ hostingFlag : HostingMode | undefined ,
297+ ) : Promise < boolean | null > {
298+ try {
299+ return await cliApi . hasProjectsAccess ( ) ;
300+ } catch ( error ) {
301+ const message = error instanceof Error ? error . message : 'unknown error' ;
302+
303+ if ( hostingFlag === 'commerce' ) {
304+ console . error (
305+ colorize (
306+ 'red' ,
307+ `\nFailed to verify Infrastructure Projects API access: ${ message } \nPlease try again.\n` ,
308+ ) ,
309+ ) ;
310+ process . exit ( 1 ) ;
311+ }
312+
313+ console . warn (
314+ colorize (
315+ 'yellow' ,
316+ `\nCould not verify Infrastructure Projects API access: ${ message } \nDefaulting to self-hosted (Next.js). Re-run create-catalyst if you intended to use Commerce-hosted.\n` ,
317+ ) ,
318+ ) ;
319+
320+ return null ;
321+ }
322+ }
323+
324+ async function resolveHostingMode ( {
325+ hostingFlag,
326+ storeHash,
327+ accessToken,
328+ cliApiOrigin,
329+ apiHostname,
330+ defaultProjectName,
331+ autoUseProjectName,
332+ } : {
333+ hostingFlag ?: HostingMode ;
334+ storeHash ?: string ;
335+ accessToken ?: string ;
336+ cliApiOrigin : string ;
337+ apiHostname : string ;
338+ defaultProjectName ?: string ;
339+ autoUseProjectName ?: boolean ;
340+ } ) : Promise < HostingResolution > {
341+ if ( hostingFlag === 'self-hosted' ) {
342+ return { mode : 'self-hosted' } ;
343+ }
344+
345+ if ( ! storeHash || ! accessToken ) {
346+ if ( hostingFlag === 'commerce' ) {
347+ console . error (
348+ colorize (
349+ 'red' ,
350+ '\n--hosting commerce requires store credentials (store hash and access token)\n' ,
351+ ) ,
352+ ) ;
353+ process . exit ( 1 ) ;
354+ }
355+
356+ return { mode : 'self-hosted' } ;
357+ }
358+
359+ const cliApi = new CliApi ( {
360+ origin : cliApiOrigin ,
361+ storeHash,
362+ accessToken,
363+ apiHostname,
364+ } ) ;
365+
366+ const hasAccess = await resolveProjectsAccess ( cliApi , hostingFlag ) ;
367+
368+ if ( hasAccess === null ) {
369+ return { mode : 'self-hosted' } ;
370+ }
371+
372+ if ( hostingFlag === 'commerce' && ! hasAccess ) {
373+ console . error (
374+ colorize (
375+ 'red' ,
376+ '\nThis store does not have access to the Infrastructure Projects API. Contact support@bigcommerce.com to enable it.\n' ,
377+ ) ,
378+ ) ;
379+ process . exit ( 1 ) ;
380+ }
381+
382+ const useNativeHosting =
383+ hostingFlag === undefined && hasAccess
384+ ? await select ( {
385+ message : 'How would you like to host your Catalyst storefront?' ,
386+ choices : [
387+ {
388+ name : 'Self-hosted (Next.js)' ,
389+ value : false ,
390+ description : 'Use standard next dev / build / start' ,
391+ } ,
392+ {
393+ name : 'Commerce-hosted (Native)' ,
394+ value : true ,
395+ description : 'Use catalyst dev / build / deploy' ,
396+ } ,
397+ ] ,
398+ } )
399+ : false ;
400+
401+ if ( ! useNativeHosting ) {
402+ return { mode : 'self-hosted' } ;
403+ }
404+
405+ const project = await promptForInfrastructureProject (
406+ cliApi ,
407+ defaultProjectName ,
408+ autoUseProjectName ,
409+ ) ;
410+
411+ return { mode : 'commerce' , projectUuid : project . uuid , accessToken } ;
412+ }
413+
286414function checkRequiredTools ( ) {
287415 try {
288416 execSync ( getPlatformCheckCommand ( 'git' ) , { stdio : 'ignore' } ) ;
@@ -316,6 +444,12 @@ export const create = new Command('create')
316444 . option ( '--reset-main' , 'Reset the main branch to the gh-ref' )
317445 . option ( '--repository <repository>' , 'GitHub repository to clone from' , 'bigcommerce/catalyst' )
318446 . option ( '--env <vars...>' , 'Arbitrary environment variables to set in .env.local' )
447+ . addOption (
448+ new Option (
449+ '--hosting <mode>' ,
450+ 'Hosting mode: "self-hosted" or "commerce" for Commerce Hosting.' ,
451+ ) . choices ( [ 'self-hosted' , 'commerce' ] as const ) ,
452+ )
319453 . addOption (
320454 new Option ( '--bigcommerce-hostname <hostname>' , 'BigCommerce hostname' )
321455 . default ( 'bigcommerce.com' )
@@ -363,6 +497,16 @@ export const create = new Command('create')
363497 envVars . BIGCOMMERCE_STOREFRONT_API_TOKEN = storefrontToken ;
364498 } else {
365499 if ( ! storeHash || ! accessToken ) {
500+ if ( options . hosting === 'commerce' ) {
501+ console . error (
502+ colorize (
503+ 'red' ,
504+ '\n--hosting commerce requires store credentials (store hash and access token)\n' ,
505+ ) ,
506+ ) ;
507+ process . exit ( 1 ) ;
508+ }
509+
366510 // Create project without credentials
367511 console . log ( `\nCreating '${ projectName } ' at '${ projectDir } '\n` ) ;
368512 cloneCatalyst ( { repository, projectName, projectDir, ghRef, resetMain : options . resetMain } ) ;
@@ -418,6 +562,7 @@ export const create = new Command('create')
418562 origin : options . cliApiOrigin ,
419563 storeHash,
420564 accessToken,
565+ apiHostname : `api.${ options . bigcommerceHostname } ` ,
421566 } ) ;
422567
423568 // If we have channelId but no storefrontToken, just get the init data
@@ -523,9 +668,33 @@ export const create = new Command('create')
523668 if ( ! channelId ) throw new Error ( 'Something went wrong, channelId is not defined' ) ;
524669 if ( ! storefrontToken ) throw new Error ( 'Something went wrong, storefrontToken is not defined' ) ;
525670
671+ const hosting = await resolveHostingMode ( {
672+ hostingFlag : options . hosting ,
673+ storeHash,
674+ accessToken,
675+ cliApiOrigin : options . cliApiOrigin ,
676+ apiHostname : `api.${ options . bigcommerceHostname } ` ,
677+ defaultProjectName : projectName ,
678+ autoUseProjectName : ! ! options . projectName ,
679+ } ) ;
680+
681+ if ( hosting . mode === 'commerce' ) {
682+ envVars . BIGCOMMERCE_ACCESS_TOKEN = hosting . accessToken ;
683+ }
684+
526685 // Create the project with all necessary configuration
527686 console . log ( `\nCreating '${ projectName } ' at '${ projectDir } '\n` ) ;
528687 cloneCatalyst ( { repository, projectName, projectDir, ghRef, resetMain : options . resetMain } ) ;
688+
689+ if ( hosting . mode === 'commerce' ) {
690+ setupNativeHosting ( {
691+ projectDir,
692+ projectUuid : hosting . projectUuid ,
693+ storeHash,
694+ accessToken : hosting . accessToken ,
695+ } ) ;
696+ }
697+
529698 await installDependencies ( projectDir ) ;
530699
531700 // Write env vars
@@ -540,6 +709,14 @@ export const create = new Command('create')
540709 colorize ( 'green' , `\nSuccess! Created '${ projectName } ' at '${ projectDir } '\n` ) ,
541710 '\nNext steps:\n' ,
542711 colorize ( 'yellow' , `\ncd ${ projectName } && pnpm run dev\n` ) ,
712+ ...( hosting . mode === 'commerce'
713+ ? [
714+ colorize (
715+ 'yellow' ,
716+ `\nRun 'cd ${ projectName } /core && pnpm run deploy' when ready to deploy to Commerce hosting.\n` ,
717+ ) ,
718+ ]
719+ : [ ] ) ,
543720 ) ;
544721 } ) ;
545722
0 commit comments