storyblok 4.0.5 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -7,24 +7,27 @@ import { Command } from 'commander';
7
7
  import { readPackageUp } from 'read-package-up';
8
8
  import { Spinner } from '@topcli/spinner';
9
9
  import { select, password, input } from '@inquirer/prompts';
10
- import { mkdir, writeFile, readFile as readFile$1, access, readdir } from 'node:fs/promises';
11
- import { join, parse, resolve } from 'node:path';
12
- import { exec } from 'node:child_process';
10
+ import fs, { mkdir, writeFile, readFile as readFile$1, access, readdir } from 'node:fs/promises';
11
+ import path, { join, parse, resolve } from 'node:path';
12
+ import { exec, spawn } from 'node:child_process';
13
13
  import { promisify } from 'node:util';
14
14
  import { minimatch } from 'minimatch';
15
15
  import { hash } from 'ohash';
16
16
  import { compile } from 'json-schema-to-typescript';
17
17
  import { readFileSync } from 'node:fs';
18
+ import open from 'open';
19
+ import { Octokit } from 'octokit';
18
20
 
19
21
  const commands = {
20
22
  LOGIN: "login",
21
23
  LOGOUT: "logout",
22
24
  SIGNUP: "signup",
23
25
  USER: "user",
24
- COMPONENTS: "Components",
26
+ COMPONENTS: "components",
25
27
  LANGUAGES: "languages",
26
- MIGRATIONS: "Migrations",
27
- TYPES: "Types"
28
+ MIGRATIONS: "migrations",
29
+ TYPES: "types",
30
+ CREATE: "create"
28
31
  };
29
32
  const colorPalette = {
30
33
  PRIMARY: "#8d60ff",
@@ -36,6 +39,7 @@ const colorPalette = {
36
39
  LANGUAGES: "#f5c003",
37
40
  MIGRATIONS: "#8CE2FF",
38
41
  TYPES: "#3178C6",
42
+ CREATE: "#ffb3ba",
39
43
  GROUPS: "#4ade80",
40
44
  TAGS: "#fbbf24",
41
45
  PRESETS: "#a855f7"
@@ -54,6 +58,13 @@ const regionsDomain = {
54
58
  ca: "api-ca.storyblok.com",
55
59
  ap: "api-ap.storyblok.com"
56
60
  };
61
+ const appDomains = {
62
+ eu: "app.storyblok.com",
63
+ us: "app-us.storyblok.com",
64
+ cn: "app.storyblokchina.cn",
65
+ ca: "app-ca.storyblok.com",
66
+ ap: "app-ap.storyblok.com"
67
+ };
57
68
  const regionNames = {
58
69
  eu: "Europe",
59
70
  us: "United States",
@@ -159,7 +170,9 @@ const API_ACTIONS = {
159
170
  update_component_preset: "Failed to update component preset",
160
171
  pull_stories: "Failed to pull stories",
161
172
  pull_story: "Failed to pull story",
162
- update_story: "Failed to update story"
173
+ update_story: "Failed to update story",
174
+ create_space: "Failed to create space",
175
+ fetch_blueprints: "Failed to fetch blueprints from GitHub"
163
176
  };
164
177
  const API_ERRORS = {
165
178
  unauthorized: "The user is not authorized to access the API",
@@ -388,6 +401,9 @@ const toCamelCase = (str) => {
388
401
  const capitalize = (str) => {
389
402
  return str.charAt(0).toUpperCase() + str.slice(1);
390
403
  };
404
+ const toHumanReadable = (str) => {
405
+ return str.replace(/[-_]/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").split(" ").map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(" ").replace(/\s+/g, " ").trim();
406
+ };
391
407
  function maskToken(token) {
392
408
  if (token.length <= 4) {
393
409
  return token;
@@ -718,7 +734,7 @@ function session() {
718
734
  return sessionInstance;
719
735
  }
720
736
 
721
- const program$e = getProgram();
737
+ const program$f = getProgram();
722
738
  const allRegionsText = Object.values(regions).join(",");
723
739
  const loginStrategy = {
724
740
  message: "How would you like to login?",
@@ -735,12 +751,12 @@ const loginStrategy = {
735
751
  }
736
752
  ]
737
753
  };
738
- program$e.command(commands.LOGIN).description("Login to the Storyblok CLI").option("-t, --token <token>", "Token to login directly without questions, like for CI environments").option(
754
+ program$f.command(commands.LOGIN).description("Login to the Storyblok CLI").option("-t, --token <token>", "Token to login directly without questions, like for CI environments").option(
739
755
  "-r, --region <region>",
740
756
  `The region you would like to work in. Please keep in mind that the region must match the region of your space. This region flag will be used for the other cli's commands. You can use the values: ${allRegionsText}.`
741
757
  ).action(async (options) => {
742
758
  konsola.title(` ${commands.LOGIN} `, colorPalette.LOGIN);
743
- const verbose = program$e.opts().verbose;
759
+ const verbose = program$f.opts().verbose;
744
760
  const { token, region } = options;
745
761
  const { state, updateSession, persistCredentials, initializeSession } = session();
746
762
  await initializeSession();
@@ -859,10 +875,10 @@ program$e.command(commands.LOGIN).description("Login to the Storyblok CLI").opti
859
875
  konsola.br();
860
876
  });
861
877
 
862
- const program$d = getProgram();
863
- program$d.command(commands.LOGOUT).description("Logout from the Storyblok CLI").action(async () => {
878
+ const program$e = getProgram();
879
+ program$e.command(commands.LOGOUT).description("Logout from the Storyblok CLI").action(async () => {
864
880
  konsola.title(` ${commands.LOGOUT} `, colorPalette.LOGOUT);
865
- const verbose = program$d.opts().verbose;
881
+ const verbose = program$e.opts().verbose;
866
882
  try {
867
883
  const { state, initializeSession } = session();
868
884
  await initializeSession();
@@ -910,10 +926,10 @@ async function openSignupInBrowser(url) {
910
926
  }
911
927
  }
912
928
 
913
- const program$c = getProgram();
914
- program$c.command(commands.SIGNUP).description("Sign up for Storyblok").action(async () => {
929
+ const program$d = getProgram();
930
+ program$d.command(commands.SIGNUP).description("Sign up for Storyblok").action(async () => {
915
931
  konsola.title(` ${commands.SIGNUP} `, colorPalette.SIGNUP);
916
- const verbose = program$c.opts().verbose;
932
+ const verbose = program$d.opts().verbose;
917
933
  const { state, initializeSession } = session();
918
934
  await initializeSession();
919
935
  if (state.isLoggedIn && !state.envLogin) {
@@ -959,8 +975,8 @@ const getUser = async (token, region) => {
959
975
  }
960
976
  };
961
977
 
962
- const program$b = getProgram();
963
- program$b.command(commands.USER).description("Get the current user").action(async () => {
978
+ const program$c = getProgram();
979
+ program$c.command(commands.USER).description("Get the current user").action(async () => {
964
980
  konsola.title(` ${commands.USER} `, colorPalette.USER);
965
981
  const { state, initializeSession } = session();
966
982
  await initializeSession();
@@ -985,8 +1001,8 @@ program$b.command(commands.USER).description("Get the current user").action(asyn
985
1001
  konsola.br();
986
1002
  });
987
1003
 
988
- const program$a = getProgram();
989
- const componentsCommand = program$a.command("components").alias("comp").description(`Manage your space's block schema`).option("-s, --space <space>", "space ID").option("-p, --path <path>", "path to save the file. Default is .storyblok/components");
1004
+ const program$b = getProgram();
1005
+ const componentsCommand = program$b.command("components").alias("comp").description(`Manage your space's block schema`).option("-s, --space <space>", "space ID").option("-p, --path <path>", "path to save the file. Default is .storyblok/components");
990
1006
 
991
1007
  let instance = null;
992
1008
  const createMapiClient = (options) => {
@@ -1448,10 +1464,10 @@ async function readConsolidatedFiles(resolvedPath, suffix) {
1448
1464
  };
1449
1465
  }
1450
1466
 
1451
- const program$9 = getProgram();
1467
+ const program$a = getProgram();
1452
1468
  componentsCommand.command("pull [componentName]").option("-f, --filename <filename>", "custom name to be used in file(s) name instead of space id").option("--sf, --separate-files", "Argument to create a single file for each component").option("--su, --suffix <suffix>", "suffix to add to the file name (e.g. components.<suffix>.json)").description(`Download your space's components schema as json. Optionally specify a component name to pull a single component.`).action(async (componentName, options) => {
1453
1469
  konsola.title(` ${commands.COMPONENTS} `, colorPalette.COMPONENTS, componentName ? `Pulling component ${componentName}...` : "Pulling components...");
1454
- const verbose = program$9.opts().verbose;
1470
+ const verbose = program$a.opts().verbose;
1455
1471
  const { space, path } = componentsCommand.opts();
1456
1472
  const { separateFiles, suffix, filename = "components" } = options;
1457
1473
  const { state, initializeSession } = session();
@@ -2472,10 +2488,10 @@ async function pushWithDependencyGraph(space, spaceState, maxConcurrency = 5) {
2472
2488
  return results;
2473
2489
  }
2474
2490
 
2475
- const program$8 = getProgram();
2491
+ const program$9 = getProgram();
2476
2492
  componentsCommand.command("push [componentName]").description(`Push your space's components schema as json`).option("-f, --from <from>", "source space id").option("--fi, --filter <filter>", "glob filter to apply to the components before pushing").option("--sf, --separate-files", "Read from separate files instead of consolidated files").option("--su, --suffix <suffix>", "Suffix to add to the component name").action(async (componentName, options) => {
2477
2493
  konsola.title(` ${commands.COMPONENTS} `, colorPalette.COMPONENTS, componentName ? `Pushing component ${componentName}...` : "Pushing components...");
2478
- const verbose = program$8.opts().verbose;
2494
+ const verbose = program$9.opts().verbose;
2479
2495
  const { space, path } = componentsCommand.opts();
2480
2496
  const { from, filter } = options;
2481
2497
  const { state, initializeSession } = session();
@@ -2617,11 +2633,11 @@ const saveLanguagesToFile = async (space, internationalizationOptions, options)
2617
2633
  }
2618
2634
  };
2619
2635
 
2620
- const program$7 = getProgram();
2621
- const languagesCommand = program$7.command("languages").alias("lang").description(`Manage your space's languages`).option("-s, --space <space>", "space ID").option("-p, --path <path>", "path to save the file. Default is .storyblok/languages");
2636
+ const program$8 = getProgram();
2637
+ const languagesCommand = program$8.command("languages").alias("lang").description(`Manage your space's languages`).option("-s, --space <space>", "space ID").option("-p, --path <path>", "path to save the file. Default is .storyblok/languages");
2622
2638
  languagesCommand.command("pull").description(`Download your space's languages schema as json`).option("-f, --filename <filename>", "filename to save the file as <filename>.<suffix>.json").option("--su, --suffix <suffix>", "suffix to add to the file name (e.g. languages.<suffix>.json). By default, the space ID is used.").action(async (options) => {
2623
2639
  konsola.title(` ${commands.LANGUAGES} `, colorPalette.LANGUAGES);
2624
- const verbose = program$7.opts().verbose;
2640
+ const verbose = program$8.opts().verbose;
2625
2641
  const { space, path } = languagesCommand.opts();
2626
2642
  const { filename = "languages", suffix = options.space } = options;
2627
2643
  const { state, initializeSession } = session();
@@ -2662,8 +2678,8 @@ languagesCommand.command("pull").description(`Download your space's languages sc
2662
2678
  konsola.br();
2663
2679
  });
2664
2680
 
2665
- const program$6 = getProgram();
2666
- const migrationsCommand = program$6.command("migrations").alias("mig").description(`Manage your space's migrations`).option("-s, --space <space>", "space ID").option("-p, --path <path>", "path to save the file. Default is .storyblok/migrations");
2681
+ const program$7 = getProgram();
2682
+ const migrationsCommand = program$7.command("migrations").alias("mig").description(`Manage your space's migrations`).option("-s, --space <space>", "space ID").option("-p, --path <path>", "path to save the file. Default is .storyblok/migrations");
2667
2683
 
2668
2684
  const getMigrationTemplate = () => {
2669
2685
  return `export default function (block) {
@@ -2691,10 +2707,10 @@ const generateMigration = async (space, path, component, suffix) => {
2691
2707
  }
2692
2708
  };
2693
2709
 
2694
- const program$5 = getProgram();
2710
+ const program$6 = getProgram();
2695
2711
  migrationsCommand.command("generate [componentName]").description("Generate a migration file").option("--su, --suffix <suffix>", "suffix to add to the file name (e.g. {component-name}.<suffix>.js)").action(async (componentName, options) => {
2696
2712
  konsola.title(` ${commands.MIGRATIONS} `, colorPalette.MIGRATIONS, componentName ? `Generating migration for component ${componentName}...` : "Generating migrations...");
2697
- const verbose = program$5.opts().verbose;
2713
+ const verbose = program$6.opts().verbose;
2698
2714
  const { space, path } = migrationsCommand.opts();
2699
2715
  const { suffix } = options;
2700
2716
  if (!componentName) {
@@ -3110,10 +3126,10 @@ const isStoryWithUnpublishedChanges = (story) => {
3110
3126
  return story.published && story.unpublished_changes;
3111
3127
  };
3112
3128
 
3113
- const program$4 = getProgram();
3129
+ const program$5 = getProgram();
3114
3130
  migrationsCommand.command("run [componentName]").description("Run migrations").option("--fi, --filter <filter>", "glob filter to apply to the components before pushing").option("-d, --dry-run", "Preview changes without applying them to Storyblok").option("-q, --query <query>", 'Filter stories by content attributes using Storyblok filter query syntax. Example: --query="[highlighted][in]=true"').option("--starts-with <path>", 'Filter stories by path. Example: --starts-with="/en/blog/"').option("--publish <publish>", "Options for publication mode: all | published | published-with-changes").action(async (componentName, options) => {
3115
3131
  konsola.title(` ${commands.MIGRATIONS} `, colorPalette.MIGRATIONS, componentName ? `Running migrations for component ${componentName}...` : "Running migrations...");
3116
- const verbose = program$4.opts().verbose;
3132
+ const verbose = program$5.opts().verbose;
3117
3133
  const { filter, dryRun = false, query, startsWith, publish } = options;
3118
3134
  const { space, path } = migrationsCommand.opts();
3119
3135
  const { state, initializeSession } = session();
@@ -3267,10 +3283,10 @@ migrationsCommand.command("run [componentName]").description("Run migrations").o
3267
3283
  }
3268
3284
  });
3269
3285
 
3270
- const program$3 = getProgram();
3286
+ const program$4 = getProgram();
3271
3287
  migrationsCommand.command("rollback [migrationFile]").description("Rollback a migration").action(async (migrationFile) => {
3272
3288
  konsola.title(` ${commands.MIGRATIONS} `, colorPalette.MIGRATIONS, `Rolling back migration ${chalk.hex(colorPalette.MIGRATIONS)(migrationFile)}...`);
3273
- const verbose = program$3.opts().verbose;
3289
+ const verbose = program$4.opts().verbose;
3274
3290
  const { space, path } = migrationsCommand.opts();
3275
3291
  const { state, initializeSession } = session();
3276
3292
  await initializeSession();
@@ -3309,8 +3325,8 @@ migrationsCommand.command("rollback [migrationFile]").description("Rollback a mi
3309
3325
  }
3310
3326
  });
3311
3327
 
3312
- const program$2 = getProgram();
3313
- const typesCommand = program$2.command("types").alias("ts").description(`Generate types d.ts for your component schemas`).option("-s, --space <space>", "space ID").option("-p, --path <path>", "path to save the file. Default is .storyblok/types");
3328
+ const program$3 = getProgram();
3329
+ const typesCommand = program$3.command(commands.TYPES).alias("ts").description(`Generate types d.ts for your component schemas`).option("-s, --space <space>", "space ID").option("-p, --path <path>", "path to save the file. Default is .storyblok/types");
3314
3330
 
3315
3331
  const getAssetJSONSchema = (title) => ({
3316
3332
  $id: "#/asset",
@@ -4009,20 +4025,11 @@ const generateStoryblokTypes = async (options = {}) => {
4009
4025
  }
4010
4026
  };
4011
4027
 
4012
- const program$1 = getProgram();
4028
+ const program$2 = getProgram();
4013
4029
  typesCommand.command("generate").description("Generate types d.ts for your component schemas").option("--sf, --separate-files", "").option("--strict", "strict mode, no loose typing").option("--type-prefix <prefix>", "prefix to be prepended to all generated component type names").option("--suffix <suffix>", "Components suffix").option("--custom-fields-parser <path>", "Path to the parser file for Custom Field Types").option("--compiler-options <options>", "path to the compiler options from json-schema-to-typescript").action(async (options) => {
4014
4030
  konsola.title(` ${commands.TYPES} `, colorPalette.TYPES, "Generating types...");
4015
- const verbose = program$1.opts().verbose;
4031
+ const verbose = program$2.opts().verbose;
4016
4032
  const { space, path } = typesCommand.opts();
4017
- const { state, initializeSession } = session();
4018
- await initializeSession();
4019
- if (!requireAuthentication(state, verbose)) {
4020
- return;
4021
- }
4022
- if (!space) {
4023
- handleError(new CommandError(`Please provide the space as argument --space YOUR_SPACE_ID.`), verbose);
4024
- return;
4025
- }
4026
4033
  const spinner = new Spinner({
4027
4034
  verbose: !isVitest
4028
4035
  });
@@ -4057,7 +4064,264 @@ typesCommand.command("generate").description("Generate types d.ts for your compo
4057
4064
  }
4058
4065
  });
4059
4066
 
4060
- const version = "4.0.5";
4067
+ let octokit;
4068
+ let lastToken;
4069
+ const createOctokit = (token) => {
4070
+ if (!octokit || token !== lastToken) {
4071
+ const options = {
4072
+ request: {
4073
+ fetch
4074
+ }
4075
+ };
4076
+ octokit = new Octokit(options);
4077
+ }
4078
+ return octokit;
4079
+ };
4080
+
4081
+ const generateProject = async (blueprint, projectName, targetPath = process.cwd()) => {
4082
+ try {
4083
+ const projectPath = path.join(targetPath, projectName);
4084
+ const templateRepo = `storyblok/blueprint-core-${blueprint}`;
4085
+ try {
4086
+ await fs.access(projectPath);
4087
+ const existsError = new Error(`Directory ${projectName} already exists`);
4088
+ existsError.code = "ENOTEMPTY";
4089
+ existsError.path = projectPath;
4090
+ throw new FileSystemError("directory_not_empty", "mkdir", existsError, `Directory ${projectName} already exists`);
4091
+ } catch (error) {
4092
+ const fsError = error;
4093
+ if (fsError.code === "ENOENT") {
4094
+ } else {
4095
+ handleFileSystemError("read", fsError);
4096
+ }
4097
+ }
4098
+ const degitProcess = spawn("npx", ["degit", templateRepo, projectPath], {
4099
+ stdio: "inherit",
4100
+ shell: true
4101
+ });
4102
+ return new Promise((resolve, reject) => {
4103
+ degitProcess.on("close", (code) => {
4104
+ if (code === 0) {
4105
+ resolve();
4106
+ } else {
4107
+ reject(new Error(`Failed to clone template. Process exited with code ${code}`));
4108
+ }
4109
+ });
4110
+ degitProcess.on("error", (error) => {
4111
+ reject(new Error(`Failed to spawn degit process: ${error.message}`));
4112
+ });
4113
+ });
4114
+ } catch (error) {
4115
+ handleFileSystemError("read", error);
4116
+ }
4117
+ };
4118
+ const createEnvFile = async (projectPath, accessToken, additionalVars) => {
4119
+ try {
4120
+ const envPath = path.join(projectPath, ".env");
4121
+ let envContent = `# Storyblok Configuration
4122
+ STORYBLOK_DELIVERY_API_TOKEN=${accessToken}
4123
+ `;
4124
+ if (additionalVars && Object.keys(additionalVars).length > 0) ;
4125
+ await saveToFile(envPath, envContent);
4126
+ } catch (error) {
4127
+ throw new Error(`Failed to create .env file: ${error.message}`);
4128
+ }
4129
+ };
4130
+ const generateSpaceUrl = (spaceId, region) => {
4131
+ const domain = appDomains[region];
4132
+ return `https://${domain}/#/me/spaces/${spaceId}/dashboard`;
4133
+ };
4134
+ const openSpaceInBrowser = async (spaceId, region) => {
4135
+ try {
4136
+ const spaceUrl = generateSpaceUrl(spaceId, region);
4137
+ await open(spaceUrl);
4138
+ } catch (error) {
4139
+ throw new Error(`Failed to open space in browser: ${error.message}`);
4140
+ }
4141
+ };
4142
+ const extractPortFromTopics = (topics) => {
4143
+ const portTopic = topics.find((topic) => topic.startsWith("port-"));
4144
+ if (portTopic) {
4145
+ const port = portTopic.replace("port-", "");
4146
+ if (/^\d+$/.test(port) && Number.parseInt(port) > 0 && Number.parseInt(port) <= 65535) {
4147
+ return port;
4148
+ }
4149
+ }
4150
+ return "3000";
4151
+ };
4152
+ const repositoryToBlueprint = (repo) => {
4153
+ const technology = repo.name.replace("blueprint-core-", "");
4154
+ const port = extractPortFromTopics(repo.topics || []);
4155
+ return {
4156
+ name: technology.charAt(0).toUpperCase() + technology.slice(1),
4157
+ value: technology,
4158
+ template: repo.clone_url,
4159
+ location: port ? `https://localhost:${port}/` : "https://localhost:3000/",
4160
+ description: repo.description,
4161
+ updated_at: repo.updated_at
4162
+ };
4163
+ };
4164
+ const fetchBlueprintRepositories = async () => {
4165
+ try {
4166
+ const octokit = createOctokit();
4167
+ const { data } = await octokit.rest.search.repos({
4168
+ q: "org:storyblok blueprint-core-",
4169
+ sort: "updated",
4170
+ order: "desc",
4171
+ per_page: 100
4172
+ });
4173
+ const blueprints = data.items.filter((repo) => repo.name.startsWith("blueprint-core-")).map(repositoryToBlueprint).sort((a, b) => a.name.localeCompare(b.name));
4174
+ return blueprints;
4175
+ } catch (error) {
4176
+ handleAPIError("fetch_blueprints", error, "Failed to fetch blueprints from GitHub");
4177
+ }
4178
+ };
4179
+
4180
+ const createSpace = async (space) => {
4181
+ try {
4182
+ const client = mapiClient();
4183
+ const { data } = await client.post("spaces", {
4184
+ body: JSON.stringify(space)
4185
+ });
4186
+ return data.space;
4187
+ } catch (error) {
4188
+ handleAPIError("create_space", error, `Failed to create space ${space.name}`);
4189
+ }
4190
+ };
4191
+
4192
+ const program$1 = getProgram();
4193
+ program$1.command(`${commands.CREATE} [project-path]`).alias("c").description(`Scaffold a new project using Storyblok`).option("-b, --blueprint <blueprint>", "technology starter blueprint").option("--skip-space", "skip space creation").action(async (projectPath, options) => {
4194
+ konsola.title(` ${commands.CREATE} `, colorPalette.CREATE);
4195
+ const verbose = program$1.opts().verbose;
4196
+ const { blueprint } = options;
4197
+ const { state, initializeSession } = session();
4198
+ await initializeSession();
4199
+ if (!requireAuthentication(state, verbose)) {
4200
+ return;
4201
+ }
4202
+ const { password, region } = state;
4203
+ mapiClient({
4204
+ token: password,
4205
+ region
4206
+ });
4207
+ const spinnerBlueprints = new Spinner({
4208
+ verbose: !isVitest
4209
+ });
4210
+ const spinnerSpace = new Spinner({
4211
+ verbose: !isVitest
4212
+ });
4213
+ try {
4214
+ spinnerBlueprints.start("Fetching starter blueprints...");
4215
+ const blueprints = await fetchBlueprintRepositories();
4216
+ spinnerBlueprints.succeed("Starter blueprints fetched successfully");
4217
+ if (!blueprints) {
4218
+ spinnerBlueprints.failed();
4219
+ konsola.warn("No starter blueprints found. Please contact support@storyblok.com");
4220
+ konsola.br();
4221
+ return;
4222
+ }
4223
+ let technologyBlueprint = blueprint;
4224
+ if (blueprint) {
4225
+ const validBlueprints = blueprints;
4226
+ const isValidBlueprint = validBlueprints.find((bp) => bp.value === blueprint);
4227
+ if (!isValidBlueprint) {
4228
+ const validOptions = validBlueprints.map((bp) => bp.value).join(", ");
4229
+ konsola.warn(`Invalid blueprint "${chalk.hex(colorPalette.CREATE)(blueprint)}". Valid options are: ${chalk.hex(colorPalette.CREATE)(validOptions)}`);
4230
+ konsola.br();
4231
+ technologyBlueprint = void 0;
4232
+ }
4233
+ }
4234
+ if (!technologyBlueprint) {
4235
+ technologyBlueprint = await select({
4236
+ message: "Please select the technology you would like to use:",
4237
+ choices: blueprints.map((blueprint2) => ({
4238
+ name: blueprint2.name,
4239
+ value: blueprint2.value
4240
+ }))
4241
+ });
4242
+ }
4243
+ let finalProjectPath = projectPath;
4244
+ if (!projectPath) {
4245
+ finalProjectPath = await input({
4246
+ message: "What is the path for your project?",
4247
+ default: `./my-${technologyBlueprint}-project`,
4248
+ validate: (value) => {
4249
+ if (!value.trim()) {
4250
+ return "Project path is required";
4251
+ }
4252
+ const projectName2 = path.basename(value);
4253
+ if (!/^[\w-]+$/.test(projectName2)) {
4254
+ return "Project name (last part of the path) can only contain letters, numbers, hyphens, and underscores";
4255
+ }
4256
+ return true;
4257
+ }
4258
+ });
4259
+ }
4260
+ const resolvedPath = path.resolve(finalProjectPath);
4261
+ const targetDirectory = path.dirname(resolvedPath);
4262
+ const projectName = path.basename(resolvedPath);
4263
+ konsola.br();
4264
+ konsola.info(`Scaffolding your project using the ${chalk.hex(colorPalette.CREATE)(technologyBlueprint)} blueprint...`);
4265
+ await generateProject(technologyBlueprint, projectName, targetDirectory);
4266
+ konsola.ok(`Project ${chalk.hex(colorPalette.PRIMARY)(projectName)} created successfully in ${chalk.hex(colorPalette.PRIMARY)(finalProjectPath)}`, true);
4267
+ let createdSpace;
4268
+ if (!options.skipSpace) {
4269
+ try {
4270
+ spinnerSpace.start(`Creating space "${toHumanReadable(projectName)}"`);
4271
+ const selectedBlueprint = blueprints.find((bp) => bp.value === technologyBlueprint);
4272
+ const blueprintDomain = selectedBlueprint?.location || "https://localhost:3000/";
4273
+ createdSpace = await createSpace({
4274
+ name: toHumanReadable(projectName),
4275
+ domain: blueprintDomain
4276
+ });
4277
+ spinnerSpace.succeed(`Space "${chalk.hex(colorPalette.PRIMARY)(toHumanReadable(projectName))}" created successfully`);
4278
+ } catch (error) {
4279
+ spinnerSpace.failed();
4280
+ konsola.br();
4281
+ handleError(error, verbose);
4282
+ return;
4283
+ }
4284
+ }
4285
+ if (createdSpace?.first_token) {
4286
+ try {
4287
+ await createEnvFile(resolvedPath, createdSpace.first_token);
4288
+ konsola.ok(`Created .env file with Storyblok access token`, true);
4289
+ } catch (error) {
4290
+ konsola.warn(`Failed to create .env file: ${error.message}`);
4291
+ konsola.info(`You can manually add this token to your .env file: ${createdSpace.first_token}`);
4292
+ }
4293
+ }
4294
+ if (createdSpace?.id) {
4295
+ try {
4296
+ await openSpaceInBrowser(createdSpace.id, region);
4297
+ konsola.info(`Opened space in your browser`);
4298
+ } catch (error) {
4299
+ konsola.warn(`Failed to open browser: ${error.message}`);
4300
+ const spaceUrl = generateSpaceUrl(createdSpace.id, region);
4301
+ konsola.info(`You can manually open your space at: ${chalk.hex(colorPalette.PRIMARY)(spaceUrl)}`);
4302
+ }
4303
+ }
4304
+ konsola.br();
4305
+ konsola.ok(`Your ${chalk.hex(colorPalette.PRIMARY)(technologyBlueprint)} project is ready \u{1F389} !`);
4306
+ if (createdSpace?.first_token) {
4307
+ konsola.ok(`Storyblok space created, preview url and .env configured automatically`);
4308
+ }
4309
+ konsola.br();
4310
+ konsola.info(`Next steps:
4311
+ cd ${finalProjectPath}
4312
+ npm install
4313
+ npm run dev
4314
+ `);
4315
+ } catch (error) {
4316
+ spinnerSpace.failed();
4317
+ spinnerBlueprints.failed();
4318
+ konsola.br();
4319
+ handleError(error, verbose);
4320
+ }
4321
+ konsola.br();
4322
+ });
4323
+
4324
+ const version = "4.1.0";
4061
4325
  const pkg = {
4062
4326
  version: version};
4063
4327