storyblok 4.0.5 → 4.2.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
@@ -6,25 +6,29 @@ import chalk from 'chalk';
6
6
  import { Command } from 'commander';
7
7
  import { readPackageUp } from 'read-package-up';
8
8
  import { Spinner } from '@topcli/spinner';
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';
9
+ import { select, password, input, confirm } from '@inquirer/prompts';
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
28
  MIGRATIONS: "Migrations",
27
- TYPES: "Types"
29
+ TYPES: "Types",
30
+ DATASOURCES: "Datasources",
31
+ CREATE: "create"
28
32
  };
29
33
  const colorPalette = {
30
34
  PRIMARY: "#8d60ff",
@@ -36,9 +40,11 @@ const colorPalette = {
36
40
  LANGUAGES: "#f5c003",
37
41
  MIGRATIONS: "#8CE2FF",
38
42
  TYPES: "#3178C6",
43
+ CREATE: "#ffb3ba",
39
44
  GROUPS: "#4ade80",
40
45
  TAGS: "#fbbf24",
41
- PRESETS: "#a855f7"
46
+ PRESETS: "#a855f7",
47
+ DATASOURCES: "#4ade80"
42
48
  };
43
49
  const regions = {
44
50
  EU: "eu",
@@ -54,6 +60,13 @@ const regionsDomain = {
54
60
  ca: "api-ca.storyblok.com",
55
61
  ap: "api-ap.storyblok.com"
56
62
  };
63
+ const appDomains = {
64
+ eu: "app.storyblok.com",
65
+ us: "app-us.storyblok.com",
66
+ cn: "app.storyblokchina.cn",
67
+ ca: "app-ca.storyblok.com",
68
+ ap: "app-ap.storyblok.com"
69
+ };
57
70
  const regionNames = {
58
71
  eu: "Europe",
59
72
  us: "United States",
@@ -159,7 +172,13 @@ const API_ACTIONS = {
159
172
  update_component_preset: "Failed to update component preset",
160
173
  pull_stories: "Failed to pull stories",
161
174
  pull_story: "Failed to pull story",
162
- update_story: "Failed to update story"
175
+ update_story: "Failed to update story",
176
+ pull_datasources: "Failed to pull datasources",
177
+ push_datasource: "Failed to push datasource",
178
+ update_datasource: "Failed to update datasource",
179
+ delete_datasource: "Failed to delete datasource",
180
+ create_space: "Failed to create space",
181
+ fetch_blueprints: "Failed to fetch blueprints from GitHub"
163
182
  };
164
183
  const API_ERRORS = {
165
184
  unauthorized: "The user is not authorized to access the API",
@@ -388,6 +407,9 @@ const toCamelCase = (str) => {
388
407
  const capitalize = (str) => {
389
408
  return str.charAt(0).toUpperCase() + str.slice(1);
390
409
  };
410
+ const toHumanReadable = (str) => {
411
+ 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();
412
+ };
391
413
  function maskToken(token) {
392
414
  if (token.length <= 4) {
393
415
  return token;
@@ -591,6 +613,18 @@ const resolvePath = (path, folder) => {
591
613
  const getComponentNameFromFilename = (filename) => {
592
614
  return filename.replace(/\.js$/, "");
593
615
  };
616
+ async function readJsonFile(filePath) {
617
+ try {
618
+ const content = (await readFile(filePath)).toString();
619
+ if (!content) {
620
+ return { data: [] };
621
+ }
622
+ const parsed = JSON.parse(content);
623
+ return { data: Array.isArray(parsed) ? parsed : [parsed] };
624
+ } catch (error) {
625
+ return { data: [], error };
626
+ }
627
+ }
594
628
 
595
629
  const getCredentials = async (filePath = join(getStoryblokGlobalPath(), "credentials.json")) => {
596
630
  try {
@@ -718,7 +752,7 @@ function session() {
718
752
  return sessionInstance;
719
753
  }
720
754
 
721
- const program$e = getProgram();
755
+ const program$i = getProgram();
722
756
  const allRegionsText = Object.values(regions).join(",");
723
757
  const loginStrategy = {
724
758
  message: "How would you like to login?",
@@ -735,12 +769,12 @@ const loginStrategy = {
735
769
  }
736
770
  ]
737
771
  };
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(
772
+ program$i.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
773
  "-r, --region <region>",
740
774
  `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
775
  ).action(async (options) => {
742
776
  konsola.title(` ${commands.LOGIN} `, colorPalette.LOGIN);
743
- const verbose = program$e.opts().verbose;
777
+ const verbose = program$i.opts().verbose;
744
778
  const { token, region } = options;
745
779
  const { state, updateSession, persistCredentials, initializeSession } = session();
746
780
  await initializeSession();
@@ -859,10 +893,10 @@ program$e.command(commands.LOGIN).description("Login to the Storyblok CLI").opti
859
893
  konsola.br();
860
894
  });
861
895
 
862
- const program$d = getProgram();
863
- program$d.command(commands.LOGOUT).description("Logout from the Storyblok CLI").action(async () => {
896
+ const program$h = getProgram();
897
+ program$h.command(commands.LOGOUT).description("Logout from the Storyblok CLI").action(async () => {
864
898
  konsola.title(` ${commands.LOGOUT} `, colorPalette.LOGOUT);
865
- const verbose = program$d.opts().verbose;
899
+ const verbose = program$h.opts().verbose;
866
900
  try {
867
901
  const { state, initializeSession } = session();
868
902
  await initializeSession();
@@ -910,10 +944,10 @@ async function openSignupInBrowser(url) {
910
944
  }
911
945
  }
912
946
 
913
- const program$c = getProgram();
914
- program$c.command(commands.SIGNUP).description("Sign up for Storyblok").action(async () => {
947
+ const program$g = getProgram();
948
+ program$g.command(commands.SIGNUP).description("Sign up for Storyblok").action(async () => {
915
949
  konsola.title(` ${commands.SIGNUP} `, colorPalette.SIGNUP);
916
- const verbose = program$c.opts().verbose;
950
+ const verbose = program$g.opts().verbose;
917
951
  const { state, initializeSession } = session();
918
952
  await initializeSession();
919
953
  if (state.isLoggedIn && !state.envLogin) {
@@ -959,8 +993,8 @@ const getUser = async (token, region) => {
959
993
  }
960
994
  };
961
995
 
962
- const program$b = getProgram();
963
- program$b.command(commands.USER).description("Get the current user").action(async () => {
996
+ const program$f = getProgram();
997
+ program$f.command(commands.USER).description("Get the current user").action(async () => {
964
998
  konsola.title(` ${commands.USER} `, colorPalette.USER);
965
999
  const { state, initializeSession } = session();
966
1000
  await initializeSession();
@@ -985,8 +1019,8 @@ program$b.command(commands.USER).description("Get the current user").action(asyn
985
1019
  konsola.br();
986
1020
  });
987
1021
 
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");
1022
+ const program$e = getProgram();
1023
+ const componentsCommand = program$e.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
1024
 
991
1025
  let instance = null;
992
1026
  const createMapiClient = (options) => {
@@ -1037,14 +1071,18 @@ const createMapiClient = (options) => {
1037
1071
  ...fetchOptions
1038
1072
  });
1039
1073
  let data;
1040
- try {
1041
- data = await res.json();
1042
- } catch {
1043
- throw new FetchError("Non-JSON response", {
1044
- status: res.status,
1045
- statusText: res.statusText,
1046
- data: null
1047
- });
1074
+ if (res.status === 204 || res.headers.get("content-length") === "0") {
1075
+ data = null;
1076
+ } else {
1077
+ try {
1078
+ data = await res.json();
1079
+ } catch {
1080
+ throw new FetchError("Non-JSON response", {
1081
+ status: res.status,
1082
+ statusText: res.statusText,
1083
+ data: null
1084
+ });
1085
+ }
1048
1086
  }
1049
1087
  options?.onResponse?.({
1050
1088
  path,
@@ -1111,11 +1149,15 @@ const createMapiClient = (options) => {
1111
1149
  const put = async (path, fetchOptions) => {
1112
1150
  return request(path, { ...fetchOptions, method: "PUT" });
1113
1151
  };
1152
+ const _delete = async (path, fetchOptions) => {
1153
+ return request(path, { ...fetchOptions, method: "DELETE" });
1154
+ };
1114
1155
  instance = {
1115
1156
  uuid: state.uuid,
1116
1157
  get,
1117
1158
  post,
1118
1159
  put,
1160
+ delete: _delete,
1119
1161
  dispose: () => {
1120
1162
  instance = null;
1121
1163
  }
@@ -1332,20 +1374,8 @@ const upsertComponentInternalTag = async (space, tag, existingId) => {
1332
1374
  return await pushComponentInternalTag(space, tag);
1333
1375
  }
1334
1376
  };
1335
- async function readJsonFile(filePath) {
1336
- try {
1337
- const content = (await readFile$1(filePath)).toString();
1338
- if (!content) {
1339
- return { data: [] };
1340
- }
1341
- const parsed = JSON.parse(content);
1342
- return { data: Array.isArray(parsed) ? parsed : [parsed] };
1343
- } catch (error) {
1344
- return { data: [], error };
1345
- }
1346
- }
1347
1377
  const readComponentsFiles = async (options) => {
1348
- const { from, path, separateFiles = false, suffix, space } = options;
1378
+ const { from, path, separateFiles = false, suffix } = options;
1349
1379
  const resolvedPath = resolvePath(path, `components/${from}`);
1350
1380
  try {
1351
1381
  await readdir(resolvedPath);
@@ -1356,7 +1386,7 @@ const readComponentsFiles = async (options) => {
1356
1386
  ${chalk.cyan(`storyblok components pull --space ${from}`)}
1357
1387
 
1358
1388
  2. Then try pushing again:
1359
- ${chalk.cyan(`storyblok components push --space ${space} --from ${from}`)}`;
1389
+ ${chalk.cyan(`storyblok components push --space <target_space> --from ${from}`)}`;
1360
1390
  throw new FileSystemError(
1361
1391
  "file_not_found",
1362
1392
  "read",
@@ -1365,11 +1395,11 @@ const readComponentsFiles = async (options) => {
1365
1395
  );
1366
1396
  }
1367
1397
  if (separateFiles) {
1368
- return await readSeparateFiles(resolvedPath, suffix);
1398
+ return await readSeparateFiles$1(resolvedPath, suffix);
1369
1399
  }
1370
- return await readConsolidatedFiles(resolvedPath, suffix);
1400
+ return await readConsolidatedFiles$1(resolvedPath, suffix);
1371
1401
  };
1372
- async function readSeparateFiles(resolvedPath, suffix) {
1402
+ async function readSeparateFiles$1(resolvedPath, suffix) {
1373
1403
  const files = await readdir(resolvedPath);
1374
1404
  const components = [];
1375
1405
  const presets = [];
@@ -1424,7 +1454,7 @@ async function readSeparateFiles(resolvedPath, suffix) {
1424
1454
  internalTags
1425
1455
  };
1426
1456
  }
1427
- async function readConsolidatedFiles(resolvedPath, suffix) {
1457
+ async function readConsolidatedFiles$1(resolvedPath, suffix) {
1428
1458
  const componentsPath = join(resolvedPath, suffix ? `components.${suffix}.json` : "components.json");
1429
1459
  const componentsResult = await readJsonFile(componentsPath);
1430
1460
  if (componentsResult.error || !componentsResult.data.length) {
@@ -1448,10 +1478,10 @@ async function readConsolidatedFiles(resolvedPath, suffix) {
1448
1478
  };
1449
1479
  }
1450
1480
 
1451
- const program$9 = getProgram();
1481
+ const program$d = getProgram();
1452
1482
  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
1483
  konsola.title(` ${commands.COMPONENTS} `, colorPalette.COMPONENTS, componentName ? `Pulling component ${componentName}...` : "Pulling components...");
1454
- const verbose = program$9.opts().verbose;
1484
+ const verbose = program$d.opts().verbose;
1455
1485
  const { space, path } = componentsCommand.opts();
1456
1486
  const { separateFiles, suffix, filename = "components" } = options;
1457
1487
  const { state, initializeSession } = session();
@@ -1539,6 +1569,153 @@ componentsCommand.command("pull [componentName]").option("-f, --filename <filena
1539
1569
  }
1540
1570
  });
1541
1571
 
1572
+ const pushDatasource = async (space, datasource) => {
1573
+ try {
1574
+ const client = mapiClient();
1575
+ const { data } = await client.post(`spaces/${space}/datasources`, {
1576
+ body: JSON.stringify(datasource)
1577
+ });
1578
+ return data.datasource;
1579
+ } catch (error) {
1580
+ handleAPIError("push_datasource", error, `Failed to push datasource ${datasource.name}`);
1581
+ }
1582
+ };
1583
+ const updateDatasource = async (space, datasourceId, datasource) => {
1584
+ try {
1585
+ const client = mapiClient();
1586
+ const { data } = await client.put(`spaces/${space}/datasources/${datasourceId}`, {
1587
+ body: JSON.stringify(datasource)
1588
+ });
1589
+ return data.datasource;
1590
+ } catch (error) {
1591
+ handleAPIError("update_datasource", error, `Failed to update datasource ${datasource.name}`);
1592
+ }
1593
+ };
1594
+ const upsertDatasource = async (space, datasource, existingId) => {
1595
+ if (existingId) {
1596
+ return await updateDatasource(space, existingId, datasource);
1597
+ } else {
1598
+ return await pushDatasource(space, datasource);
1599
+ }
1600
+ };
1601
+ const pushDatasourceEntry = async (space, datasourceId, entry) => {
1602
+ try {
1603
+ const client = mapiClient();
1604
+ const { data } = await client.post(`spaces/${space}/datasource_entries`, {
1605
+ body: JSON.stringify({
1606
+ datasource_entry: {
1607
+ ...entry,
1608
+ datasource_id: datasourceId
1609
+ }
1610
+ })
1611
+ });
1612
+ return data.datasource_entry;
1613
+ } catch (error) {
1614
+ handleAPIError("push_datasource", error, `Failed to push datasource entry ${entry.name}`);
1615
+ }
1616
+ };
1617
+ const updateDatasourceEntry = async (space, entryId, entry) => {
1618
+ try {
1619
+ const client = mapiClient();
1620
+ await client.put(`spaces/${space}/datasource_entries/${entryId}`, {
1621
+ body: JSON.stringify({
1622
+ datasource_entry: entry
1623
+ })
1624
+ });
1625
+ } catch (error) {
1626
+ handleAPIError("update_datasource", error, `Failed to update datasource entry ${entry.name}`);
1627
+ }
1628
+ };
1629
+ const upsertDatasourceEntry = async (space, datasourceId, entry, existingId) => {
1630
+ if (existingId) {
1631
+ await updateDatasourceEntry(space, existingId, entry);
1632
+ return void 0;
1633
+ } else {
1634
+ return await pushDatasourceEntry(space, datasourceId, entry);
1635
+ }
1636
+ };
1637
+ const readDatasourcesFiles = async (options) => {
1638
+ const { from, path, separateFiles = false, suffix, space } = options;
1639
+ const resolvedPath = resolvePath(path, `datasources/${from}`);
1640
+ try {
1641
+ await readdir(resolvedPath);
1642
+ } catch (error) {
1643
+ const message = `No local datasources found for space ${chalk.bold(from)}. To push datasources, you need to pull them first:
1644
+
1645
+ 1. Pull the datasources from your source space:
1646
+ ${chalk.cyan(`storyblok datasources pull --space ${from}`)}
1647
+
1648
+ 2. Then try pushing again:
1649
+ ${chalk.cyan(`storyblok datasources push --space ${space} --from ${from}`)}`;
1650
+ throw new FileSystemError(
1651
+ "file_not_found",
1652
+ "read",
1653
+ error,
1654
+ message
1655
+ );
1656
+ }
1657
+ if (separateFiles) {
1658
+ return await readSeparateFiles(resolvedPath, suffix);
1659
+ }
1660
+ return await readConsolidatedFiles(resolvedPath, suffix);
1661
+ };
1662
+ async function readSeparateFiles(resolvedPath, suffix) {
1663
+ const files = await readdir(resolvedPath);
1664
+ const datasources = [];
1665
+ const filteredFiles = files.filter((file) => {
1666
+ if (suffix) {
1667
+ return file.endsWith(`.${suffix}.json`);
1668
+ } else {
1669
+ return !/\.\w+\.json$/.test(file);
1670
+ }
1671
+ });
1672
+ for (const file of filteredFiles) {
1673
+ const filePath = join(resolvedPath, file);
1674
+ if (file.endsWith(".json") || file.endsWith(`${suffix}.json`)) {
1675
+ if (file === "datasources.json" || /^datasources\.\w+\.json$/.test(file)) {
1676
+ continue;
1677
+ }
1678
+ const result = await readJsonFile(filePath);
1679
+ if (result.error) {
1680
+ handleFileSystemError("read", result.error);
1681
+ continue;
1682
+ }
1683
+ datasources.push(...result.data);
1684
+ }
1685
+ }
1686
+ return {
1687
+ datasources
1688
+ };
1689
+ }
1690
+ async function readConsolidatedFiles(resolvedPath, suffix) {
1691
+ const datasourcesPath = join(resolvedPath, suffix ? `datasources.${suffix}.json` : "datasources.json");
1692
+ const datasourcesResult = await readJsonFile(datasourcesPath);
1693
+ if (datasourcesResult.error || !datasourcesResult.data.length) {
1694
+ throw new FileSystemError(
1695
+ "file_not_found",
1696
+ "read",
1697
+ datasourcesResult.error || new Error("Datasources file is empty"),
1698
+ `No datasources found in ${datasourcesPath}. Please make sure you have pulled the datasources first.`
1699
+ );
1700
+ }
1701
+ return {
1702
+ datasources: datasourcesResult.data
1703
+ };
1704
+ }
1705
+
1706
+ function createStubDatasource(name) {
1707
+ return {
1708
+ id: 0,
1709
+ // Will be set by API
1710
+ name,
1711
+ slug: name,
1712
+ dimensions: [],
1713
+ entries: [],
1714
+ // Empty entries for stub
1715
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
1716
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
1717
+ };
1718
+ }
1542
1719
  function buildDependencyGraph(context) {
1543
1720
  const { spaceState } = context;
1544
1721
  const graph = { nodes: /* @__PURE__ */ new Map() };
@@ -1550,6 +1727,22 @@ function buildDependencyGraph(context) {
1550
1727
  dependency.dependents.add(dependentId);
1551
1728
  }
1552
1729
  }
1730
+ const referencedDatasources = /* @__PURE__ */ new Set();
1731
+ spaceState.local.components.forEach((component) => {
1732
+ if (component.schema) {
1733
+ const dependencies = collectWhitelistDependencies(component.schema);
1734
+ dependencies.datasourceNames.forEach((datasourceName) => {
1735
+ referencedDatasources.add(datasourceName);
1736
+ });
1737
+ }
1738
+ });
1739
+ referencedDatasources.forEach((datasourceName) => {
1740
+ const nodeId = `datasource:${datasourceName}`;
1741
+ const targetDatasource = spaceState.target.datasources?.get(datasourceName);
1742
+ const stubDatasource = createStubDatasource(datasourceName);
1743
+ const node = new DatasourceNode(nodeId, stubDatasource, targetDatasource);
1744
+ graph.nodes.set(nodeId, node);
1745
+ });
1553
1746
  spaceState.local.internalTags.forEach((tag) => {
1554
1747
  const nodeId = `tag:${tag.id}`;
1555
1748
  const targetTag = spaceState.target.tags.get(tag.name);
@@ -1625,6 +1818,10 @@ function buildDependencyGraph(context) {
1625
1818
  const dependencyId = `component:${componentName}`;
1626
1819
  addDependency(componentId, dependencyId);
1627
1820
  });
1821
+ dependencies.datasourceNames.forEach((datasourceName) => {
1822
+ const datasourceId = `datasource:${datasourceName}`;
1823
+ addDependency(componentId, datasourceId);
1824
+ });
1628
1825
  }
1629
1826
  });
1630
1827
  spaceState.local.presets.forEach((preset) => {
@@ -1641,6 +1838,7 @@ function collectWhitelistDependencies(schema) {
1641
1838
  const groupUuids = /* @__PURE__ */ new Set();
1642
1839
  const tagIds = /* @__PURE__ */ new Set();
1643
1840
  const componentNames = /* @__PURE__ */ new Set();
1841
+ const datasourceNames = /* @__PURE__ */ new Set();
1644
1842
  function traverseField(field) {
1645
1843
  if (field.type === "bloks") {
1646
1844
  if (field.component_group_whitelist && Array.isArray(field.component_group_whitelist)) {
@@ -1653,6 +1851,11 @@ function collectWhitelistDependencies(schema) {
1653
1851
  field.component_whitelist.forEach((name) => componentNames.add(name));
1654
1852
  }
1655
1853
  }
1854
+ if ((field.type === "option" || field.type === "options") && field.source === "internal") {
1855
+ if (field.datasource_slug && typeof field.datasource_slug === "string") {
1856
+ datasourceNames.add(field.datasource_slug);
1857
+ }
1858
+ }
1656
1859
  Object.values(field).forEach((value) => {
1657
1860
  if (Array.isArray(value)) {
1658
1861
  value.forEach((item) => {
@@ -1670,7 +1873,7 @@ function collectWhitelistDependencies(schema) {
1670
1873
  traverseField(field);
1671
1874
  }
1672
1875
  });
1673
- return { groupUuids, tagIds, componentNames };
1876
+ return { groupUuids, tagIds, componentNames, datasourceNames };
1674
1877
  }
1675
1878
  function detectProblematicCycles(graph) {
1676
1879
  const problematicCycles = [];
@@ -2001,6 +2204,15 @@ class ComponentNode extends GraphNode {
2001
2204
  });
2002
2205
  }
2003
2206
  }
2207
+ if ((resolvedField.type === "option" || resolvedField.type === "options") && resolvedField.source === "internal") {
2208
+ if (resolvedField.datasource_slug && typeof resolvedField.datasource_slug === "string") {
2209
+ const datasourceNodeId = `datasource:${resolvedField.datasource_slug}`;
2210
+ const datasourceNode = graph.nodes.get(datasourceNodeId);
2211
+ if (datasourceNode?.targetData) {
2212
+ resolvedField.datasource_slug = datasourceNode.targetData.resource.slug;
2213
+ }
2214
+ }
2215
+ }
2004
2216
  Object.keys(resolvedField).forEach((key) => {
2005
2217
  if (typeof resolvedField[key] === "object" && resolvedField[key] !== null) {
2006
2218
  resolvedField[key] = resolveField(resolvedField[key]);
@@ -2072,6 +2284,23 @@ class PresetNode {
2072
2284
  };
2073
2285
  }
2074
2286
  }
2287
+ class DatasourceNode extends GraphNode {
2288
+ constructor(id, data, targetDatasource) {
2289
+ super(id, "datasource", data.name, data, targetDatasource);
2290
+ }
2291
+ resolveReferences(_graph) {
2292
+ }
2293
+ async upsert(space) {
2294
+ const existingDatasource = this.targetData?.resource;
2295
+ const existingId = existingDatasource?.id;
2296
+ const { entries, ...datasourceDefinition } = this.sourceData;
2297
+ const result = await upsertDatasource(space, datasourceDefinition, existingId);
2298
+ if (!result) {
2299
+ throw new Error(`Failed to upsert datasource ${this.name}`);
2300
+ }
2301
+ return result;
2302
+ }
2303
+ }
2075
2304
 
2076
2305
  function collectAllDependencies(components, allComponents, allGroups, allTags) {
2077
2306
  const requiredComponents = /* @__PURE__ */ new Set();
@@ -2472,10 +2701,67 @@ async function pushWithDependencyGraph(space, spaceState, maxConcurrency = 5) {
2472
2701
  return results;
2473
2702
  }
2474
2703
 
2475
- const program$8 = getProgram();
2704
+ const fetchDatasourceEntries = async (space, datasourceId) => {
2705
+ try {
2706
+ const client = mapiClient();
2707
+ const { data } = await client.get(`spaces/${space}/datasource_entries?datasource_id=${datasourceId}`);
2708
+ return data.datasource_entries;
2709
+ } catch (error) {
2710
+ handleAPIError("pull_datasources", error);
2711
+ }
2712
+ };
2713
+ const fetchDatasources = async (space) => {
2714
+ try {
2715
+ const client = mapiClient();
2716
+ const { data } = await client.get(`spaces/${space}/datasources`);
2717
+ const datasources = data.datasources;
2718
+ const datasourcesWithEntries = await Promise.all(
2719
+ datasources.map(async (ds) => {
2720
+ const entries = await fetchDatasourceEntries(space, ds.id);
2721
+ return { ...ds, entries };
2722
+ })
2723
+ );
2724
+ return datasourcesWithEntries;
2725
+ } catch (error) {
2726
+ handleAPIError("pull_datasources", error);
2727
+ }
2728
+ };
2729
+ const fetchDatasource = async (space, datasourceName) => {
2730
+ try {
2731
+ const client = mapiClient();
2732
+ const { data } = await client.get(`spaces/${space}/datasources?search=${encodeURIComponent(datasourceName)}`);
2733
+ const found = data.datasources?.find((d) => d.name === datasourceName);
2734
+ if (!found) {
2735
+ return void 0;
2736
+ }
2737
+ const entries = await fetchDatasourceEntries(space, found.id);
2738
+ return { ...found, entries };
2739
+ } catch (error) {
2740
+ handleAPIError("pull_datasources", error, `Failed to fetch datasource ${datasourceName}`);
2741
+ }
2742
+ };
2743
+ const saveDatasourcesToFiles = async (space, datasources, options) => {
2744
+ const { filename = "datasources", suffix, path, separateFiles } = options;
2745
+ const resolvedPath = path ? resolve(process.cwd(), path, "datasources", space) : resolvePath(path, `datasources/${space}`);
2746
+ try {
2747
+ if (separateFiles) {
2748
+ for (const datasource of datasources) {
2749
+ const datasourceFilePath = join(resolvedPath, suffix ? `${datasource.name}.${suffix}.json` : `${datasource.name}.json`);
2750
+ await saveToFile(datasourceFilePath, JSON.stringify(datasource, null, 2));
2751
+ }
2752
+ return;
2753
+ }
2754
+ const datasourcesFilePath = join(resolvedPath, suffix ? `${filename}.${suffix}.json` : `${filename}.json`);
2755
+ await saveToFile(datasourcesFilePath, JSON.stringify(datasources, null, 2));
2756
+ } catch (error) {
2757
+ handleFileSystemError("write", error);
2758
+ }
2759
+ };
2760
+
2761
+ const program$c = getProgram();
2476
2762
  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
2763
  konsola.title(` ${commands.COMPONENTS} `, colorPalette.COMPONENTS, componentName ? `Pushing component ${componentName}...` : "Pushing components...");
2478
- const verbose = program$8.opts().verbose;
2764
+ const verbose = program$c.opts().verbose;
2479
2765
  const { space, path } = componentsCommand.opts();
2480
2766
  const { from, filter } = options;
2481
2767
  const { state, initializeSession } = session();
@@ -2502,26 +2788,33 @@ componentsCommand.command("push [componentName]").description(`Push your space's
2502
2788
  }
2503
2789
  });
2504
2790
  try {
2791
+ const componentsData = await readComponentsFiles({
2792
+ ...options,
2793
+ path,
2794
+ space
2795
+ });
2796
+ const localData = {
2797
+ ...componentsData,
2798
+ datasources: []
2799
+ };
2505
2800
  const spaceState = {
2506
- local: await readComponentsFiles({
2507
- ...options,
2508
- path,
2509
- space
2510
- }),
2801
+ local: localData,
2511
2802
  target: {
2512
2803
  components: /* @__PURE__ */ new Map(),
2513
2804
  groups: /* @__PURE__ */ new Map(),
2514
2805
  tags: /* @__PURE__ */ new Map(),
2515
- presets: /* @__PURE__ */ new Map()
2806
+ presets: /* @__PURE__ */ new Map(),
2807
+ datasources: /* @__PURE__ */ new Map()
2516
2808
  }
2517
2809
  };
2518
2810
  const promises = [
2519
2811
  fetchComponents(space),
2520
2812
  fetchComponentGroups(space),
2521
2813
  fetchComponentPresets(space),
2522
- fetchComponentInternalTags(space)
2814
+ fetchComponentInternalTags(space),
2815
+ fetchDatasources(space)
2523
2816
  ];
2524
- const [components, groups, presets, internalTags] = await Promise.all(promises);
2817
+ const [components, groups, presets, internalTags, datasources] = await Promise.all(promises);
2525
2818
  if (components) {
2526
2819
  components.forEach((component) => {
2527
2820
  spaceState.target.components.set(component.name, component);
@@ -2546,6 +2839,11 @@ componentsCommand.command("push [componentName]").description(`Push your space's
2546
2839
  spaceState.target.tags.set(tag.name, tag);
2547
2840
  });
2548
2841
  }
2842
+ if (datasources) {
2843
+ datasources.forEach((datasource) => {
2844
+ spaceState.target.datasources.set(datasource.name, datasource);
2845
+ });
2846
+ }
2549
2847
  if (componentName) {
2550
2848
  spaceState.local = filterSpaceDataByComponent(spaceState.local, componentName);
2551
2849
  if (!spaceState.local.components.length) {
@@ -2617,11 +2915,11 @@ const saveLanguagesToFile = async (space, internationalizationOptions, options)
2617
2915
  }
2618
2916
  };
2619
2917
 
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");
2918
+ const program$b = getProgram();
2919
+ const languagesCommand = program$b.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
2920
  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
2921
  konsola.title(` ${commands.LANGUAGES} `, colorPalette.LANGUAGES);
2624
- const verbose = program$7.opts().verbose;
2922
+ const verbose = program$b.opts().verbose;
2625
2923
  const { space, path } = languagesCommand.opts();
2626
2924
  const { filename = "languages", suffix = options.space } = options;
2627
2925
  const { state, initializeSession } = session();
@@ -2662,8 +2960,8 @@ languagesCommand.command("pull").description(`Download your space's languages sc
2662
2960
  konsola.br();
2663
2961
  });
2664
2962
 
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");
2963
+ const program$a = getProgram();
2964
+ const migrationsCommand = program$a.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
2965
 
2668
2966
  const getMigrationTemplate = () => {
2669
2967
  return `export default function (block) {
@@ -2691,10 +2989,10 @@ const generateMigration = async (space, path, component, suffix) => {
2691
2989
  }
2692
2990
  };
2693
2991
 
2694
- const program$5 = getProgram();
2992
+ const program$9 = getProgram();
2695
2993
  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
2994
  konsola.title(` ${commands.MIGRATIONS} `, colorPalette.MIGRATIONS, componentName ? `Generating migration for component ${componentName}...` : "Generating migrations...");
2697
- const verbose = program$5.opts().verbose;
2995
+ const verbose = program$9.opts().verbose;
2698
2996
  const { space, path } = migrationsCommand.opts();
2699
2997
  const { suffix } = options;
2700
2998
  if (!componentName) {
@@ -3110,10 +3408,10 @@ const isStoryWithUnpublishedChanges = (story) => {
3110
3408
  return story.published && story.unpublished_changes;
3111
3409
  };
3112
3410
 
3113
- const program$4 = getProgram();
3411
+ const program$8 = getProgram();
3114
3412
  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
3413
  konsola.title(` ${commands.MIGRATIONS} `, colorPalette.MIGRATIONS, componentName ? `Running migrations for component ${componentName}...` : "Running migrations...");
3116
- const verbose = program$4.opts().verbose;
3414
+ const verbose = program$8.opts().verbose;
3117
3415
  const { filter, dryRun = false, query, startsWith, publish } = options;
3118
3416
  const { space, path } = migrationsCommand.opts();
3119
3417
  const { state, initializeSession } = session();
@@ -3267,10 +3565,10 @@ migrationsCommand.command("run [componentName]").description("Run migrations").o
3267
3565
  }
3268
3566
  });
3269
3567
 
3270
- const program$3 = getProgram();
3568
+ const program$7 = getProgram();
3271
3569
  migrationsCommand.command("rollback [migrationFile]").description("Rollback a migration").action(async (migrationFile) => {
3272
3570
  konsola.title(` ${commands.MIGRATIONS} `, colorPalette.MIGRATIONS, `Rolling back migration ${chalk.hex(colorPalette.MIGRATIONS)(migrationFile)}...`);
3273
- const verbose = program$3.opts().verbose;
3571
+ const verbose = program$7.opts().verbose;
3274
3572
  const { space, path } = migrationsCommand.opts();
3275
3573
  const { state, initializeSession } = session();
3276
3574
  await initializeSession();
@@ -3309,8 +3607,8 @@ migrationsCommand.command("rollback [migrationFile]").description("Rollback a mi
3309
3607
  }
3310
3608
  });
3311
3609
 
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");
3610
+ const program$6 = getProgram();
3611
+ const typesCommand = program$6.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
3612
 
3315
3613
  const getAssetJSONSchema = (title) => ({
3316
3614
  $id: "#/asset",
@@ -4009,20 +4307,11 @@ const generateStoryblokTypes = async (options = {}) => {
4009
4307
  }
4010
4308
  };
4011
4309
 
4012
- const program$1 = getProgram();
4310
+ const program$5 = getProgram();
4013
4311
  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
4312
  konsola.title(` ${commands.TYPES} `, colorPalette.TYPES, "Generating types...");
4015
- const verbose = program$1.opts().verbose;
4313
+ const verbose = program$5.opts().verbose;
4016
4314
  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
4315
  const spinner = new Spinner({
4027
4316
  verbose: !isVitest
4028
4317
  });
@@ -4037,7 +4326,11 @@ typesCommand.command("generate").description("Generate types d.ts for your compo
4037
4326
  ...options,
4038
4327
  path
4039
4328
  });
4040
- const typedefString = await generateTypes(spaceData, {
4329
+ const spaceDataWithDatasources = {
4330
+ ...spaceData,
4331
+ datasources: []
4332
+ };
4333
+ const typedefString = await generateTypes(spaceDataWithDatasources, {
4041
4334
  ...options,
4042
4335
  path
4043
4336
  });
@@ -4057,7 +4350,529 @@ typesCommand.command("generate").description("Generate types d.ts for your compo
4057
4350
  }
4058
4351
  });
4059
4352
 
4060
- const version = "4.0.5";
4353
+ const program$4 = getProgram();
4354
+ const datasourcesCommand = program$4.command("datasources").alias("ds").description(`Manage your space's datasources`).option("-s, --space <space>", "space ID").option("-p, --path <path>", "path to save the file. Default is .storyblok/datasources");
4355
+
4356
+ const program$3 = getProgram();
4357
+ datasourcesCommand.command("pull [datasourceName]").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 datasource").option("--su, --suffix <suffix>", "suffix to add to the file name (e.g. datasources.<suffix>.json)").description("Pull datasources from your space").action(async (datasourceName, options) => {
4358
+ konsola.title(` ${commands.DATASOURCES} `, colorPalette.DATASOURCES, datasourceName ? `Pulling datasource ${datasourceName}...` : "Pulling datasources...");
4359
+ const verbose = program$3.opts().verbose;
4360
+ const { space, path } = datasourcesCommand.opts();
4361
+ const { separateFiles, suffix, filename = "datasources" } = options;
4362
+ const { state, initializeSession } = session();
4363
+ await initializeSession();
4364
+ if (!requireAuthentication(state, verbose)) {
4365
+ return;
4366
+ }
4367
+ if (!space) {
4368
+ handleError(new CommandError(`Please provide the space as argument --space YOUR_SPACE_ID.`), verbose);
4369
+ return;
4370
+ }
4371
+ const { password, region } = state;
4372
+ mapiClient({
4373
+ token: password,
4374
+ region
4375
+ });
4376
+ const spinnerDatasources = new Spinner({
4377
+ verbose: !isVitest
4378
+ });
4379
+ try {
4380
+ spinnerDatasources.start(`Fetching ${chalk.hex(colorPalette.DATASOURCES)("datasources")}`);
4381
+ let datasources;
4382
+ if (datasourceName) {
4383
+ const datasource = await fetchDatasource(space, datasourceName);
4384
+ if (!datasource) {
4385
+ konsola.warn(`No datasource found with name "${datasourceName}"`);
4386
+ return;
4387
+ }
4388
+ datasources = [datasource];
4389
+ } else {
4390
+ datasources = await fetchDatasources(space);
4391
+ if (!datasources || datasources.length === 0) {
4392
+ konsola.warn(`No datasources found in the space ${space}`);
4393
+ return;
4394
+ }
4395
+ }
4396
+ spinnerDatasources.succeed(`${chalk.hex(colorPalette.DATASOURCES)("Datasources")} - Completed in ${spinnerDatasources.elapsedTime.toFixed(2)}ms`);
4397
+ await saveDatasourcesToFiles(
4398
+ space,
4399
+ datasources,
4400
+ { ...options, path, separateFiles: separateFiles || !!datasourceName }
4401
+ );
4402
+ konsola.br();
4403
+ if (separateFiles) {
4404
+ if (filename !== "datasources") {
4405
+ konsola.warn(`The --filename option is ignored when using --separate-files`);
4406
+ }
4407
+ const filePath = path ? `${path}/datasources/${space}/` : `.storyblok/datasources/${space}/`;
4408
+ konsola.ok(`Datasources downloaded successfully to ${chalk.hex(colorPalette.PRIMARY)(filePath)}`);
4409
+ } else if (datasourceName) {
4410
+ const fileName = suffix ? `${filename}.${suffix}.json` : `${datasourceName}.json`;
4411
+ const filePath = path ? `${path}/datasources/${space}/${fileName}` : `.storyblok/datasources/${space}/${fileName}`;
4412
+ konsola.ok(`Datasource ${chalk.hex(colorPalette.PRIMARY)(datasourceName)} downloaded successfully in ${chalk.hex(colorPalette.PRIMARY)(filePath)}`);
4413
+ } else {
4414
+ const fileName = suffix ? `${filename}.${suffix}.json` : `${filename}.json`;
4415
+ const filePath = path ? `${path}/datasources/${space}/${fileName}` : `.storyblok/datasources/${space}/${fileName}`;
4416
+ konsola.ok(`Datasources downloaded successfully to ${chalk.hex(colorPalette.PRIMARY)(filePath)}`);
4417
+ }
4418
+ konsola.br();
4419
+ } catch (error) {
4420
+ spinnerDatasources.failed(`Fetching ${chalk.hex(colorPalette.DATASOURCES)("Datasources")} - Failed`);
4421
+ konsola.br();
4422
+ handleError(error, verbose);
4423
+ }
4424
+ });
4425
+
4426
+ const program$2 = getProgram();
4427
+ datasourcesCommand.command("push [datasourceName]").description(`Push your space's datasources schema as json`).option("-f, --from <from>", "source space id").option("--fi, --filter <filter>", "glob filter to apply to the datasources before pushing").option("--sf, --separate-files", "Read from separate files instead of consolidated files").option("--su, --suffix <suffix>", "Suffix to add to the datasource name").action(async (datasourceName, options) => {
4428
+ konsola.title(` ${commands.DATASOURCES} `, colorPalette.DATASOURCES, datasourceName ? `Pushing datasource ${datasourceName}...` : "Pushing datasources...");
4429
+ const verbose = program$2.opts().verbose;
4430
+ const { space, path } = datasourcesCommand.opts();
4431
+ const { from, filter } = options;
4432
+ const { state, initializeSession } = session();
4433
+ await initializeSession();
4434
+ if (!requireAuthentication(state, verbose)) {
4435
+ return;
4436
+ }
4437
+ if (!space) {
4438
+ handleError(new CommandError(`Please provide the target space as argument --space TARGET_SPACE_ID.`), verbose);
4439
+ return;
4440
+ }
4441
+ if (!from) {
4442
+ options.from = space;
4443
+ }
4444
+ konsola.info(`Attempting to push datasources ${chalk.bold("from")} space ${chalk.hex(colorPalette.DATASOURCES)(options.from || space)} ${chalk.bold("to")} ${chalk.hex(colorPalette.DATASOURCES)(space)}`);
4445
+ konsola.br();
4446
+ const { password, region } = state;
4447
+ mapiClient({
4448
+ token: password,
4449
+ region
4450
+ });
4451
+ try {
4452
+ const spaceState = {
4453
+ local: await readDatasourcesFiles({
4454
+ ...options,
4455
+ path,
4456
+ space
4457
+ }),
4458
+ target: {
4459
+ datasources: /* @__PURE__ */ new Map()
4460
+ }
4461
+ };
4462
+ const targetSpaceDatasources = await fetchDatasources(space);
4463
+ if (targetSpaceDatasources) {
4464
+ targetSpaceDatasources.forEach((datasource) => {
4465
+ spaceState.target.datasources.set(datasource.name, datasource);
4466
+ });
4467
+ }
4468
+ if (datasourceName) {
4469
+ spaceState.local = {
4470
+ datasources: [spaceState.local.datasources.find((datasource) => datasource.name === datasourceName) || []]
4471
+ };
4472
+ if (!spaceState.local.datasources.length) {
4473
+ handleError(new CommandError(`Datasource "${datasourceName}" not found.`), verbose);
4474
+ return;
4475
+ }
4476
+ } else if (filter) {
4477
+ spaceState.local.datasources = spaceState.local.datasources.filter((datasource) => datasource.name.includes(filter));
4478
+ if (!spaceState.local.datasources.length) {
4479
+ handleError(new CommandError(`No datasources found matching pattern "${filter}".`), verbose);
4480
+ return;
4481
+ }
4482
+ konsola.info(`Filter applied: ${filter}`);
4483
+ }
4484
+ if (!spaceState.local.datasources.length) {
4485
+ konsola.warn("No datasources found. Please make sure you have pulled the datasources first.");
4486
+ return;
4487
+ }
4488
+ const results = {
4489
+ successful: [],
4490
+ failed: []
4491
+ };
4492
+ for (const datasource of spaceState.local.datasources) {
4493
+ const spinner = new Spinner({
4494
+ verbose: !isVitest
4495
+ });
4496
+ spinner.start(`Pushing ${chalk.hex(colorPalette.DATASOURCES)(datasource.name)}`);
4497
+ const existingDatasource = spaceState.target.datasources.get(datasource.name);
4498
+ const existingId = existingDatasource?.id;
4499
+ const { entries, ...datasourceDefinition } = datasource;
4500
+ const result = await upsertDatasource(space, datasourceDefinition, existingId);
4501
+ if (result) {
4502
+ results.successful.push(datasource.name);
4503
+ if (entries && entries.length > 0) {
4504
+ for (const entry of entries) {
4505
+ const existingEntryId = existingDatasource?.entries?.find((e) => e.name === entry.name)?.id;
4506
+ try {
4507
+ const { id, ...entryData } = entry;
4508
+ await upsertDatasourceEntry(space, result.id, entryData, existingEntryId);
4509
+ } catch (entryError) {
4510
+ results.failed.push({ name: datasource.name, error: entryError });
4511
+ spinner.failed(`${chalk.hex(colorPalette.DATASOURCES)(datasource.name)} - Failed in ${spinner.elapsedTime.toFixed(2)}ms`);
4512
+ }
4513
+ }
4514
+ }
4515
+ spinner.succeed(`${chalk.hex(colorPalette.DATASOURCES)(datasource.name)} - Completed in ${spinner.elapsedTime.toFixed(2)}ms`);
4516
+ } else {
4517
+ results.failed.push({ name: datasource.name, error: result });
4518
+ spinner.failed(`${chalk.hex(colorPalette.DATASOURCES)(datasource.name)} - Failed in ${spinner.elapsedTime.toFixed(2)}ms`);
4519
+ }
4520
+ }
4521
+ if (results.failed.length > 0) {
4522
+ if (!verbose) {
4523
+ konsola.br();
4524
+ konsola.info("For more information about the error, run the command with the `--verbose` flag");
4525
+ } else {
4526
+ results.failed.forEach((failed) => {
4527
+ handleError(failed.error, verbose);
4528
+ });
4529
+ }
4530
+ }
4531
+ } catch (error) {
4532
+ handleError(error, verbose);
4533
+ }
4534
+ });
4535
+
4536
+ async function deleteDatasource(space, id) {
4537
+ try {
4538
+ const client = mapiClient();
4539
+ await client.delete(`spaces/${space}/datasources/${id}`);
4540
+ } catch (error) {
4541
+ handleAPIError("delete_datasource", error, `Datasource with id '${id}' not found in space ${space}.`);
4542
+ }
4543
+ }
4544
+
4545
+ datasourcesCommand.command("delete [name]").description("Delete a datasource from your space by name or id").option("--id <id>", "Delete by datasource id instead of name").option("--force", "Skip confirmation prompt for deletion (useful for CI)").action(async (name, options) => {
4546
+ konsola.title(
4547
+ ` ${commands.DATASOURCES} `,
4548
+ colorPalette.DATASOURCES,
4549
+ options.id ? `Deleting datasource with id ${options.id}...` : `Deleting datasource with name ${name}...`
4550
+ );
4551
+ if (name && options.id) {
4552
+ konsola.warn(
4553
+ "Both a datasource name and an id were provided. Only one is required. The id will be used as the source of truth."
4554
+ );
4555
+ }
4556
+ const { space } = datasourcesCommand.opts();
4557
+ const verbose = datasourcesCommand.parent?.opts().verbose;
4558
+ const { state, initializeSession } = session();
4559
+ await initializeSession();
4560
+ if (!requireAuthentication(state, verbose)) {
4561
+ return;
4562
+ }
4563
+ if (!space) {
4564
+ handleError(new CommandError("Please provide the space as argument --space YOUR_SPACE_ID."), verbose);
4565
+ return;
4566
+ }
4567
+ const { password, region } = state;
4568
+ mapiClient({
4569
+ token: password,
4570
+ region
4571
+ });
4572
+ const spinner = new Spinner({
4573
+ verbose: !isVitest
4574
+ });
4575
+ try {
4576
+ if (options.id) {
4577
+ spinner.start(`Deleting datasource...`);
4578
+ await deleteDatasource(space, options.id);
4579
+ spinner.succeed();
4580
+ konsola.ok(`Datasource ${chalk.hex(colorPalette.DATASOURCES)(options.id)} deleted successfully from space ${space}.`);
4581
+ } else {
4582
+ const datasource = await fetchDatasource(space, name);
4583
+ if (!datasource) {
4584
+ throw new CommandError(`Datasource with name '${name}' not found in space ${space}.`);
4585
+ }
4586
+ if (!options.force) {
4587
+ konsola.info(`Datasource details:`);
4588
+ console.log(` Name: ${chalk.hex(colorPalette.DATASOURCES)(datasource.name)}`);
4589
+ console.log(` ID: ${chalk.hex(colorPalette.DATASOURCES)(datasource.id)}`);
4590
+ console.log(` Space: ${chalk.hex(colorPalette.DATASOURCES)(space)}`);
4591
+ console.log(` Slug: ${chalk.hex(colorPalette.DATASOURCES)(datasource.slug)}`);
4592
+ console.log(` Created at: ${chalk.hex(colorPalette.DATASOURCES)(datasource.created_at)}`);
4593
+ console.log(` Updated at: ${chalk.hex(colorPalette.DATASOURCES)(datasource.updated_at)}`);
4594
+ konsola.br();
4595
+ const confirmed = await confirm({
4596
+ message: `\u26A0\uFE0F ${chalk.yellow(` Are you sure you want to delete the ${datasource.name} datasource from space ${space}? This action cannot be undone.`)} `,
4597
+ default: false
4598
+ });
4599
+ if (!confirmed) {
4600
+ spinner.failed("Deletion aborted by user.");
4601
+ konsola.warn("Deletion aborted by user.");
4602
+ return;
4603
+ }
4604
+ }
4605
+ spinner.start(`Deleting datasource...`);
4606
+ await deleteDatasource(space, datasource.id.toString());
4607
+ spinner.succeed();
4608
+ konsola.ok(`Datasource ${chalk.hex(colorPalette.DATASOURCES)(name)} deleted successfully from space ${space}.`);
4609
+ }
4610
+ } catch (error) {
4611
+ spinner.failed(
4612
+ `Failed to delete datasource ${chalk.hex(colorPalette.DATASOURCES)(options.id ? options.id : name)}`
4613
+ );
4614
+ handleError(error, verbose);
4615
+ }
4616
+ });
4617
+
4618
+ let octokit;
4619
+ let lastToken;
4620
+ const createOctokit = (token) => {
4621
+ if (!octokit || token !== lastToken) {
4622
+ const options = {
4623
+ request: {
4624
+ fetch
4625
+ }
4626
+ };
4627
+ octokit = new Octokit(options);
4628
+ }
4629
+ return octokit;
4630
+ };
4631
+
4632
+ const generateProject = async (blueprint, projectName, targetPath = process.cwd()) => {
4633
+ try {
4634
+ const projectPath = path.join(targetPath, projectName);
4635
+ const templateRepo = `storyblok/blueprint-core-${blueprint}`;
4636
+ try {
4637
+ await fs.access(projectPath);
4638
+ const existsError = new Error(`Directory ${projectName} already exists`);
4639
+ existsError.code = "ENOTEMPTY";
4640
+ existsError.path = projectPath;
4641
+ throw new FileSystemError("directory_not_empty", "mkdir", existsError, `Directory ${projectName} already exists`);
4642
+ } catch (error) {
4643
+ const fsError = error;
4644
+ if (fsError.code === "ENOENT") {
4645
+ } else {
4646
+ handleFileSystemError("read", fsError);
4647
+ }
4648
+ }
4649
+ const degitProcess = spawn("npx", ["degit", templateRepo, projectPath], {
4650
+ stdio: "inherit",
4651
+ shell: true
4652
+ });
4653
+ return new Promise((resolve, reject) => {
4654
+ degitProcess.on("close", (code) => {
4655
+ if (code === 0) {
4656
+ resolve();
4657
+ } else {
4658
+ reject(new Error(`Failed to clone template. Process exited with code ${code}`));
4659
+ }
4660
+ });
4661
+ degitProcess.on("error", (error) => {
4662
+ reject(new Error(`Failed to spawn degit process: ${error.message}`));
4663
+ });
4664
+ });
4665
+ } catch (error) {
4666
+ handleFileSystemError("read", error);
4667
+ }
4668
+ };
4669
+ const createEnvFile = async (projectPath, accessToken, additionalVars) => {
4670
+ try {
4671
+ const envPath = path.join(projectPath, ".env");
4672
+ let envContent = `# Storyblok Configuration
4673
+ STORYBLOK_DELIVERY_API_TOKEN=${accessToken}
4674
+ `;
4675
+ if (additionalVars && Object.keys(additionalVars).length > 0) ;
4676
+ await saveToFile(envPath, envContent);
4677
+ } catch (error) {
4678
+ throw new Error(`Failed to create .env file: ${error.message}`);
4679
+ }
4680
+ };
4681
+ const generateSpaceUrl = (spaceId, region) => {
4682
+ const domain = appDomains[region];
4683
+ return `https://${domain}/#/me/spaces/${spaceId}/dashboard`;
4684
+ };
4685
+ const openSpaceInBrowser = async (spaceId, region) => {
4686
+ try {
4687
+ const spaceUrl = generateSpaceUrl(spaceId, region);
4688
+ await open(spaceUrl);
4689
+ } catch (error) {
4690
+ throw new Error(`Failed to open space in browser: ${error.message}`);
4691
+ }
4692
+ };
4693
+ const extractPortFromTopics = (topics) => {
4694
+ const portTopic = topics.find((topic) => topic.startsWith("port-"));
4695
+ if (portTopic) {
4696
+ const port = portTopic.replace("port-", "");
4697
+ if (/^\d+$/.test(port) && Number.parseInt(port) > 0 && Number.parseInt(port) <= 65535) {
4698
+ return port;
4699
+ }
4700
+ }
4701
+ return "3000";
4702
+ };
4703
+ const repositoryToBlueprint = (repo) => {
4704
+ const technology = repo.name.replace("blueprint-core-", "");
4705
+ const port = extractPortFromTopics(repo.topics || []);
4706
+ return {
4707
+ name: technology.charAt(0).toUpperCase() + technology.slice(1),
4708
+ value: technology,
4709
+ template: repo.clone_url,
4710
+ location: port ? `https://localhost:${port}/` : "https://localhost:3000/",
4711
+ description: repo.description,
4712
+ updated_at: repo.updated_at
4713
+ };
4714
+ };
4715
+ const fetchBlueprintRepositories = async () => {
4716
+ try {
4717
+ const octokit = createOctokit();
4718
+ const { data } = await octokit.rest.search.repos({
4719
+ q: "org:storyblok blueprint-core-",
4720
+ sort: "updated",
4721
+ order: "desc",
4722
+ per_page: 100
4723
+ });
4724
+ const blueprints = data.items.filter((repo) => repo.name.startsWith("blueprint-core-")).map(repositoryToBlueprint).sort((a, b) => a.name.localeCompare(b.name));
4725
+ return blueprints;
4726
+ } catch (error) {
4727
+ handleAPIError("fetch_blueprints", error, "Failed to fetch blueprints from GitHub");
4728
+ }
4729
+ };
4730
+
4731
+ const createSpace = async (space) => {
4732
+ try {
4733
+ const client = mapiClient();
4734
+ const { data } = await client.post("spaces", {
4735
+ body: JSON.stringify(space)
4736
+ });
4737
+ return data.space;
4738
+ } catch (error) {
4739
+ handleAPIError("create_space", error, `Failed to create space ${space.name}`);
4740
+ }
4741
+ };
4742
+
4743
+ const program$1 = getProgram();
4744
+ 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) => {
4745
+ konsola.title(` ${commands.CREATE} `, colorPalette.CREATE);
4746
+ const verbose = program$1.opts().verbose;
4747
+ const { blueprint } = options;
4748
+ const { state, initializeSession } = session();
4749
+ await initializeSession();
4750
+ if (!requireAuthentication(state, verbose)) {
4751
+ return;
4752
+ }
4753
+ const { password, region } = state;
4754
+ mapiClient({
4755
+ token: password,
4756
+ region
4757
+ });
4758
+ const spinnerBlueprints = new Spinner({
4759
+ verbose: !isVitest
4760
+ });
4761
+ const spinnerSpace = new Spinner({
4762
+ verbose: !isVitest
4763
+ });
4764
+ try {
4765
+ spinnerBlueprints.start("Fetching starter blueprints...");
4766
+ const blueprints = await fetchBlueprintRepositories();
4767
+ spinnerBlueprints.succeed("Starter blueprints fetched successfully");
4768
+ if (!blueprints) {
4769
+ spinnerBlueprints.failed();
4770
+ konsola.warn("No starter blueprints found. Please contact support@storyblok.com");
4771
+ konsola.br();
4772
+ return;
4773
+ }
4774
+ let technologyBlueprint = blueprint;
4775
+ if (blueprint) {
4776
+ const validBlueprints = blueprints;
4777
+ const isValidBlueprint = validBlueprints.find((bp) => bp.value === blueprint);
4778
+ if (!isValidBlueprint) {
4779
+ const validOptions = validBlueprints.map((bp) => bp.value).join(", ");
4780
+ konsola.warn(`Invalid blueprint "${chalk.hex(colorPalette.CREATE)(blueprint)}". Valid options are: ${chalk.hex(colorPalette.CREATE)(validOptions)}`);
4781
+ konsola.br();
4782
+ technologyBlueprint = void 0;
4783
+ }
4784
+ }
4785
+ if (!technologyBlueprint) {
4786
+ technologyBlueprint = await select({
4787
+ message: "Please select the technology you would like to use:",
4788
+ choices: blueprints.map((blueprint2) => ({
4789
+ name: blueprint2.name,
4790
+ value: blueprint2.value
4791
+ }))
4792
+ });
4793
+ }
4794
+ let finalProjectPath = projectPath;
4795
+ if (!projectPath) {
4796
+ finalProjectPath = await input({
4797
+ message: "What is the path for your project?",
4798
+ default: `./my-${technologyBlueprint}-project`,
4799
+ validate: (value) => {
4800
+ if (!value.trim()) {
4801
+ return "Project path is required";
4802
+ }
4803
+ const projectName2 = path.basename(value);
4804
+ if (!/^[\w-]+$/.test(projectName2)) {
4805
+ return "Project name (last part of the path) can only contain letters, numbers, hyphens, and underscores";
4806
+ }
4807
+ return true;
4808
+ }
4809
+ });
4810
+ }
4811
+ const resolvedPath = path.resolve(finalProjectPath);
4812
+ const targetDirectory = path.dirname(resolvedPath);
4813
+ const projectName = path.basename(resolvedPath);
4814
+ konsola.br();
4815
+ konsola.info(`Scaffolding your project using the ${chalk.hex(colorPalette.CREATE)(technologyBlueprint)} blueprint...`);
4816
+ await generateProject(technologyBlueprint, projectName, targetDirectory);
4817
+ konsola.ok(`Project ${chalk.hex(colorPalette.PRIMARY)(projectName)} created successfully in ${chalk.hex(colorPalette.PRIMARY)(finalProjectPath)}`, true);
4818
+ let createdSpace;
4819
+ if (!options.skipSpace) {
4820
+ try {
4821
+ spinnerSpace.start(`Creating space "${toHumanReadable(projectName)}"`);
4822
+ const selectedBlueprint = blueprints.find((bp) => bp.value === technologyBlueprint);
4823
+ const blueprintDomain = selectedBlueprint?.location || "https://localhost:3000/";
4824
+ createdSpace = await createSpace({
4825
+ name: toHumanReadable(projectName),
4826
+ domain: blueprintDomain
4827
+ });
4828
+ spinnerSpace.succeed(`Space "${chalk.hex(colorPalette.PRIMARY)(toHumanReadable(projectName))}" created successfully`);
4829
+ } catch (error) {
4830
+ spinnerSpace.failed();
4831
+ konsola.br();
4832
+ handleError(error, verbose);
4833
+ return;
4834
+ }
4835
+ }
4836
+ if (createdSpace?.first_token) {
4837
+ try {
4838
+ await createEnvFile(resolvedPath, createdSpace.first_token);
4839
+ konsola.ok(`Created .env file with Storyblok access token`, true);
4840
+ } catch (error) {
4841
+ konsola.warn(`Failed to create .env file: ${error.message}`);
4842
+ konsola.info(`You can manually add this token to your .env file: ${createdSpace.first_token}`);
4843
+ }
4844
+ }
4845
+ if (createdSpace?.id) {
4846
+ try {
4847
+ await openSpaceInBrowser(createdSpace.id, region);
4848
+ konsola.info(`Opened space in your browser`);
4849
+ } catch (error) {
4850
+ konsola.warn(`Failed to open browser: ${error.message}`);
4851
+ const spaceUrl = generateSpaceUrl(createdSpace.id, region);
4852
+ konsola.info(`You can manually open your space at: ${chalk.hex(colorPalette.PRIMARY)(spaceUrl)}`);
4853
+ }
4854
+ }
4855
+ konsola.br();
4856
+ konsola.ok(`Your ${chalk.hex(colorPalette.PRIMARY)(technologyBlueprint)} project is ready \u{1F389} !`);
4857
+ if (createdSpace?.first_token) {
4858
+ konsola.ok(`Storyblok space created, preview url and .env configured automatically`);
4859
+ }
4860
+ konsola.br();
4861
+ konsola.info(`Next steps:
4862
+ cd ${finalProjectPath}
4863
+ npm install
4864
+ npm run dev
4865
+ `);
4866
+ } catch (error) {
4867
+ spinnerSpace.failed();
4868
+ spinnerBlueprints.failed();
4869
+ konsola.br();
4870
+ handleError(error, verbose);
4871
+ }
4872
+ konsola.br();
4873
+ });
4874
+
4875
+ const version = "4.2.0";
4061
4876
  const pkg = {
4062
4877
  version: version};
4063
4878