storyblok 4.0.4 → 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();
@@ -1568,9 +1584,16 @@ function buildDependencyGraph(context) {
1568
1584
  const node = new ComponentNode(nodeId, component, targetComponent);
1569
1585
  graph.nodes.set(nodeId, node);
1570
1586
  });
1587
+ const componentMap = new Map(spaceState.local.components.map((c) => [c.id, c]));
1571
1588
  spaceState.local.presets.forEach((preset) => {
1572
- const nodeId = `preset:${preset.name}`;
1573
- const targetPreset = spaceState.target.presets.get(preset.name);
1589
+ const nodeId = `preset:${preset.id}`;
1590
+ const sourceComponent = componentMap.get(preset.component_id);
1591
+ if (!sourceComponent) {
1592
+ console.warn(`Warning: Preset "${preset.name}" (ID: ${preset.id}) references component ID ${preset.component_id} which is not available in local data. Skipping preset.`);
1593
+ return;
1594
+ }
1595
+ const compositeKey = `${sourceComponent.name}:${preset.name}`;
1596
+ const targetPreset = spaceState.target.presets.get(compositeKey);
1574
1597
  const targetData = targetPreset ? {
1575
1598
  resource: targetPreset,
1576
1599
  id: targetPreset.id
@@ -1600,7 +1623,7 @@ function buildDependencyGraph(context) {
1600
1623
  if (component.preset_id) {
1601
1624
  const preset = spaceState.local.presets.find((p) => p.id === component.preset_id);
1602
1625
  if (preset) {
1603
- const presetId = `preset:${preset.name}`;
1626
+ const presetId = `preset:${preset.id}`;
1604
1627
  addDependency(componentId, presetId);
1605
1628
  }
1606
1629
  }
@@ -1621,7 +1644,7 @@ function buildDependencyGraph(context) {
1621
1644
  }
1622
1645
  });
1623
1646
  spaceState.local.presets.forEach((preset) => {
1624
- const presetId = `preset:${preset.name}`;
1647
+ const presetId = `preset:${preset.id}`;
1625
1648
  const component = spaceState.local.components.find((c) => c.id === preset.component_id);
1626
1649
  if (component) {
1627
1650
  const componentId = `component:${component.name}`;
@@ -1948,7 +1971,7 @@ class ComponentNode extends GraphNode {
1948
1971
  if (this.sourceData.preset_id) {
1949
1972
  const preset = this.findPresetById(this.sourceData.preset_id, graph);
1950
1973
  if (preset) {
1951
- const presetNodeId = `preset:${preset.name}`;
1974
+ const presetNodeId = `preset:${preset.id}`;
1952
1975
  const presetNode = graph.nodes.get(presetNodeId);
1953
1976
  if (presetNode?.targetData) {
1954
1977
  updatedData.preset_id = presetNode.targetData.id;
@@ -2027,7 +2050,7 @@ class PresetNode {
2027
2050
  constructor(preset, targetData) {
2028
2051
  this.sourceData = preset;
2029
2052
  this.targetData = targetData;
2030
- this.id = `preset:${preset.name}`;
2053
+ this.id = `preset:${preset.id}`;
2031
2054
  this.name = preset.name;
2032
2055
  }
2033
2056
  getName() {
@@ -2465,10 +2488,10 @@ async function pushWithDependencyGraph(space, spaceState, maxConcurrency = 5) {
2465
2488
  return results;
2466
2489
  }
2467
2490
 
2468
- const program$8 = getProgram();
2491
+ const program$9 = getProgram();
2469
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) => {
2470
2493
  konsola.title(` ${commands.COMPONENTS} `, colorPalette.COMPONENTS, componentName ? `Pushing component ${componentName}...` : "Pushing components...");
2471
- const verbose = program$8.opts().verbose;
2494
+ const verbose = program$9.opts().verbose;
2472
2495
  const { space, path } = componentsCommand.opts();
2473
2496
  const { from, filter } = options;
2474
2497
  const { state, initializeSession } = session();
@@ -2527,7 +2550,11 @@ componentsCommand.command("push [componentName]").description(`Push your space's
2527
2550
  }
2528
2551
  if (presets) {
2529
2552
  presets.forEach((preset) => {
2530
- spaceState.target.presets.set(preset.name, preset);
2553
+ const targetComponent = components?.find((c) => c.id === preset.component_id);
2554
+ if (targetComponent) {
2555
+ const compositeKey = `${targetComponent.name}:${preset.name}`;
2556
+ spaceState.target.presets.set(compositeKey, preset);
2557
+ }
2531
2558
  });
2532
2559
  }
2533
2560
  if (internalTags) {
@@ -2606,11 +2633,11 @@ const saveLanguagesToFile = async (space, internationalizationOptions, options)
2606
2633
  }
2607
2634
  };
2608
2635
 
2609
- const program$7 = getProgram();
2610
- 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");
2611
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) => {
2612
2639
  konsola.title(` ${commands.LANGUAGES} `, colorPalette.LANGUAGES);
2613
- const verbose = program$7.opts().verbose;
2640
+ const verbose = program$8.opts().verbose;
2614
2641
  const { space, path } = languagesCommand.opts();
2615
2642
  const { filename = "languages", suffix = options.space } = options;
2616
2643
  const { state, initializeSession } = session();
@@ -2651,8 +2678,8 @@ languagesCommand.command("pull").description(`Download your space's languages sc
2651
2678
  konsola.br();
2652
2679
  });
2653
2680
 
2654
- const program$6 = getProgram();
2655
- 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");
2656
2683
 
2657
2684
  const getMigrationTemplate = () => {
2658
2685
  return `export default function (block) {
@@ -2680,10 +2707,10 @@ const generateMigration = async (space, path, component, suffix) => {
2680
2707
  }
2681
2708
  };
2682
2709
 
2683
- const program$5 = getProgram();
2710
+ const program$6 = getProgram();
2684
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) => {
2685
2712
  konsola.title(` ${commands.MIGRATIONS} `, colorPalette.MIGRATIONS, componentName ? `Generating migration for component ${componentName}...` : "Generating migrations...");
2686
- const verbose = program$5.opts().verbose;
2713
+ const verbose = program$6.opts().verbose;
2687
2714
  const { space, path } = migrationsCommand.opts();
2688
2715
  const { suffix } = options;
2689
2716
  if (!componentName) {
@@ -3099,10 +3126,10 @@ const isStoryWithUnpublishedChanges = (story) => {
3099
3126
  return story.published && story.unpublished_changes;
3100
3127
  };
3101
3128
 
3102
- const program$4 = getProgram();
3129
+ const program$5 = getProgram();
3103
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) => {
3104
3131
  konsola.title(` ${commands.MIGRATIONS} `, colorPalette.MIGRATIONS, componentName ? `Running migrations for component ${componentName}...` : "Running migrations...");
3105
- const verbose = program$4.opts().verbose;
3132
+ const verbose = program$5.opts().verbose;
3106
3133
  const { filter, dryRun = false, query, startsWith, publish } = options;
3107
3134
  const { space, path } = migrationsCommand.opts();
3108
3135
  const { state, initializeSession } = session();
@@ -3256,10 +3283,10 @@ migrationsCommand.command("run [componentName]").description("Run migrations").o
3256
3283
  }
3257
3284
  });
3258
3285
 
3259
- const program$3 = getProgram();
3286
+ const program$4 = getProgram();
3260
3287
  migrationsCommand.command("rollback [migrationFile]").description("Rollback a migration").action(async (migrationFile) => {
3261
3288
  konsola.title(` ${commands.MIGRATIONS} `, colorPalette.MIGRATIONS, `Rolling back migration ${chalk.hex(colorPalette.MIGRATIONS)(migrationFile)}...`);
3262
- const verbose = program$3.opts().verbose;
3289
+ const verbose = program$4.opts().verbose;
3263
3290
  const { space, path } = migrationsCommand.opts();
3264
3291
  const { state, initializeSession } = session();
3265
3292
  await initializeSession();
@@ -3298,8 +3325,8 @@ migrationsCommand.command("rollback [migrationFile]").description("Rollback a mi
3298
3325
  }
3299
3326
  });
3300
3327
 
3301
- const program$2 = getProgram();
3302
- 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");
3303
3330
 
3304
3331
  const getAssetJSONSchema = (title) => ({
3305
3332
  $id: "#/asset",
@@ -3998,20 +4025,11 @@ const generateStoryblokTypes = async (options = {}) => {
3998
4025
  }
3999
4026
  };
4000
4027
 
4001
- const program$1 = getProgram();
4028
+ const program$2 = getProgram();
4002
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) => {
4003
4030
  konsola.title(` ${commands.TYPES} `, colorPalette.TYPES, "Generating types...");
4004
- const verbose = program$1.opts().verbose;
4031
+ const verbose = program$2.opts().verbose;
4005
4032
  const { space, path } = typesCommand.opts();
4006
- const { state, initializeSession } = session();
4007
- await initializeSession();
4008
- if (!requireAuthentication(state, verbose)) {
4009
- return;
4010
- }
4011
- if (!space) {
4012
- handleError(new CommandError(`Please provide the space as argument --space YOUR_SPACE_ID.`), verbose);
4013
- return;
4014
- }
4015
4033
  const spinner = new Spinner({
4016
4034
  verbose: !isVitest
4017
4035
  });
@@ -4046,7 +4064,264 @@ typesCommand.command("generate").description("Generate types d.ts for your compo
4046
4064
  }
4047
4065
  });
4048
4066
 
4049
- const version = "4.0.4";
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";
4050
4325
  const pkg = {
4051
4326
  version: version};
4052
4327