From 9444f7ebea4f5f8dd047b6f9001ef5168b8613e3 Mon Sep 17 00:00:00 2001 From: Dipankar Maikap <45673791+dipankarmaikap@users.noreply.github.com> Date: Tue, 25 Nov 2025 11:54:43 +0530 Subject: [PATCH 1/6] fix(cli): skip space creation was not working --- packages/cli/src/commands/create/index.ts | 162 ++++++++++++---------- 1 file changed, 85 insertions(+), 77 deletions(-) diff --git a/packages/cli/src/commands/create/index.ts b/packages/cli/src/commands/create/index.ts index 370fbfb6b..b04702d34 100644 --- a/packages/cli/src/commands/create/index.ts +++ b/packages/cli/src/commands/create/index.ts @@ -64,21 +64,6 @@ export const createCommand = program verbose: !isVitest, }); - let userData: User; - - try { - const user = await getUser(password, region); - if (!user) { - throw new Error('User data is undefined'); - } - userData = user; - } - catch (error) { - konsola.error('Failed to fetch user info. Please login again.', error); - konsola.br(); - return; - } - try { spinnerBlueprints.start('Fetching starter templates...'); const templates = await fetchBlueprintRepositories(); @@ -148,34 +133,51 @@ export const createCommand = program await generateProject(technologyTemplate!, projectName, targetDirectory); konsola.ok(`Project ${chalk.hex(colorPalette.PRIMARY)(projectName)} created successfully in ${chalk.hex(colorPalette.PRIMARY)(finalProjectPath)}`, true); + // Only fetch user info and create space if skipSpace is not set let createdSpace; - const choices = [ - { name: 'My personal account', value: 'personal' }, - ]; - if (userData.has_org) { - choices.push({ name: `Organization (${userData?.org?.name})`, value: 'org' }); - } - if (userData.has_partner) { - choices.push({ name: 'Partner Portal', value: 'partner' }); - } + let userData: User; let whereToCreateSpace = 'personal'; - if (region === 'eu' && (userData.has_partner || userData.has_org)) { - whereToCreateSpace = await select({ - message: `Where would you like to create this space?`, - choices, - }); - } - if (region !== 'eu' && userData.has_org) { - whereToCreateSpace = 'org'; - } - if (region !== 'eu' && !userData.has_org) { - konsola.warn(`Space creation in this region is limited to Enterprise accounts. If you're part of an organization, please ensure you have the required permissions. For more information about Enterprise access, contact our Sales Team.`); - konsola.br(); - return; - } - if (!options.skipSpace) { try { + try { + const user = await getUser(password, region); + if (!user) { + throw new Error('User data is undefined'); + } + userData = user; + } + catch (error) { + konsola.error('Failed to fetch user info. Please login again.', error); + konsola.br(); + return; + } + + // Prepare choices for space creation + const choices = [ + { name: 'My personal account', value: 'personal' }, + ]; + if (userData.has_org) { + choices.push({ name: `Organization (${userData?.org?.name})`, value: 'org' }); + } + if (userData.has_partner) { + choices.push({ name: 'Partner Portal', value: 'partner' }); + } + + if (region === 'eu' && (userData.has_partner || userData.has_org)) { + whereToCreateSpace = await select({ + message: `Where would you like to create this space?`, + choices, + }); + } + if (region !== 'eu' && userData.has_org) { + whereToCreateSpace = 'org'; + } + if (region !== 'eu' && !userData.has_org) { + konsola.warn(`Space creation in this region is limited to Enterprise accounts. If you're part of an organization, please ensure you have the required permissions. For more information about Enterprise access, contact our Sales Team.`); + konsola.br(); + return; + } + spinnerSpace.start(`Creating space "${toHumanReadable(projectName)}"`); // Find the selected blueprint from the dynamic blueprints array @@ -194,6 +196,46 @@ export const createCommand = program } createdSpace = await createSpace(spaceToCreate); spinnerSpace.succeed(`Space "${chalk.hex(colorPalette.PRIMARY)(toHumanReadable(projectName))}" created successfully`); + + // Create .env file with the Storyblok token + if (createdSpace?.first_token) { + try { + await createEnvFile(resolvedPath, createdSpace.first_token); + konsola.ok(`Created .env file with Storyblok access token`, true); + } + catch (error) { + konsola.warn(`Failed to create .env file: ${(error as Error).message}`); + konsola.info(`You can manually add this token to your .env file: ${createdSpace.first_token}`); + } + } + + // Open the space in the browser + if (createdSpace?.id) { + try { + await openSpaceInBrowser(createdSpace.id, region); + konsola.info(`Opened space in your browser`); + } + catch (error) { + konsola.warn(`Failed to open browser: ${(error as Error).message}`); + const spaceUrl = generateSpaceUrl(createdSpace.id, region); + konsola.info(`You can manually open your space at: ${chalk.hex(colorPalette.PRIMARY)(spaceUrl)}`); + } + } + + // Show next steps + konsola.br(); + konsola.ok(`Your ${chalk.hex(colorPalette.PRIMARY)(technologyTemplate)} project is ready 🎉 !`); + if (createdSpace?.first_token) { + if (whereToCreateSpace === 'org') { + konsola.ok(`Storyblok space created in organization ${chalk.hex(colorPalette.PRIMARY)(userData?.org?.name)}, preview url and .env configured automatically. You can now open your space in the browser at ${chalk.hex(colorPalette.PRIMARY)(generateSpaceUrl(createdSpace.id, region))}`); + } + else if (whereToCreateSpace === 'partner') { + konsola.ok(`Storyblok space created in partner portal, preview url and .env configured automatically. You can now open your space in the browser at ${chalk.hex(colorPalette.PRIMARY)(generateSpaceUrl(createdSpace.id, region))}`); + } + else { + konsola.ok(`Storyblok space created, preview url and .env configured automatically. You can now open your space in the browser at ${chalk.hex(colorPalette.PRIMARY)(generateSpaceUrl(createdSpace.id, region))}`); + } + } } catch (error) { spinnerSpace.failed(); @@ -202,46 +244,12 @@ export const createCommand = program return; } } - - // Create .env file with the Storyblok token - if (createdSpace?.first_token) { - try { - await createEnvFile(resolvedPath, createdSpace.first_token); - konsola.ok(`Created .env file with Storyblok access token`, true); - } - catch (error) { - konsola.warn(`Failed to create .env file: ${(error as Error).message}`); - konsola.info(`You can manually add this token to your .env file: ${createdSpace.first_token}`); - } - } - - // Open the space in the browser - if (createdSpace?.id) { - try { - await openSpaceInBrowser(createdSpace.id, region); - konsola.info(`Opened space in your browser`); - } - catch (error) { - konsola.warn(`Failed to open browser: ${(error as Error).message}`); - const spaceUrl = generateSpaceUrl(createdSpace.id, region); - konsola.info(`You can manually open your space at: ${chalk.hex(colorPalette.PRIMARY)(spaceUrl)}`); - } + else { + // If skipSpace, just show next steps for the project + konsola.br(); + konsola.ok(`Your ${chalk.hex(colorPalette.PRIMARY)(technologyTemplate)} project is ready 🎉 !`); } - // Show next steps - konsola.br(); - konsola.ok(`Your ${chalk.hex(colorPalette.PRIMARY)(technologyTemplate)} project is ready 🎉 !`); - if (createdSpace?.first_token) { - if (whereToCreateSpace === 'org') { - konsola.ok(`Storyblok space created in organization ${chalk.hex(colorPalette.PRIMARY)(userData?.org?.name)}, preview url and .env configured automatically. You can now open your space in the browser at ${chalk.hex(colorPalette.PRIMARY)(generateSpaceUrl(createdSpace.id, region))}`); - } - else if (whereToCreateSpace === 'partner') { - konsola.ok(`Storyblok space created in partner portal, preview url and .env configured automatically. You can now open your space in the browser at ${chalk.hex(colorPalette.PRIMARY)(generateSpaceUrl(createdSpace.id, region))}`); - } - else { - konsola.ok(`Storyblok space created, preview url and .env configured automatically. You can now open your space in the browser at ${chalk.hex(colorPalette.PRIMARY)(generateSpaceUrl(createdSpace.id, region))}`); - } - } konsola.br(); konsola.info(`Next steps: cd ${finalProjectPath} From 85605e6c375b0b80d32dac5f791fee4d909f024e Mon Sep 17 00:00:00 2001 From: Dipankar Maikap <45673791+dipankarmaikap@users.noreply.github.com> Date: Tue, 25 Nov 2025 12:10:03 +0530 Subject: [PATCH 2/6] test(cli): updated the test to make sure the skip space is respected --- .../cli/src/commands/create/index.test.ts | 21 ++++++++++++++++++- packages/cli/src/types/index.ts | 2 ++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/commands/create/index.test.ts b/packages/cli/src/commands/create/index.test.ts index 94c304bca..891a6a66a 100644 --- a/packages/cli/src/commands/create/index.test.ts +++ b/packages/cli/src/commands/create/index.test.ts @@ -126,6 +126,8 @@ const createMockUser = (overrides: Partial = {}): StoryblokUser = name: 'Test Organization', }, has_partner: false, + created_at: '2024-01-01T00:00:00Z', + updated_at: '2024-01-01T00:00:00Z', ...overrides, }); @@ -672,6 +674,23 @@ describe('createCommand', () => { expect(createEnvFile).not.toHaveBeenCalled(); expect(openSpaceInBrowser).not.toHaveBeenCalled(); }); + + it('should NOT prompt for space creation when --skip-space is provided', async () => { + vi.mocked(select).mockResolvedValue('vue'); + vi.mocked(input).mockResolvedValue('./my-vue-project'); + vi.mocked(generateProject).mockResolvedValue(undefined); + vi.mocked(fetchBlueprintRepositories).mockResolvedValue([ + { name: 'React', value: 'react', template: '', location: 'https://localhost:5173/', description: '', updated_at: '' }, + { name: 'Vue', value: 'vue', template: '', location: 'https://localhost:5173/', description: '', updated_at: '' }, + ]); + + await createCommand.parseAsync(['node', 'test', 'my-project', '--template', 'react', '--skip-space']); + + // Should NOT prompt for space creation location + expect(select).not.toHaveBeenCalledWith(expect.objectContaining({ + message: 'Where would you like to create this space?', + })); + }); }); describe('space creation choices and location', () => { @@ -997,7 +1016,7 @@ describe('createCommand', () => { await createCommand.parseAsync(['node', 'test', 'my-project', '--blueprint', 'react']); expect(konsola.error).toHaveBeenCalledWith('Failed to fetch user info. Please login again.', userError); - expect(generateProject).not.toHaveBeenCalled(); + expect(generateProject).toHaveBeenCalled(); expect(createSpace).not.toHaveBeenCalled(); }); diff --git a/packages/cli/src/types/index.ts b/packages/cli/src/types/index.ts index d9b4ea868..a75e5fcf0 100644 --- a/packages/cli/src/types/index.ts +++ b/packages/cli/src/types/index.ts @@ -39,6 +39,8 @@ export interface StoryblokUser { name: string; }; has_partner: boolean; + created_at: string; + updated_at: string; } export interface StoryblokLoginResponse { From 38f2e71cff9ce50b925b2093b9515cd15e29e91b Mon Sep 17 00:00:00 2001 From: Dipankar Maikap <45673791+dipankarmaikap@users.noreply.github.com> Date: Tue, 25 Nov 2025 12:44:43 +0530 Subject: [PATCH 3/6] feat(cli): new key option added to pass acesstoken --- packages/cli/src/commands/create/constants.ts | 1 + packages/cli/src/commands/create/index.ts | 58 ++++++++++++------- 2 files changed, 37 insertions(+), 22 deletions(-) diff --git a/packages/cli/src/commands/create/constants.ts b/packages/cli/src/commands/create/constants.ts index 3f9502381..6c2ab2bc8 100644 --- a/packages/cli/src/commands/create/constants.ts +++ b/packages/cli/src/commands/create/constants.ts @@ -2,6 +2,7 @@ export interface CreateOptions { template?: string; blueprint?: string; // Deprecated, use template instead skipSpace?: boolean; + key?: string; // Access token for Storyblok } export const templates = { diff --git a/packages/cli/src/commands/create/index.ts b/packages/cli/src/commands/create/index.ts index b04702d34..2bb076df5 100644 --- a/packages/cli/src/commands/create/index.ts +++ b/packages/cli/src/commands/create/index.ts @@ -13,6 +13,29 @@ import { mapiClient } from '../../api'; import type { User } from '../user/actions'; import { getUser } from '../user/actions'; +// Helper to show next steps and project ready message +function showNextSteps(technologyTemplate: string, finalProjectPath: string) { + konsola.br(); + konsola.ok(`Your ${chalk.hex(colorPalette.PRIMARY)(technologyTemplate)} project is ready 🎉 !`); + konsola.br(); + konsola.info(`Next steps:\n cd ${finalProjectPath}\n npm install\n npm run dev\n `); + konsola.info(`Or check the dedicated guide at: ${chalk.hex(colorPalette.PRIMARY)(`https://www.storyblok.com/docs/guides/${technologyTemplate}`)}`); +} + +// Helper to create .env file and handle errors +async function handleEnvFileCreation(resolvedPath: string, token: string) { + try { + await createEnvFile(resolvedPath, token); + konsola.ok(`Created .env file with Storyblok access token`, true); + return true; + } + catch (error) { + konsola.warn(`Failed to create .env file: ${(error as Error).message}`); + konsola.info(`You can manually add this token to your .env file: ${token}`); + return false; + } +} + const program = getProgram(); // Get the shared singleton instance // Create root command @@ -23,12 +46,13 @@ export const createCommand = program .option('-t, --template