storyblok 4.1.0 → 4.2.1

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,7 +6,7 @@ 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';
9
+ import { select, password, input, confirm } from '@inquirer/prompts';
10
10
  import fs, { mkdir, writeFile, readFile as readFile$1, access, readdir } from 'node:fs/promises';
11
11
  import path, { join, parse, resolve } from 'node:path';
12
12
  import { exec, spawn } from 'node:child_process';
@@ -25,8 +25,9 @@ const commands = {
25
25
  USER: "user",
26
26
  COMPONENTS: "components",
27
27
  LANGUAGES: "languages",
28
- MIGRATIONS: "migrations",
29
- TYPES: "types",
28
+ MIGRATIONS: "Migrations",
29
+ TYPES: "Types",
30
+ DATASOURCES: "Datasources",
30
31
  CREATE: "create"
31
32
  };
32
33
  const colorPalette = {
@@ -42,7 +43,8 @@ const colorPalette = {
42
43
  CREATE: "#ffb3ba",
43
44
  GROUPS: "#4ade80",
44
45
  TAGS: "#fbbf24",
45
- PRESETS: "#a855f7"
46
+ PRESETS: "#a855f7",
47
+ DATASOURCES: "#4ade80"
46
48
  };
47
49
  const regions = {
48
50
  EU: "eu",
@@ -171,6 +173,10 @@ const API_ACTIONS = {
171
173
  pull_stories: "Failed to pull stories",
172
174
  pull_story: "Failed to pull story",
173
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",
174
180
  create_space: "Failed to create space",
175
181
  fetch_blueprints: "Failed to fetch blueprints from GitHub"
176
182
  };
@@ -607,6 +613,18 @@ const resolvePath = (path, folder) => {
607
613
  const getComponentNameFromFilename = (filename) => {
608
614
  return filename.replace(/\.js$/, "");
609
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
+ }
610
628
 
611
629
  const getCredentials = async (filePath = join(getStoryblokGlobalPath(), "credentials.json")) => {
612
630
  try {
@@ -734,7 +752,7 @@ function session() {
734
752
  return sessionInstance;
735
753
  }
736
754
 
737
- const program$f = getProgram();
755
+ const program$i = getProgram();
738
756
  const allRegionsText = Object.values(regions).join(",");
739
757
  const loginStrategy = {
740
758
  message: "How would you like to login?",
@@ -751,12 +769,12 @@ const loginStrategy = {
751
769
  }
752
770
  ]
753
771
  };
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(
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(
755
773
  "-r, --region <region>",
756
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}.`
757
775
  ).action(async (options) => {
758
776
  konsola.title(` ${commands.LOGIN} `, colorPalette.LOGIN);
759
- const verbose = program$f.opts().verbose;
777
+ const verbose = program$i.opts().verbose;
760
778
  const { token, region } = options;
761
779
  const { state, updateSession, persistCredentials, initializeSession } = session();
762
780
  await initializeSession();
@@ -875,10 +893,10 @@ program$f.command(commands.LOGIN).description("Login to the Storyblok CLI").opti
875
893
  konsola.br();
876
894
  });
877
895
 
878
- const program$e = getProgram();
879
- program$e.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 () => {
880
898
  konsola.title(` ${commands.LOGOUT} `, colorPalette.LOGOUT);
881
- const verbose = program$e.opts().verbose;
899
+ const verbose = program$h.opts().verbose;
882
900
  try {
883
901
  const { state, initializeSession } = session();
884
902
  await initializeSession();
@@ -926,10 +944,10 @@ async function openSignupInBrowser(url) {
926
944
  }
927
945
  }
928
946
 
929
- const program$d = getProgram();
930
- program$d.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 () => {
931
949
  konsola.title(` ${commands.SIGNUP} `, colorPalette.SIGNUP);
932
- const verbose = program$d.opts().verbose;
950
+ const verbose = program$g.opts().verbose;
933
951
  const { state, initializeSession } = session();
934
952
  await initializeSession();
935
953
  if (state.isLoggedIn && !state.envLogin) {
@@ -975,8 +993,8 @@ const getUser = async (token, region) => {
975
993
  }
976
994
  };
977
995
 
978
- const program$c = getProgram();
979
- program$c.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 () => {
980
998
  konsola.title(` ${commands.USER} `, colorPalette.USER);
981
999
  const { state, initializeSession } = session();
982
1000
  await initializeSession();
@@ -1001,8 +1019,8 @@ program$c.command(commands.USER).description("Get the current user").action(asyn
1001
1019
  konsola.br();
1002
1020
  });
1003
1021
 
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");
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");
1006
1024
 
1007
1025
  let instance = null;
1008
1026
  const createMapiClient = (options) => {
@@ -1053,14 +1071,18 @@ const createMapiClient = (options) => {
1053
1071
  ...fetchOptions
1054
1072
  });
1055
1073
  let data;
1056
- try {
1057
- data = await res.json();
1058
- } catch {
1059
- throw new FetchError("Non-JSON response", {
1060
- status: res.status,
1061
- statusText: res.statusText,
1062
- data: null
1063
- });
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
+ }
1064
1086
  }
1065
1087
  options?.onResponse?.({
1066
1088
  path,
@@ -1127,11 +1149,15 @@ const createMapiClient = (options) => {
1127
1149
  const put = async (path, fetchOptions) => {
1128
1150
  return request(path, { ...fetchOptions, method: "PUT" });
1129
1151
  };
1152
+ const _delete = async (path, fetchOptions) => {
1153
+ return request(path, { ...fetchOptions, method: "DELETE" });
1154
+ };
1130
1155
  instance = {
1131
1156
  uuid: state.uuid,
1132
1157
  get,
1133
1158
  post,
1134
1159
  put,
1160
+ delete: _delete,
1135
1161
  dispose: () => {
1136
1162
  instance = null;
1137
1163
  }
@@ -1348,20 +1374,8 @@ const upsertComponentInternalTag = async (space, tag, existingId) => {
1348
1374
  return await pushComponentInternalTag(space, tag);
1349
1375
  }
1350
1376
  };
1351
- async function readJsonFile(filePath) {
1352
- try {
1353
- const content = (await readFile$1(filePath)).toString();
1354
- if (!content) {
1355
- return { data: [] };
1356
- }
1357
- const parsed = JSON.parse(content);
1358
- return { data: Array.isArray(parsed) ? parsed : [parsed] };
1359
- } catch (error) {
1360
- return { data: [], error };
1361
- }
1362
- }
1363
1377
  const readComponentsFiles = async (options) => {
1364
- const { from, path, separateFiles = false, suffix, space } = options;
1378
+ const { from, path, separateFiles = false, suffix } = options;
1365
1379
  const resolvedPath = resolvePath(path, `components/${from}`);
1366
1380
  try {
1367
1381
  await readdir(resolvedPath);
@@ -1372,7 +1386,7 @@ const readComponentsFiles = async (options) => {
1372
1386
  ${chalk.cyan(`storyblok components pull --space ${from}`)}
1373
1387
 
1374
1388
  2. Then try pushing again:
1375
- ${chalk.cyan(`storyblok components push --space ${space} --from ${from}`)}`;
1389
+ ${chalk.cyan(`storyblok components push --space <target_space> --from ${from}`)}`;
1376
1390
  throw new FileSystemError(
1377
1391
  "file_not_found",
1378
1392
  "read",
@@ -1381,11 +1395,11 @@ const readComponentsFiles = async (options) => {
1381
1395
  );
1382
1396
  }
1383
1397
  if (separateFiles) {
1384
- return await readSeparateFiles(resolvedPath, suffix);
1398
+ return await readSeparateFiles$1(resolvedPath, suffix);
1385
1399
  }
1386
- return await readConsolidatedFiles(resolvedPath, suffix);
1400
+ return await readConsolidatedFiles$1(resolvedPath, suffix);
1387
1401
  };
1388
- async function readSeparateFiles(resolvedPath, suffix) {
1402
+ async function readSeparateFiles$1(resolvedPath, suffix) {
1389
1403
  const files = await readdir(resolvedPath);
1390
1404
  const components = [];
1391
1405
  const presets = [];
@@ -1440,7 +1454,7 @@ async function readSeparateFiles(resolvedPath, suffix) {
1440
1454
  internalTags
1441
1455
  };
1442
1456
  }
1443
- async function readConsolidatedFiles(resolvedPath, suffix) {
1457
+ async function readConsolidatedFiles$1(resolvedPath, suffix) {
1444
1458
  const componentsPath = join(resolvedPath, suffix ? `components.${suffix}.json` : "components.json");
1445
1459
  const componentsResult = await readJsonFile(componentsPath);
1446
1460
  if (componentsResult.error || !componentsResult.data.length) {
@@ -1464,10 +1478,10 @@ async function readConsolidatedFiles(resolvedPath, suffix) {
1464
1478
  };
1465
1479
  }
1466
1480
 
1467
- const program$a = getProgram();
1481
+ const program$d = getProgram();
1468
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) => {
1469
1483
  konsola.title(` ${commands.COMPONENTS} `, colorPalette.COMPONENTS, componentName ? `Pulling component ${componentName}...` : "Pulling components...");
1470
- const verbose = program$a.opts().verbose;
1484
+ const verbose = program$d.opts().verbose;
1471
1485
  const { space, path } = componentsCommand.opts();
1472
1486
  const { separateFiles, suffix, filename = "components" } = options;
1473
1487
  const { state, initializeSession } = session();
@@ -1555,6 +1569,153 @@ componentsCommand.command("pull [componentName]").option("-f, --filename <filena
1555
1569
  }
1556
1570
  });
1557
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
+ }
1558
1719
  function buildDependencyGraph(context) {
1559
1720
  const { spaceState } = context;
1560
1721
  const graph = { nodes: /* @__PURE__ */ new Map() };
@@ -1566,6 +1727,22 @@ function buildDependencyGraph(context) {
1566
1727
  dependency.dependents.add(dependentId);
1567
1728
  }
1568
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
+ });
1569
1746
  spaceState.local.internalTags.forEach((tag) => {
1570
1747
  const nodeId = `tag:${tag.id}`;
1571
1748
  const targetTag = spaceState.target.tags.get(tag.name);
@@ -1641,6 +1818,10 @@ function buildDependencyGraph(context) {
1641
1818
  const dependencyId = `component:${componentName}`;
1642
1819
  addDependency(componentId, dependencyId);
1643
1820
  });
1821
+ dependencies.datasourceNames.forEach((datasourceName) => {
1822
+ const datasourceId = `datasource:${datasourceName}`;
1823
+ addDependency(componentId, datasourceId);
1824
+ });
1644
1825
  }
1645
1826
  });
1646
1827
  spaceState.local.presets.forEach((preset) => {
@@ -1657,6 +1838,7 @@ function collectWhitelistDependencies(schema) {
1657
1838
  const groupUuids = /* @__PURE__ */ new Set();
1658
1839
  const tagIds = /* @__PURE__ */ new Set();
1659
1840
  const componentNames = /* @__PURE__ */ new Set();
1841
+ const datasourceNames = /* @__PURE__ */ new Set();
1660
1842
  function traverseField(field) {
1661
1843
  if (field.type === "bloks") {
1662
1844
  if (field.component_group_whitelist && Array.isArray(field.component_group_whitelist)) {
@@ -1669,6 +1851,11 @@ function collectWhitelistDependencies(schema) {
1669
1851
  field.component_whitelist.forEach((name) => componentNames.add(name));
1670
1852
  }
1671
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
+ }
1672
1859
  Object.values(field).forEach((value) => {
1673
1860
  if (Array.isArray(value)) {
1674
1861
  value.forEach((item) => {
@@ -1686,7 +1873,7 @@ function collectWhitelistDependencies(schema) {
1686
1873
  traverseField(field);
1687
1874
  }
1688
1875
  });
1689
- return { groupUuids, tagIds, componentNames };
1876
+ return { groupUuids, tagIds, componentNames, datasourceNames };
1690
1877
  }
1691
1878
  function detectProblematicCycles(graph) {
1692
1879
  const problematicCycles = [];
@@ -2017,6 +2204,15 @@ class ComponentNode extends GraphNode {
2017
2204
  });
2018
2205
  }
2019
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
+ }
2020
2216
  Object.keys(resolvedField).forEach((key) => {
2021
2217
  if (typeof resolvedField[key] === "object" && resolvedField[key] !== null) {
2022
2218
  resolvedField[key] = resolveField(resolvedField[key]);
@@ -2088,6 +2284,23 @@ class PresetNode {
2088
2284
  };
2089
2285
  }
2090
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
+ }
2091
2304
 
2092
2305
  function collectAllDependencies(components, allComponents, allGroups, allTags) {
2093
2306
  const requiredComponents = /* @__PURE__ */ new Set();
@@ -2488,10 +2701,67 @@ async function pushWithDependencyGraph(space, spaceState, maxConcurrency = 5) {
2488
2701
  return results;
2489
2702
  }
2490
2703
 
2491
- const program$9 = 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();
2492
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) => {
2493
2763
  konsola.title(` ${commands.COMPONENTS} `, colorPalette.COMPONENTS, componentName ? `Pushing component ${componentName}...` : "Pushing components...");
2494
- const verbose = program$9.opts().verbose;
2764
+ const verbose = program$c.opts().verbose;
2495
2765
  const { space, path } = componentsCommand.opts();
2496
2766
  const { from, filter } = options;
2497
2767
  const { state, initializeSession } = session();
@@ -2518,26 +2788,33 @@ componentsCommand.command("push [componentName]").description(`Push your space's
2518
2788
  }
2519
2789
  });
2520
2790
  try {
2791
+ const componentsData = await readComponentsFiles({
2792
+ ...options,
2793
+ path,
2794
+ space
2795
+ });
2796
+ const localData = {
2797
+ ...componentsData,
2798
+ datasources: []
2799
+ };
2521
2800
  const spaceState = {
2522
- local: await readComponentsFiles({
2523
- ...options,
2524
- path,
2525
- space
2526
- }),
2801
+ local: localData,
2527
2802
  target: {
2528
2803
  components: /* @__PURE__ */ new Map(),
2529
2804
  groups: /* @__PURE__ */ new Map(),
2530
2805
  tags: /* @__PURE__ */ new Map(),
2531
- presets: /* @__PURE__ */ new Map()
2806
+ presets: /* @__PURE__ */ new Map(),
2807
+ datasources: /* @__PURE__ */ new Map()
2532
2808
  }
2533
2809
  };
2534
2810
  const promises = [
2535
2811
  fetchComponents(space),
2536
2812
  fetchComponentGroups(space),
2537
2813
  fetchComponentPresets(space),
2538
- fetchComponentInternalTags(space)
2814
+ fetchComponentInternalTags(space),
2815
+ fetchDatasources(space)
2539
2816
  ];
2540
- const [components, groups, presets, internalTags] = await Promise.all(promises);
2817
+ const [components, groups, presets, internalTags, datasources] = await Promise.all(promises);
2541
2818
  if (components) {
2542
2819
  components.forEach((component) => {
2543
2820
  spaceState.target.components.set(component.name, component);
@@ -2562,6 +2839,11 @@ componentsCommand.command("push [componentName]").description(`Push your space's
2562
2839
  spaceState.target.tags.set(tag.name, tag);
2563
2840
  });
2564
2841
  }
2842
+ if (datasources) {
2843
+ datasources.forEach((datasource) => {
2844
+ spaceState.target.datasources.set(datasource.name, datasource);
2845
+ });
2846
+ }
2565
2847
  if (componentName) {
2566
2848
  spaceState.local = filterSpaceDataByComponent(spaceState.local, componentName);
2567
2849
  if (!spaceState.local.components.length) {
@@ -2633,11 +2915,11 @@ const saveLanguagesToFile = async (space, internationalizationOptions, options)
2633
2915
  }
2634
2916
  };
2635
2917
 
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");
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");
2638
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) => {
2639
2921
  konsola.title(` ${commands.LANGUAGES} `, colorPalette.LANGUAGES);
2640
- const verbose = program$8.opts().verbose;
2922
+ const verbose = program$b.opts().verbose;
2641
2923
  const { space, path } = languagesCommand.opts();
2642
2924
  const { filename = "languages", suffix = options.space } = options;
2643
2925
  const { state, initializeSession } = session();
@@ -2678,8 +2960,8 @@ languagesCommand.command("pull").description(`Download your space's languages sc
2678
2960
  konsola.br();
2679
2961
  });
2680
2962
 
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");
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");
2683
2965
 
2684
2966
  const getMigrationTemplate = () => {
2685
2967
  return `export default function (block) {
@@ -2707,10 +2989,10 @@ const generateMigration = async (space, path, component, suffix) => {
2707
2989
  }
2708
2990
  };
2709
2991
 
2710
- const program$6 = getProgram();
2992
+ const program$9 = getProgram();
2711
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) => {
2712
2994
  konsola.title(` ${commands.MIGRATIONS} `, colorPalette.MIGRATIONS, componentName ? `Generating migration for component ${componentName}...` : "Generating migrations...");
2713
- const verbose = program$6.opts().verbose;
2995
+ const verbose = program$9.opts().verbose;
2714
2996
  const { space, path } = migrationsCommand.opts();
2715
2997
  const { suffix } = options;
2716
2998
  if (!componentName) {
@@ -3126,10 +3408,10 @@ const isStoryWithUnpublishedChanges = (story) => {
3126
3408
  return story.published && story.unpublished_changes;
3127
3409
  };
3128
3410
 
3129
- const program$5 = getProgram();
3411
+ const program$8 = getProgram();
3130
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) => {
3131
3413
  konsola.title(` ${commands.MIGRATIONS} `, colorPalette.MIGRATIONS, componentName ? `Running migrations for component ${componentName}...` : "Running migrations...");
3132
- const verbose = program$5.opts().verbose;
3414
+ const verbose = program$8.opts().verbose;
3133
3415
  const { filter, dryRun = false, query, startsWith, publish } = options;
3134
3416
  const { space, path } = migrationsCommand.opts();
3135
3417
  const { state, initializeSession } = session();
@@ -3283,10 +3565,10 @@ migrationsCommand.command("run [componentName]").description("Run migrations").o
3283
3565
  }
3284
3566
  });
3285
3567
 
3286
- const program$4 = getProgram();
3568
+ const program$7 = getProgram();
3287
3569
  migrationsCommand.command("rollback [migrationFile]").description("Rollback a migration").action(async (migrationFile) => {
3288
3570
  konsola.title(` ${commands.MIGRATIONS} `, colorPalette.MIGRATIONS, `Rolling back migration ${chalk.hex(colorPalette.MIGRATIONS)(migrationFile)}...`);
3289
- const verbose = program$4.opts().verbose;
3571
+ const verbose = program$7.opts().verbose;
3290
3572
  const { space, path } = migrationsCommand.opts();
3291
3573
  const { state, initializeSession } = session();
3292
3574
  await initializeSession();
@@ -3325,8 +3607,8 @@ migrationsCommand.command("rollback [migrationFile]").description("Rollback a mi
3325
3607
  }
3326
3608
  });
3327
3609
 
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");
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");
3330
3612
 
3331
3613
  const getAssetJSONSchema = (title) => ({
3332
3614
  $id: "#/asset",
@@ -4025,10 +4307,10 @@ const generateStoryblokTypes = async (options = {}) => {
4025
4307
  }
4026
4308
  };
4027
4309
 
4028
- const program$2 = getProgram();
4310
+ const program$5 = getProgram();
4029
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) => {
4030
4312
  konsola.title(` ${commands.TYPES} `, colorPalette.TYPES, "Generating types...");
4031
- const verbose = program$2.opts().verbose;
4313
+ const verbose = program$5.opts().verbose;
4032
4314
  const { space, path } = typesCommand.opts();
4033
4315
  const spinner = new Spinner({
4034
4316
  verbose: !isVitest
@@ -4044,7 +4326,11 @@ typesCommand.command("generate").description("Generate types d.ts for your compo
4044
4326
  ...options,
4045
4327
  path
4046
4328
  });
4047
- const typedefString = await generateTypes(spaceData, {
4329
+ const spaceDataWithDatasources = {
4330
+ ...spaceData,
4331
+ datasources: []
4332
+ };
4333
+ const typedefString = await generateTypes(spaceDataWithDatasources, {
4048
4334
  ...options,
4049
4335
  path
4050
4336
  });
@@ -4064,6 +4350,271 @@ typesCommand.command("generate").description("Generate types d.ts for your compo
4064
4350
  }
4065
4351
  });
4066
4352
 
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
+
4067
4618
  let octokit;
4068
4619
  let lastToken;
4069
4620
  const createOctokit = (token) => {
@@ -4321,7 +4872,7 @@ program$1.command(`${commands.CREATE} [project-path]`).alias("c").description(`S
4321
4872
  konsola.br();
4322
4873
  });
4323
4874
 
4324
- const version = "4.1.0";
4875
+ const version = "4.2.1";
4325
4876
  const pkg = {
4326
4877
  version: version};
4327
4878