storyblok 4.13.0 → 4.14.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
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import 'dotenv/config';
3
- import { fileURLToPath } from 'node:url';
3
+ import { fileURLToPath, pathToFileURL } from 'node:url';
4
4
  import { resolve, dirname, isAbsolute, relative as relative$1, join as join$1 } from 'pathe';
5
5
  import { existsSync, mkdirSync, appendFileSync, writeFileSync, readdirSync, unlinkSync, readFileSync } from 'node:fs';
6
6
  import { homedir } from 'node:os';
@@ -8,14 +8,14 @@ import { loadConfig as loadConfig$1, SUPPORTED_EXTENSIONS } from 'c12';
8
8
  import chalk from 'chalk';
9
9
  import { readPackageUp } from 'read-package-up';
10
10
  import { Command } from 'commander';
11
- import path, { join, resolve as resolve$1, parse, dirname as dirname$1, extname, relative } from 'node:path';
11
+ import path, { join, resolve as resolve$1, parse, dirname as dirname$1, extname, relative, basename } from 'node:path';
12
12
  import { MultiBar, Presets } from 'cli-progress';
13
13
  import { Spinner } from '@topcli/spinner';
14
- import fs, { mkdir, writeFile, readFile as readFile$1, appendFile, access, readdir } from 'node:fs/promises';
14
+ import fs, { mkdir, writeFile, readFile as readFile$1, appendFile, access, constants, readdir, unlink, stat } from 'node:fs/promises';
15
15
  import filenamify from 'filenamify';
16
- import { select, password, input, confirm } from '@inquirer/prompts';
17
16
  import { ManagementApiClient } from '@storyblok/management-api-client';
18
17
  import { RateLimit, Sema } from 'async-sema';
18
+ import { select, password, input, confirm } from '@inquirer/prompts';
19
19
  import { exec, spawn } from 'node:child_process';
20
20
  import { promisify } from 'node:util';
21
21
  import { minimatch } from 'minimatch';
@@ -24,6 +24,10 @@ import { hash } from 'ohash';
24
24
  import { compile } from 'json-schema-to-typescript';
25
25
  import open from 'open';
26
26
  import { Octokit } from 'octokit';
27
+ import { pipeline as pipeline$1 } from 'node:stream/promises';
28
+ import { Buffer } from 'node:buffer';
29
+ import Storyblok from 'storyblok-js-client';
30
+ import { createHash } from 'node:crypto';
27
31
 
28
32
  const commands = {
29
33
  LOGIN: "login",
@@ -37,7 +41,9 @@ const commands = {
37
41
  DATASOURCES: "datasources",
38
42
  CREATE: "create",
39
43
  LOGS: "logs",
40
- REPORTS: "reports"
44
+ REPORTS: "reports",
45
+ ASSETS: "assets",
46
+ STORIES: "stories"
41
47
  };
42
48
  const colorPalette = {
43
49
  PRIMARY: "#8d60ff",
@@ -55,7 +61,9 @@ const colorPalette = {
55
61
  PRESETS: "#a855f7",
56
62
  DATASOURCES: "#4ade80",
57
63
  LOGS: "#4ade80",
58
- REPORTS: "#4ade80"
64
+ REPORTS: "#4ade80",
65
+ ASSETS: "#f97316",
66
+ STORIES: "#a185ff"
59
67
  };
60
68
  const regions = {
61
69
  EU: "eu",
@@ -96,10 +104,12 @@ const regionNames = {
96
104
  SB_Agent_Version: process.env.npm_package_version || "4.x"
97
105
  });
98
106
  const directories = {
99
- log: "logs",
100
- report: "reports",
107
+ assets: "assets",
101
108
  components: "components",
102
- datasources: "datasources"
109
+ datasources: "datasources",
110
+ logs: "logs",
111
+ reports: "reports",
112
+ stories: "stories"
103
113
  };
104
114
 
105
115
  function isPlainObject(value) {
@@ -600,7 +610,17 @@ const API_ACTIONS = {
600
610
  update_component_preset: "Failed to update component preset",
601
611
  pull_stories: "Failed to pull stories",
602
612
  pull_story: "Failed to pull story",
613
+ create_story: "Failed to create story",
603
614
  update_story: "Failed to update story",
615
+ pull_asset: "Failed to pull asset",
616
+ pull_assets: "Failed to pull assets",
617
+ pull_asset_folder: "Failed to pull asset folder",
618
+ pull_asset_folders: "Failed to pull asset folders",
619
+ push_asset_folder: "Failed to push asset folder",
620
+ push_asset_sign: "Failed to sign asset upload",
621
+ push_asset_upload: "Failed to upload asset",
622
+ push_asset_finish: "Failed to finish asset upload",
623
+ push_asset_update: "Failed to update asset",
604
624
  pull_datasources: "Failed to pull datasources",
605
625
  push_datasource: "Failed to push datasource",
606
626
  update_datasource: "Failed to update datasource",
@@ -860,7 +880,7 @@ function handleVerboseError(error) {
860
880
  konsola.error(`Unexpected Error`, error);
861
881
  }
862
882
  }
863
- function handleError(error, verbose = false) {
883
+ function handleError(error, verbose = false, context) {
864
884
  if (error instanceof APIError || error instanceof FileSystemError) {
865
885
  const messageStack = error.messageStack;
866
886
  messageStack.forEach((message, index) => {
@@ -883,7 +903,10 @@ function handleError(error, verbose = false) {
883
903
  if (!process.env.VITEST) {
884
904
  console.log("");
885
905
  }
886
- getLogger().error(error.message, { error, errorCode: "code" in error ? String(error.code) : "UNKNOWN_ERROR" });
906
+ getLogger().error(error.message, { error, errorCode: "code" in error ? String(error.code) : "UNKNOWN_ERROR", context });
907
+ }
908
+ function logOnlyError(error, context) {
909
+ getLogger().error(error.message, { error, errorCode: "code" in error ? String(error.code) : "UNKNOWN_ERROR", context });
887
910
  }
888
911
 
889
912
  function requireAuthentication(state, verbose = false) {
@@ -1148,19 +1171,22 @@ const saveToFileSync = (filePath, data, options) => {
1148
1171
  }
1149
1172
  };
1150
1173
  const appendToFile = async (filePath, data, options) => {
1151
- const resolvedPath = parse(filePath).dir;
1152
- try {
1153
- await mkdir(resolvedPath, { recursive: true });
1154
- } catch (mkdirError) {
1155
- handleFileSystemError("mkdir", mkdirError);
1156
- return;
1157
- }
1158
- try {
1159
- const dataWithNewline = data.endsWith("\n") ? data : `${data}
1174
+ const dataWithNewline = data.endsWith("\n") ? data : `${data}
1160
1175
  `;
1176
+ try {
1161
1177
  await appendFile(filePath, dataWithNewline, options);
1162
- } catch (writeError) {
1163
- handleFileSystemError("write", writeError);
1178
+ } catch (maybeError) {
1179
+ const error = toError(maybeError);
1180
+ if ("code" in error && error.code === "ENOENT") {
1181
+ const dir = parse(filePath).dir;
1182
+ await mkdir(dir, { recursive: true });
1183
+ await appendFile(filePath, dataWithNewline, options);
1184
+ } else {
1185
+ handleFileSystemError(
1186
+ "syscall" in error && error.syscall === "mkdir" ? "mkdir" : "write",
1187
+ error
1188
+ );
1189
+ }
1164
1190
  }
1165
1191
  };
1166
1192
  const appendToFileSync = (filePath, data, options) => {
@@ -1187,6 +1213,30 @@ const readFile = async (filePath) => {
1187
1213
  return "";
1188
1214
  }
1189
1215
  };
1216
+ const loadManifest = async (manifestFile) => {
1217
+ return readFile$1(manifestFile, "utf8").then((manifest) => manifest.split("\n").filter(Boolean).map((entry) => JSON.parse(entry))).catch((error) => {
1218
+ if (error && error.code === "ENOENT") {
1219
+ return [];
1220
+ }
1221
+ throw error;
1222
+ });
1223
+ };
1224
+ const saveManifest = async (manifestFile, entries) => {
1225
+ const content = entries.map((entry) => JSON.stringify(entry)).join("\n");
1226
+ await saveToFile(manifestFile, content ? `${content}
1227
+ ` : "");
1228
+ };
1229
+ const deduplicateManifest = async (manifestFile) => {
1230
+ const entries = await loadManifest(manifestFile);
1231
+ if (entries.length === 0) {
1232
+ return;
1233
+ }
1234
+ const uniqueEntries = /* @__PURE__ */ new Map();
1235
+ for (const entry of entries) {
1236
+ uniqueEntries.set(entry.old_id, entry);
1237
+ }
1238
+ await saveManifest(manifestFile, Array.from(uniqueEntries.values()));
1239
+ };
1190
1240
  const resolvePath = (path, folder) => {
1191
1241
  const basePath = path ?? DEFAULT_STORAGE_DIR;
1192
1242
  return resolve$1(process.cwd(), basePath, folder);
@@ -1205,6 +1255,15 @@ const sanitizeFilename = (filename) => {
1205
1255
  replacement: "_"
1206
1256
  });
1207
1257
  };
1258
+ async function readDirectory(directoryPath) {
1259
+ try {
1260
+ const files = await readdir(directoryPath);
1261
+ return files;
1262
+ } catch (maybeError) {
1263
+ handleFileSystemError("read", toError(maybeError));
1264
+ return [];
1265
+ }
1266
+ }
1208
1267
  async function readJsonFile(filePath) {
1209
1268
  try {
1210
1269
  const content = (await readFile(filePath)).toString();
@@ -1218,7 +1277,15 @@ async function readJsonFile(filePath) {
1218
1277
  }
1219
1278
  }
1220
1279
  function importModule(filePath) {
1221
- return import(`file://${filePath}`);
1280
+ return import(pathToFileURL(filePath).href);
1281
+ }
1282
+ async function fileExists(path) {
1283
+ try {
1284
+ await access(path, constants.F_OK);
1285
+ return true;
1286
+ } catch {
1287
+ return false;
1288
+ }
1222
1289
  }
1223
1290
 
1224
1291
  const REPORT_STATUS = {
@@ -1399,7 +1466,7 @@ class FileTransport {
1399
1466
  name: value.name,
1400
1467
  message: value.message,
1401
1468
  httpCode: value.code,
1402
- httpStatusText: value.error?.response.statusText,
1469
+ httpStatusText: value.error?.response?.statusText,
1403
1470
  stack: value.stack
1404
1471
  };
1405
1472
  continue;
@@ -1491,7 +1558,7 @@ class ConsoleTransport {
1491
1558
  name: value.name,
1492
1559
  message: value.message,
1493
1560
  httpCode: value.code,
1494
- httpStatusText: value.error?.response.statusText,
1561
+ httpStatusText: value.error?.response?.statusText,
1495
1562
  stack: value.stack
1496
1563
  });
1497
1564
  }
@@ -1512,6 +1579,174 @@ class ConsoleTransport {
1512
1579
  }
1513
1580
  }
1514
1581
 
1582
+ const getCredentials = async (filePath = join(getStoryblokGlobalPath(), "credentials.json")) => {
1583
+ try {
1584
+ await access(filePath);
1585
+ const content = await readFile(filePath);
1586
+ const parsedContent = JSON.parse(content);
1587
+ if (Object.keys(parsedContent).length === 0) {
1588
+ return null;
1589
+ }
1590
+ return parsedContent;
1591
+ } catch (error) {
1592
+ if (error.code === "ENOENT") {
1593
+ await saveToFile(filePath, JSON.stringify({}, null, 2), { mode: 384 });
1594
+ return null;
1595
+ }
1596
+ handleFileSystemError("read", error);
1597
+ return null;
1598
+ }
1599
+ };
1600
+ const addCredentials = async ({
1601
+ filePath = join(getStoryblokGlobalPath(), "credentials.json"),
1602
+ machineName,
1603
+ login,
1604
+ password,
1605
+ region
1606
+ }) => {
1607
+ const credentials = {
1608
+ ...await getCredentials(filePath),
1609
+ [machineName]: {
1610
+ login,
1611
+ password,
1612
+ region
1613
+ }
1614
+ };
1615
+ try {
1616
+ await saveToFile(filePath, JSON.stringify(credentials, null, 2), { mode: 384 });
1617
+ } catch (error) {
1618
+ throw new FileSystemError("invalid_argument", "write", error, `Error adding/updating entry for machine ${machineName} in credentials.json file`);
1619
+ }
1620
+ };
1621
+ const removeAllCredentials = async (filepath = getStoryblokGlobalPath()) => {
1622
+ const filePath = join(filepath, "credentials.json");
1623
+ await saveToFile(filePath, JSON.stringify({}, null, 2), { mode: 384 });
1624
+ };
1625
+
1626
+ let sessionInstance = null;
1627
+ function createSession() {
1628
+ const state = {
1629
+ isLoggedIn: false
1630
+ };
1631
+ async function initializeSession() {
1632
+ const envCredentials = getEnvCredentials();
1633
+ if (envCredentials) {
1634
+ state.isLoggedIn = true;
1635
+ state.login = envCredentials.login;
1636
+ state.password = envCredentials.password;
1637
+ state.region = envCredentials.region;
1638
+ state.envLogin = true;
1639
+ return;
1640
+ }
1641
+ const credentials = await getCredentials();
1642
+ if (credentials) {
1643
+ const creds = Object.values(credentials)[0];
1644
+ state.isLoggedIn = true;
1645
+ state.login = creds.login;
1646
+ state.password = creds.password;
1647
+ state.region = creds.region;
1648
+ } else {
1649
+ state.isLoggedIn = false;
1650
+ state.login = void 0;
1651
+ state.password = void 0;
1652
+ state.region = void 0;
1653
+ }
1654
+ state.envLogin = false;
1655
+ }
1656
+ function getEnvCredentials() {
1657
+ const envLogin = process.env.STORYBLOK_LOGIN || process.env.TRAVIS_STORYBLOK_LOGIN;
1658
+ const envPassword = process.env.STORYBLOK_TOKEN || process.env.TRAVIS_STORYBLOK_TOKEN;
1659
+ const envRegion = process.env.STORYBLOK_REGION || process.env.TRAVIS_STORYBLOK_REGION;
1660
+ if (envLogin && envPassword && envRegion) {
1661
+ return {
1662
+ login: envLogin,
1663
+ password: envPassword,
1664
+ region: envRegion
1665
+ };
1666
+ }
1667
+ return null;
1668
+ }
1669
+ async function persistCredentials(region) {
1670
+ if (state.isLoggedIn && state.login && state.password && state.region) {
1671
+ await addCredentials({
1672
+ machineName: regionsDomain[region] || "mapi.storyblok.com",
1673
+ login: state.login,
1674
+ password: state.password,
1675
+ region: state.region
1676
+ });
1677
+ } else {
1678
+ throw new Error("No credentials to save.");
1679
+ }
1680
+ }
1681
+ function updateSession(login, password, region) {
1682
+ state.isLoggedIn = true;
1683
+ state.login = login;
1684
+ state.password = password;
1685
+ state.region = region;
1686
+ }
1687
+ function logout() {
1688
+ state.isLoggedIn = false;
1689
+ state.login = void 0;
1690
+ state.password = void 0;
1691
+ state.region = void 0;
1692
+ }
1693
+ return {
1694
+ state,
1695
+ initializeSession,
1696
+ updateSession,
1697
+ persistCredentials,
1698
+ logout
1699
+ };
1700
+ }
1701
+ function session() {
1702
+ if (!sessionInstance) {
1703
+ sessionInstance = createSession();
1704
+ }
1705
+ return sessionInstance;
1706
+ }
1707
+
1708
+ let instance = null;
1709
+ let storedConfig = null;
1710
+ let currentLimiterCapacity = Math.max(1, getActiveConfig().api.maxConcurrency);
1711
+ let limiter = RateLimit(currentLimiterCapacity, { uniformDistribution: true });
1712
+ function resolveLimiter() {
1713
+ const desiredCapacity = Math.max(1, getActiveConfig().api.maxConcurrency);
1714
+ if (desiredCapacity !== currentLimiterCapacity) {
1715
+ limiter = RateLimit(desiredCapacity, { uniformDistribution: true });
1716
+ currentLimiterCapacity = desiredCapacity;
1717
+ }
1718
+ return limiter;
1719
+ }
1720
+ function configsAreEqual(config1, config2) {
1721
+ return JSON.stringify(config1) === JSON.stringify(config2);
1722
+ }
1723
+ function applyRateLimit(client) {
1724
+ if (getActiveConfig().api.maxConcurrency > 0) {
1725
+ client.interceptors.request.use(async (request) => {
1726
+ const limit = resolveLimiter();
1727
+ await limit();
1728
+ return request;
1729
+ });
1730
+ }
1731
+ }
1732
+ function creategetMapiClient(options) {
1733
+ const client = new ManagementApiClient(options);
1734
+ applyRateLimit(client);
1735
+ return client;
1736
+ }
1737
+ function getMapiClient(options) {
1738
+ if (!instance && options) {
1739
+ instance = creategetMapiClient(options);
1740
+ storedConfig = options;
1741
+ } else if (!instance) {
1742
+ throw new Error("MAPI client not initialized. Call getMapiClient with configuration first.");
1743
+ } else if (options && storedConfig && !configsAreEqual(options, storedConfig)) {
1744
+ instance = creategetMapiClient(options);
1745
+ storedConfig = options;
1746
+ }
1747
+ return instance;
1748
+ }
1749
+
1515
1750
  const packageJson = getPackageJson();
1516
1751
  let programInstance = null;
1517
1752
  function getProgram() {
@@ -1540,6 +1775,16 @@ function getProgram() {
1540
1775
  const resolvedConfig = await resolveConfig(targetCommand, ancestry);
1541
1776
  applyConfigToCommander(ancestry, resolvedConfig);
1542
1777
  setActiveConfig(resolvedConfig);
1778
+ const { state, initializeSession } = session();
1779
+ await initializeSession();
1780
+ if (state.password) {
1781
+ getMapiClient({
1782
+ token: {
1783
+ accessToken: state.password
1784
+ },
1785
+ region: state.region ?? resolvedConfig.region
1786
+ });
1787
+ }
1543
1788
  const options = targetCommand.optsWithGlobals();
1544
1789
  const commandPieces = [];
1545
1790
  for (let c = targetCommand; c; c = c.parent) {
@@ -1558,7 +1803,7 @@ function getProgram() {
1558
1803
  }
1559
1804
  if (resolvedConfig.log.file.enabled) {
1560
1805
  const logsPath = resolveCommandPath(
1561
- directories.log,
1806
+ directories.logs,
1562
1807
  options.space,
1563
1808
  options.path
1564
1809
  );
@@ -1582,7 +1827,7 @@ function getProgram() {
1582
1827
  getUI({ enabled: resolvedConfig.ui.enabled });
1583
1828
  if (resolvedConfig.report.enabled) {
1584
1829
  const reportPath = resolveCommandPath(
1585
- directories.report,
1830
+ directories.reports,
1586
1831
  options.space,
1587
1832
  options.path
1588
1833
  );
@@ -1611,47 +1856,9 @@ const getStoryblokUrl = (region = "eu") => {
1611
1856
  return `https://${managementApiRegions[region]}/${API_VERSION}`;
1612
1857
  };
1613
1858
 
1614
- let instance = null;
1615
- let storedConfig = null;
1616
- let currentLimiterCapacity = Math.max(1, getActiveConfig().api.maxConcurrency);
1617
- let limiter = RateLimit(currentLimiterCapacity, { uniformDistribution: true });
1618
- function resolveLimiter() {
1619
- const desiredCapacity = Math.max(1, getActiveConfig().api.maxConcurrency);
1620
- if (desiredCapacity !== currentLimiterCapacity) {
1621
- limiter = RateLimit(desiredCapacity, { uniformDistribution: true });
1622
- currentLimiterCapacity = desiredCapacity;
1623
- }
1624
- return limiter;
1625
- }
1626
- function configsAreEqual(config1, config2) {
1627
- return JSON.stringify(config1) === JSON.stringify(config2);
1628
- }
1629
- function mapiClient(options) {
1630
- if (!instance && options) {
1631
- instance = new ManagementApiClient(options);
1632
- instance.interceptors.request.use(async (request) => {
1633
- const limit = resolveLimiter();
1634
- await limit();
1635
- return request;
1636
- });
1637
- storedConfig = options;
1638
- } else if (!instance) {
1639
- throw new Error("MAPI client not initialized. Call mapiClient with configuration first.");
1640
- } else if (options && storedConfig && !configsAreEqual(options, storedConfig)) {
1641
- instance = new ManagementApiClient(options);
1642
- instance.interceptors.request.use(async (request) => {
1643
- const limit = resolveLimiter();
1644
- await limit();
1645
- return request;
1646
- });
1647
- storedConfig = options;
1648
- }
1649
- return instance;
1650
- }
1651
-
1652
1859
  const getUser = async (token, region) => {
1653
1860
  try {
1654
- const client = mapiClient({
1861
+ const client = creategetMapiClient({
1655
1862
  token: {
1656
1863
  accessToken: token
1657
1864
  },
@@ -1736,137 +1943,11 @@ const loginWithOtp = async (email, password, otp, region) => {
1736
1943
  }
1737
1944
  };
1738
1945
 
1739
- const getCredentials = async (filePath = join(getStoryblokGlobalPath(), "credentials.json")) => {
1740
- try {
1741
- await access(filePath);
1742
- const content = await readFile(filePath);
1743
- const parsedContent = JSON.parse(content);
1744
- if (Object.keys(parsedContent).length === 0) {
1745
- return null;
1746
- }
1747
- return parsedContent;
1748
- } catch (error) {
1749
- if (error.code === "ENOENT") {
1750
- await saveToFile(filePath, JSON.stringify({}, null, 2), { mode: 384 });
1751
- return null;
1752
- }
1753
- handleFileSystemError("read", error);
1754
- return null;
1755
- }
1756
- };
1757
- const addCredentials = async ({
1758
- filePath = join(getStoryblokGlobalPath(), "credentials.json"),
1759
- machineName,
1760
- login,
1761
- password,
1762
- region
1763
- }) => {
1764
- const credentials = {
1765
- ...await getCredentials(filePath),
1766
- [machineName]: {
1767
- login,
1768
- password,
1769
- region
1770
- }
1771
- };
1772
- try {
1773
- await saveToFile(filePath, JSON.stringify(credentials, null, 2), { mode: 384 });
1774
- } catch (error) {
1775
- throw new FileSystemError("invalid_argument", "write", error, `Error adding/updating entry for machine ${machineName} in credentials.json file`);
1776
- }
1777
- };
1778
- const removeAllCredentials = async (filepath = getStoryblokGlobalPath()) => {
1779
- const filePath = join(filepath, "credentials.json");
1780
- await saveToFile(filePath, JSON.stringify({}, null, 2), { mode: 384 });
1781
- };
1782
-
1783
- let sessionInstance = null;
1784
- function createSession() {
1785
- const state = {
1786
- isLoggedIn: false
1787
- };
1788
- async function initializeSession() {
1789
- const envCredentials = getEnvCredentials();
1790
- if (envCredentials) {
1791
- state.isLoggedIn = true;
1792
- state.login = envCredentials.login;
1793
- state.password = envCredentials.password;
1794
- state.region = envCredentials.region;
1795
- state.envLogin = true;
1796
- return;
1797
- }
1798
- const credentials = await getCredentials();
1799
- if (credentials) {
1800
- const creds = Object.values(credentials)[0];
1801
- state.isLoggedIn = true;
1802
- state.login = creds.login;
1803
- state.password = creds.password;
1804
- state.region = creds.region;
1805
- } else {
1806
- state.isLoggedIn = false;
1807
- state.login = void 0;
1808
- state.password = void 0;
1809
- state.region = void 0;
1810
- }
1811
- state.envLogin = false;
1812
- }
1813
- function getEnvCredentials() {
1814
- const envLogin = process.env.STORYBLOK_LOGIN || process.env.TRAVIS_STORYBLOK_LOGIN;
1815
- const envPassword = process.env.STORYBLOK_TOKEN || process.env.TRAVIS_STORYBLOK_TOKEN;
1816
- const envRegion = process.env.STORYBLOK_REGION || process.env.TRAVIS_STORYBLOK_REGION;
1817
- if (envLogin && envPassword && envRegion) {
1818
- return {
1819
- login: envLogin,
1820
- password: envPassword,
1821
- region: envRegion
1822
- };
1823
- }
1824
- return null;
1825
- }
1826
- async function persistCredentials(region) {
1827
- if (state.isLoggedIn && state.login && state.password && state.region) {
1828
- await addCredentials({
1829
- machineName: regionsDomain[region] || "mapi.storyblok.com",
1830
- login: state.login,
1831
- password: state.password,
1832
- region: state.region
1833
- });
1834
- } else {
1835
- throw new Error("No credentials to save.");
1836
- }
1837
- }
1838
- function updateSession(login, password, region) {
1839
- state.isLoggedIn = true;
1840
- state.login = login;
1841
- state.password = password;
1842
- state.region = region;
1843
- }
1844
- function logout() {
1845
- state.isLoggedIn = false;
1846
- state.login = void 0;
1847
- state.password = void 0;
1848
- state.region = void 0;
1849
- }
1850
- return {
1851
- state,
1852
- initializeSession,
1853
- updateSession,
1854
- persistCredentials,
1855
- logout
1856
- };
1857
- }
1858
- function session() {
1859
- if (!sessionInstance) {
1860
- sessionInstance = createSession();
1861
- }
1862
- return sessionInstance;
1863
- }
1864
-
1865
- async function performInteractiveLogin(options) {
1866
- const { verbose = false, preSelectedRegion, showWelcomeMessage = true } = options || {};
1867
- const spinner = new Spinner({
1868
- verbose: !isVitest
1869
- });
1946
+ async function performInteractiveLogin(options) {
1947
+ const { verbose = false, preSelectedRegion, showWelcomeMessage = true } = options || {};
1948
+ const spinner = new Spinner({
1949
+ verbose: !isVitest
1950
+ });
1870
1951
  try {
1871
1952
  const strategy = await select({
1872
1953
  message: "How would you like to login?",
@@ -1971,17 +2052,16 @@ async function performInteractiveLogin(options) {
1971
2052
  }
1972
2053
  }
1973
2054
 
1974
- const program$h = getProgram();
2055
+ const program$j = getProgram();
1975
2056
  const allRegionsText = Object.values(regions).join(",");
1976
- program$h.command(commands.LOGIN).description("Login to the Storyblok CLI").option("-t, --token <token>", "Token to login directly without questions, like for CI environments").option(
2057
+ program$j.command(commands.LOGIN).description("Login to the Storyblok CLI").option("-t, --token <token>", "Token to login directly without questions, like for CI environments").option(
1977
2058
  "-r, --region <region>",
1978
2059
  `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}.`
1979
2060
  ).action(async (options) => {
1980
2061
  konsola.title(`${commands.LOGIN}`, colorPalette.LOGIN);
1981
- const verbose = program$h.opts().verbose;
2062
+ const verbose = program$j.opts().verbose;
1982
2063
  const { token, region } = options;
1983
- const { state, updateSession, persistCredentials, initializeSession } = session();
1984
- await initializeSession();
2064
+ const { state, updateSession, persistCredentials } = session();
1985
2065
  if (state.isLoggedIn && !state.envLogin) {
1986
2066
  konsola.ok(`You are already logged in. If you want to login with a different account, please logout first.`);
1987
2067
  return;
@@ -2037,13 +2117,12 @@ program$h.command(commands.LOGIN).description("Login to the Storyblok CLI").opti
2037
2117
  konsola.br();
2038
2118
  });
2039
2119
 
2040
- const program$g = getProgram();
2041
- program$g.command(commands.LOGOUT).description("Logout from the Storyblok CLI").action(async () => {
2120
+ const program$i = getProgram();
2121
+ program$i.command(commands.LOGOUT).description("Logout from the Storyblok CLI").action(async () => {
2042
2122
  konsola.title(`${commands.LOGOUT}`, colorPalette.LOGOUT);
2043
- const verbose = program$g.opts().verbose;
2123
+ const verbose = program$i.opts().verbose;
2044
2124
  try {
2045
- const { state, initializeSession } = session();
2046
- await initializeSession();
2125
+ const { state } = session();
2047
2126
  if (!state.isLoggedIn || !state.password || !state.region) {
2048
2127
  konsola.warn(`You are already logged out. If you want to login, please use the login command.`);
2049
2128
  konsola.br();
@@ -2088,12 +2167,11 @@ async function openSignupInBrowser(url) {
2088
2167
  }
2089
2168
  }
2090
2169
 
2091
- const program$f = getProgram();
2092
- program$f.command(commands.SIGNUP).description("Sign up for Storyblok").action(async () => {
2170
+ const program$h = getProgram();
2171
+ program$h.command(commands.SIGNUP).description("Sign up for Storyblok").action(async () => {
2093
2172
  konsola.title(`${commands.SIGNUP}`, colorPalette.SIGNUP);
2094
- const verbose = program$f.opts().verbose;
2095
- const { state, initializeSession } = session();
2096
- await initializeSession();
2173
+ const verbose = program$h.opts().verbose;
2174
+ const { state } = session();
2097
2175
  if (state.isLoggedIn && !state.envLogin) {
2098
2176
  konsola.ok(`You are already logged in. If you want to signup with a different account, please logout first.`);
2099
2177
  return;
@@ -2113,12 +2191,11 @@ program$f.command(commands.SIGNUP).description("Sign up for Storyblok").action(a
2113
2191
  konsola.br();
2114
2192
  });
2115
2193
 
2116
- const program$e = getProgram();
2117
- program$e.command(commands.USER).description("Get the current user").action(async () => {
2194
+ const program$g = getProgram();
2195
+ program$g.command(commands.USER).description("Get the current user").action(async () => {
2118
2196
  konsola.title(`${commands.USER}`, colorPalette.USER);
2119
- const verbose = program$e.opts().verbose;
2120
- const { state, initializeSession } = session();
2121
- await initializeSession();
2197
+ const verbose = program$g.opts().verbose;
2198
+ const { state } = session();
2122
2199
  if (!requireAuthentication(state)) {
2123
2200
  return;
2124
2201
  }
@@ -2145,8 +2222,8 @@ program$e.command(commands.USER).description("Get the current user").action(asyn
2145
2222
  konsola.br();
2146
2223
  });
2147
2224
 
2148
- const program$d = getProgram();
2149
- const componentsCommand = program$d.command(commands.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");
2225
+ const program$f = getProgram();
2226
+ const componentsCommand = program$f.command(commands.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");
2150
2227
 
2151
2228
  const DEFAULT_COMPONENTS_FILENAME = "components";
2152
2229
  const DEFAULT_GROUPS_FILENAME = "groups";
@@ -2155,7 +2232,7 @@ const DEFAULT_TAGS_FILENAME = "tags";
2155
2232
 
2156
2233
  const fetchComponents = async (spaceId) => {
2157
2234
  try {
2158
- const client = mapiClient();
2235
+ const client = getMapiClient();
2159
2236
  const { data } = await client.components.list({
2160
2237
  path: {
2161
2238
  space_id: spaceId
@@ -2169,7 +2246,7 @@ const fetchComponents = async (spaceId) => {
2169
2246
  };
2170
2247
  const fetchComponent = async (spaceId, componentName) => {
2171
2248
  try {
2172
- const client = mapiClient();
2249
+ const client = getMapiClient();
2173
2250
  const { data } = await client.components.list({
2174
2251
  path: {
2175
2252
  space_id: spaceId
@@ -2186,7 +2263,7 @@ const fetchComponent = async (spaceId, componentName) => {
2186
2263
  };
2187
2264
  const fetchComponentGroups = async (spaceId) => {
2188
2265
  try {
2189
- const client = mapiClient();
2266
+ const client = getMapiClient();
2190
2267
  const { data } = await client.componentFolders.list({
2191
2268
  path: {
2192
2269
  space_id: spaceId
@@ -2199,7 +2276,7 @@ const fetchComponentGroups = async (spaceId) => {
2199
2276
  };
2200
2277
  const fetchComponentPresets = async (spaceId) => {
2201
2278
  try {
2202
- const client = mapiClient();
2279
+ const client = getMapiClient();
2203
2280
  const { data } = await client.presets.list({
2204
2281
  path: {
2205
2282
  space_id: spaceId
@@ -2212,7 +2289,7 @@ const fetchComponentPresets = async (spaceId) => {
2212
2289
  };
2213
2290
  const fetchComponentInternalTags = async (spaceId) => {
2214
2291
  try {
2215
- const client = mapiClient();
2292
+ const client = getMapiClient();
2216
2293
  const { data } = await client.internalTags.list({
2217
2294
  path: {
2218
2295
  space_id: spaceId
@@ -2266,7 +2343,7 @@ const saveComponentsToFiles = async (space, spaceData, options) => {
2266
2343
 
2267
2344
  const pushComponent = async (space, component) => {
2268
2345
  try {
2269
- const client = mapiClient();
2346
+ const client = getMapiClient();
2270
2347
  const { data } = await client.components.create({
2271
2348
  path: {
2272
2349
  space_id: space
@@ -2282,7 +2359,7 @@ const pushComponent = async (space, component) => {
2282
2359
  };
2283
2360
  const updateComponent = async (space, componentId, component) => {
2284
2361
  try {
2285
- const client = mapiClient();
2362
+ const client = getMapiClient();
2286
2363
  const { data } = await client.components.update({
2287
2364
  path: {
2288
2365
  space_id: Number(space),
@@ -2307,7 +2384,7 @@ const upsertComponent = async (space, component, existingId) => {
2307
2384
  };
2308
2385
  const pushComponentGroup = async (space, componentGroup) => {
2309
2386
  try {
2310
- const client = mapiClient();
2387
+ const client = getMapiClient();
2311
2388
  const { data } = await client.componentFolders.create({
2312
2389
  path: {
2313
2390
  space_id: Number(space)
@@ -2324,7 +2401,7 @@ const pushComponentGroup = async (space, componentGroup) => {
2324
2401
  };
2325
2402
  const updateComponentGroup = async (space, groupId, componentGroup) => {
2326
2403
  try {
2327
- const client = mapiClient();
2404
+ const client = getMapiClient();
2328
2405
  const { data } = await client.componentFolders.update({
2329
2406
  path: {
2330
2407
  space_id: Number(space),
@@ -2349,7 +2426,7 @@ const upsertComponentGroup = async (space, group, existingId) => {
2349
2426
  };
2350
2427
  const pushComponentPreset = async (space, preset) => {
2351
2428
  try {
2352
- const client = mapiClient();
2429
+ const client = getMapiClient();
2353
2430
  const { data } = await client.presets.create({
2354
2431
  path: {
2355
2432
  space_id: Number(space)
@@ -2366,7 +2443,7 @@ const pushComponentPreset = async (space, preset) => {
2366
2443
  };
2367
2444
  const updateComponentPreset = async (space, presetId, preset) => {
2368
2445
  try {
2369
- const client = mapiClient();
2446
+ const client = getMapiClient();
2370
2447
  const { data } = await client.presets.update({
2371
2448
  path: {
2372
2449
  space_id: Number(space),
@@ -2391,7 +2468,7 @@ const upsertComponentPreset = async (space, preset, existingId) => {
2391
2468
  };
2392
2469
  const pushComponentInternalTag = async (space, componentInternalTag) => {
2393
2470
  try {
2394
- const client = mapiClient();
2471
+ const client = getMapiClient();
2395
2472
  const { data } = await client.internalTags.create({
2396
2473
  path: {
2397
2474
  space_id: Number(space)
@@ -2406,7 +2483,7 @@ const pushComponentInternalTag = async (space, componentInternalTag) => {
2406
2483
  };
2407
2484
  const updateComponentInternalTag = async (space, tagId, componentInternalTag) => {
2408
2485
  try {
2409
- const client = mapiClient();
2486
+ const client = getMapiClient();
2410
2487
  const { data } = await client.internalTags.update({
2411
2488
  path: {
2412
2489
  space_id: Number(space),
@@ -2531,10 +2608,10 @@ async function readConsolidatedFiles$1(resolvedPath, suffix) {
2531
2608
  };
2532
2609
  }
2533
2610
 
2534
- const program$c = getProgram();
2611
+ const program$e = getProgram();
2535
2612
  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) => {
2536
2613
  konsola.title(`${commands.COMPONENTS}`, colorPalette.COMPONENTS, componentName ? `Pulling component ${componentName}...` : "Pulling components...");
2537
- const verbose = program$c.opts().verbose;
2614
+ const verbose = program$e.opts().verbose;
2538
2615
  const { space, path } = componentsCommand.opts();
2539
2616
  const {
2540
2617
  separateFiles = false,
@@ -2543,8 +2620,7 @@ componentsCommand.command("pull [componentName]").option("-f, --filename <filena
2543
2620
  } = options;
2544
2621
  const actualFilename = filename ?? DEFAULT_COMPONENTS_FILENAME;
2545
2622
  const componentsOutputDir = resolveCommandPath(directories.components, space, path);
2546
- const { state, initializeSession } = session();
2547
- await initializeSession();
2623
+ const { state } = session();
2548
2624
  if (!requireAuthentication(state, verbose)) {
2549
2625
  return;
2550
2626
  }
@@ -2552,13 +2628,6 @@ componentsCommand.command("pull [componentName]").option("-f, --filename <filena
2552
2628
  handleError(new CommandError(`Please provide the space as argument --space YOUR_SPACE_ID.`), verbose);
2553
2629
  return;
2554
2630
  }
2555
- const { password, region } = state;
2556
- mapiClient({
2557
- token: {
2558
- accessToken: password
2559
- },
2560
- region
2561
- });
2562
2631
  const spinnerGroups = new Spinner({
2563
2632
  verbose: !isVitest
2564
2633
  });
@@ -3576,15 +3645,14 @@ async function pushWithDependencyGraph(space, spaceState, maxConcurrency = getAc
3576
3645
  return results;
3577
3646
  }
3578
3647
 
3579
- const program$b = getProgram();
3648
+ const program$d = getProgram();
3580
3649
  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", false).option("--su, --suffix <suffix>", "Suffix to add to the component name").action(async (componentName, options) => {
3581
3650
  konsola.title(`${commands.COMPONENTS}`, colorPalette.COMPONENTS, componentName ? `Pushing component ${componentName}...` : "Pushing components...");
3582
- const verbose = program$b.opts().verbose;
3651
+ const verbose = program$d.opts().verbose;
3583
3652
  const { space, path } = componentsCommand.opts();
3584
3653
  const { filter } = options;
3585
3654
  const fromSpace = options.from || space;
3586
- const { state, initializeSession } = session();
3587
- await initializeSession();
3655
+ const { state } = session();
3588
3656
  if (!requireAuthentication(state, verbose)) {
3589
3657
  return;
3590
3658
  }
@@ -3594,14 +3662,8 @@ componentsCommand.command("push [componentName]").description(`Push your space's
3594
3662
  }
3595
3663
  konsola.info(`Attempting to push components ${chalk.bold("from")} space ${chalk.hex(colorPalette.COMPONENTS)(fromSpace)} ${chalk.bold("to")} ${chalk.hex(colorPalette.COMPONENTS)(space)}`);
3596
3664
  konsola.br();
3597
- const { password, region } = state;
3598
3665
  let requestCount = 0;
3599
- const client = mapiClient({
3600
- token: {
3601
- accessToken: password
3602
- },
3603
- region
3604
- });
3666
+ const client = getMapiClient();
3605
3667
  client.interceptors.request.use((config) => {
3606
3668
  requestCount++;
3607
3669
  return config;
@@ -3723,7 +3785,7 @@ const DEFAULT_LANGUAGES_FILENAME = "languages";
3723
3785
 
3724
3786
  const fetchSpace = async (spaceId) => {
3725
3787
  try {
3726
- const client = mapiClient();
3788
+ const client = getMapiClient();
3727
3789
  const { data } = await client.spaces.get({
3728
3790
  path: {
3729
3791
  space_id: spaceId
@@ -3737,7 +3799,7 @@ const fetchSpace = async (spaceId) => {
3737
3799
  };
3738
3800
  const createSpace = async (space) => {
3739
3801
  try {
3740
- const client = mapiClient();
3802
+ const client = getMapiClient();
3741
3803
  const { data } = await client.spaces.create({
3742
3804
  body: {
3743
3805
  space
@@ -3775,15 +3837,14 @@ const saveLanguagesToFile = async (space, internationalizationOptions, options)
3775
3837
  }
3776
3838
  };
3777
3839
 
3778
- const program$a = getProgram();
3779
- const languagesCommand = program$a.command(commands.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");
3840
+ const program$c = getProgram();
3841
+ const languagesCommand = program$c.command(commands.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");
3780
3842
  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) => {
3781
3843
  konsola.title(`${commands.LANGUAGES}`, colorPalette.LANGUAGES);
3782
- const verbose = program$a.opts().verbose;
3844
+ const verbose = program$c.opts().verbose;
3783
3845
  const { space, path } = languagesCommand.opts();
3784
3846
  const { filename = "languages", suffix = options.space } = options;
3785
- const { state, initializeSession } = session();
3786
- await initializeSession();
3847
+ const { state } = session();
3787
3848
  if (!requireAuthentication(state, verbose)) {
3788
3849
  return;
3789
3850
  }
@@ -3791,13 +3852,6 @@ languagesCommand.command("pull").description(`Download your space's languages sc
3791
3852
  handleError(new CommandError(`Please provide the space as argument --space YOUR_SPACE_ID.`), verbose);
3792
3853
  return;
3793
3854
  }
3794
- const { password, region } = state;
3795
- mapiClient({
3796
- token: {
3797
- accessToken: password
3798
- },
3799
- region
3800
- });
3801
3855
  const spinner = new Spinner({
3802
3856
  verbose: !isVitest
3803
3857
  });
@@ -3826,8 +3880,8 @@ languagesCommand.command("pull").description(`Download your space's languages sc
3826
3880
  konsola.br();
3827
3881
  });
3828
3882
 
3829
- const program$9 = getProgram();
3830
- const migrationsCommand = program$9.command(commands.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");
3883
+ const program$b = getProgram();
3884
+ const migrationsCommand = program$b.command(commands.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");
3831
3885
 
3832
3886
  const getMigrationTemplate = () => {
3833
3887
  return `export default function (block) {
@@ -3872,8 +3926,7 @@ migrationsCommand.command("generate [componentName]").description("Generate a mi
3872
3926
  handleError(new CommandError(`Please provide the component name as argument ${chalk.hex(colorPalette.MIGRATIONS)("storyblok migrations generate YOUR_COMPONENT_NAME.")}`), verbose);
3873
3927
  return;
3874
3928
  }
3875
- const { state, initializeSession } = session();
3876
- await initializeSession();
3929
+ const { state } = session();
3877
3930
  if (!requireAuthentication(state, verbose)) {
3878
3931
  return;
3879
3932
  }
@@ -3881,13 +3934,6 @@ migrationsCommand.command("generate [componentName]").description("Generate a mi
3881
3934
  handleError(new CommandError(`Please provide the space as argument --space YOUR_SPACE_ID.`), verbose);
3882
3935
  return;
3883
3936
  }
3884
- const { password, region } = state;
3885
- mapiClient({
3886
- token: {
3887
- accessToken: password
3888
- },
3889
- region
3890
- });
3891
3937
  const spinner = ui.createSpinner(`Generating migration for component ${componentName}...`);
3892
3938
  try {
3893
3939
  const component = await fetchComponent(space, componentName);
@@ -3915,7 +3961,7 @@ migrationsCommand.command("generate [componentName]").description("Generate a mi
3915
3961
 
3916
3962
  const fetchStories = async (spaceId, params) => {
3917
3963
  try {
3918
- const client = mapiClient();
3964
+ const client = getMapiClient();
3919
3965
  const { data, response } = await client.stories.list({
3920
3966
  path: {
3921
3967
  space_id: spaceId
@@ -3937,7 +3983,7 @@ const fetchStories = async (spaceId, params) => {
3937
3983
  };
3938
3984
  const fetchStory = async (spaceId, storyId) => {
3939
3985
  try {
3940
- const client = mapiClient();
3986
+ const client = getMapiClient();
3941
3987
  const { data } = await client.stories.get({
3942
3988
  path: {
3943
3989
  space_id: spaceId,
@@ -3945,14 +3991,32 @@ const fetchStory = async (spaceId, storyId) => {
3945
3991
  },
3946
3992
  throwOnError: true
3947
3993
  });
3948
- return data?.story;
3994
+ return data.story;
3949
3995
  } catch (error) {
3950
3996
  handleAPIError("pull_story", error);
3951
3997
  }
3952
3998
  };
3999
+ const createStory = async (spaceId, payload) => {
4000
+ try {
4001
+ const client = getMapiClient();
4002
+ const { data } = await client.stories.create({
4003
+ path: {
4004
+ space_id: spaceId
4005
+ },
4006
+ body: {
4007
+ story: payload.story,
4008
+ publish: payload.publish
4009
+ },
4010
+ throwOnError: true
4011
+ });
4012
+ return data?.story;
4013
+ } catch (maybeError) {
4014
+ handleAPIError("create_story", toError(maybeError));
4015
+ }
4016
+ };
3953
4017
  const updateStory = async (spaceId, storyId, payload) => {
3954
4018
  try {
3955
- const client = mapiClient();
4019
+ const client = getMapiClient();
3956
4020
  const { data } = await client.stories.updateStory({
3957
4021
  path: {
3958
4022
  space_id: spaceId,
@@ -3965,9 +4029,14 @@ const updateStory = async (spaceId, storyId, payload) => {
3965
4029
  },
3966
4030
  throwOnError: true
3967
4031
  });
3968
- return data?.story;
3969
- } catch (error) {
3970
- handleAPIError("update_story", error);
4032
+ const { story } = data;
4033
+ if (!story) {
4034
+ throw new Error("Failed to update story");
4035
+ }
4036
+ return story;
4037
+ } catch (maybeError) {
4038
+ handleAPIError("update_story", toError(maybeError));
4039
+ throw maybeError;
3971
4040
  }
3972
4041
  };
3973
4042
 
@@ -4424,6 +4493,49 @@ const isStoryPublishedWithoutChanges = (story) => {
4424
4493
  const isStoryWithUnpublishedChanges = (story) => {
4425
4494
  return story.published && story.unpublished_changes;
4426
4495
  };
4496
+ const toComponent = (maybeComponent) => {
4497
+ if (maybeComponent.component_group_uuid === void 0) {
4498
+ return null;
4499
+ }
4500
+ return maybeComponent;
4501
+ };
4502
+ const findComponentSchemas = async (directoryPath) => {
4503
+ const files = await readdir(directoryPath).catch((error) => {
4504
+ if (error.code === "ENOENT") {
4505
+ return [];
4506
+ }
4507
+ throw error;
4508
+ });
4509
+ const fileContents = files.filter((f) => path.extname(f) === ".json").map((f) => {
4510
+ const filePath = path.join(directoryPath, f);
4511
+ const fileContent = readFileSync(filePath, "utf-8");
4512
+ return JSON.parse(fileContent);
4513
+ });
4514
+ const components = [];
4515
+ for (const content of fileContents) {
4516
+ if (Array.isArray(content)) {
4517
+ for (const maybeComponent of content) {
4518
+ const component2 = toComponent(maybeComponent);
4519
+ if (component2) {
4520
+ components.push(component2);
4521
+ }
4522
+ }
4523
+ continue;
4524
+ }
4525
+ const component = toComponent(content);
4526
+ if (component) {
4527
+ components.push(component);
4528
+ }
4529
+ }
4530
+ const schemas = {};
4531
+ for (const component of components) {
4532
+ schemas[component.name] = component.schema;
4533
+ }
4534
+ return schemas;
4535
+ };
4536
+ const getStoryFilename = (story) => {
4537
+ return `${story.slug}_${story.uuid}.json`;
4538
+ };
4427
4539
 
4428
4540
  class UpdateStream extends Writable {
4429
4541
  constructor(options) {
@@ -4556,8 +4668,7 @@ migrationsCommand.command("run [componentName]").description("Run migrations").o
4556
4668
  }
4557
4669
  const verbose = program.opts().verbose;
4558
4670
  const { space, path } = migrationsCommand.opts();
4559
- const { state, initializeSession } = session();
4560
- await initializeSession();
4671
+ const { state } = session();
4561
4672
  if (!requireAuthentication(state, verbose)) {
4562
4673
  return;
4563
4674
  }
@@ -4566,13 +4677,6 @@ migrationsCommand.command("run [componentName]").description("Run migrations").o
4566
4677
  return;
4567
4678
  }
4568
4679
  const { filter, dryRun = false, query, startsWith, publish } = options;
4569
- const { password, region } = state;
4570
- mapiClient({
4571
- token: {
4572
- accessToken: password
4573
- },
4574
- region
4575
- });
4576
4680
  try {
4577
4681
  const spinner = ui.createSpinner(`Fetching migration files and stories...`);
4578
4682
  const migrationFiles = await readMigrationFiles({
@@ -4690,8 +4794,7 @@ migrationsCommand.command("rollback [migrationFile]").description("Rollback a mi
4690
4794
  space,
4691
4795
  path
4692
4796
  });
4693
- const { state, initializeSession } = session();
4694
- await initializeSession();
4797
+ const { state } = session();
4695
4798
  if (!requireAuthentication(state, verbose)) {
4696
4799
  return;
4697
4800
  }
@@ -4699,13 +4802,6 @@ migrationsCommand.command("rollback [migrationFile]").description("Rollback a mi
4699
4802
  handleError(new CommandError(`Please provide the space as argument --space YOUR_SPACE_ID.`), verbose);
4700
4803
  return;
4701
4804
  }
4702
- const { password, region } = state;
4703
- mapiClient({
4704
- token: {
4705
- accessToken: password
4706
- },
4707
- region
4708
- });
4709
4805
  try {
4710
4806
  const rollbackData = await readRollbackFile({
4711
4807
  space,
@@ -4763,8 +4859,8 @@ migrationsCommand.command("rollback [migrationFile]").description("Rollback a mi
4763
4859
  }
4764
4860
  });
4765
4861
 
4766
- const program$8 = getProgram();
4767
- const typesCommand = program$8.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");
4862
+ const program$a = getProgram();
4863
+ const typesCommand = program$a.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");
4768
4864
 
4769
4865
  const getAssetJSONSchema = (title) => ({
4770
4866
  $id: "#/asset",
@@ -5522,7 +5618,7 @@ const DEFAULT_DATASOURCES_FILENAME = "datasources";
5522
5618
 
5523
5619
  const pushDatasource = async (spaceId, datasource) => {
5524
5620
  try {
5525
- const client = mapiClient();
5621
+ const client = getMapiClient();
5526
5622
  const { data } = await client.datasources.create({
5527
5623
  path: {
5528
5624
  space_id: spaceId
@@ -5537,7 +5633,7 @@ const pushDatasource = async (spaceId, datasource) => {
5537
5633
  };
5538
5634
  const updateDatasource = async (spaceId, datasourceId, datasource) => {
5539
5635
  try {
5540
- const client = mapiClient();
5636
+ const client = getMapiClient();
5541
5637
  const { data } = await client.datasources.update({
5542
5638
  path: {
5543
5639
  space_id: spaceId,
@@ -5562,7 +5658,7 @@ const upsertDatasource = async (space, datasource, existingId) => {
5562
5658
  };
5563
5659
  const pushDatasourceEntry = async (spaceId, datasourceId, entry) => {
5564
5660
  try {
5565
- const client = mapiClient();
5661
+ const client = getMapiClient();
5566
5662
  const { data } = await client.datasourceEntries.create({
5567
5663
  path: {
5568
5664
  space_id: spaceId
@@ -5582,7 +5678,7 @@ const pushDatasourceEntry = async (spaceId, datasourceId, entry) => {
5582
5678
  };
5583
5679
  const updateDatasourceEntry = async (spaceId, entryId, entry) => {
5584
5680
  try {
5585
- const client = mapiClient();
5681
+ const client = getMapiClient();
5586
5682
  await client.datasourceEntries.updateDatasourceEntry({
5587
5683
  path: {
5588
5684
  space_id: spaceId,
@@ -5674,13 +5770,13 @@ async function readConsolidatedFiles(resolvedPath, suffix) {
5674
5770
  };
5675
5771
  }
5676
5772
 
5677
- const program$7 = getProgram();
5773
+ const program$9 = getProgram();
5678
5774
  typesCommand.command("generate").description("Generate types d.ts for your component schemas").option(
5679
5775
  "--filename <name>",
5680
5776
  "Base file name for all component types when generating a single declarations file (e.g. components.d.ts). Ignored when using --separate-files."
5681
5777
  ).option("--sf, --separate-files", "Generate one .d.ts file per component instead of a single combined file").option("--strict", "strict mode, no loose typing").option("--type-prefix <prefix>", "prefix to be prepended to all generated component type names").option("--type-suffix <suffix>", "suffix to be appended 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) => {
5682
5778
  konsola.title(`${commands.TYPES}`, colorPalette.TYPES, "Generating types...");
5683
- const verbose = program$7.opts().verbose;
5779
+ const verbose = program$9.opts().verbose;
5684
5780
  const { space, path } = typesCommand.opts();
5685
5781
  const spinner = new Spinner({
5686
5782
  verbose: !isVitest
@@ -5733,8 +5829,8 @@ typesCommand.command("generate").description("Generate types d.ts for your compo
5733
5829
  }
5734
5830
  });
5735
5831
 
5736
- const program$6 = getProgram();
5737
- const datasourcesCommand = program$6.command(commands.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");
5832
+ const program$8 = getProgram();
5833
+ const datasourcesCommand = program$8.command(commands.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");
5738
5834
 
5739
5835
  async function fetchAllPages(fetchFunction, extractDataFunction, page = 1, collectedItems = []) {
5740
5836
  const { data, response } = await fetchFunction(page);
@@ -5752,7 +5848,7 @@ async function fetchAllPages(fetchFunction, extractDataFunction, page = 1, colle
5752
5848
  }
5753
5849
  const fetchDatasourceEntries = async (spaceId, datasourceId) => {
5754
5850
  try {
5755
- const client = mapiClient();
5851
+ const client = getMapiClient();
5756
5852
  return await fetchAllPages(
5757
5853
  (page) => client.datasourceEntries.list({
5758
5854
  path: {
@@ -5772,7 +5868,7 @@ const fetchDatasourceEntries = async (spaceId, datasourceId) => {
5772
5868
  };
5773
5869
  const fetchDatasources = async (spaceId) => {
5774
5870
  try {
5775
- const client = mapiClient();
5871
+ const client = getMapiClient();
5776
5872
  const datasources = await fetchAllPages(
5777
5873
  (page) => client.datasources.list({
5778
5874
  path: {
@@ -5801,7 +5897,7 @@ const fetchDatasources = async (spaceId) => {
5801
5897
  };
5802
5898
  const fetchDatasource = async (spaceId, datasourceName) => {
5803
5899
  try {
5804
- const client = mapiClient();
5900
+ const client = getMapiClient();
5805
5901
  const { data } = await client.datasources.list({
5806
5902
  path: {
5807
5903
  space_id: spaceId
@@ -5840,10 +5936,10 @@ const saveDatasourcesToFiles = async (space, datasources, options) => {
5840
5936
  }
5841
5937
  };
5842
5938
 
5843
- const program$5 = getProgram();
5939
+ const program$7 = getProgram();
5844
5940
  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) => {
5845
5941
  konsola.title(`${commands.DATASOURCES}`, colorPalette.DATASOURCES, datasourceName ? `Pulling datasource ${datasourceName}...` : "Pulling datasources...");
5846
- const verbose = program$5.opts().verbose;
5942
+ const verbose = program$7.opts().verbose;
5847
5943
  const { space, path } = datasourcesCommand.opts();
5848
5944
  const {
5849
5945
  separateFiles = false,
@@ -5852,8 +5948,7 @@ datasourcesCommand.command("pull [datasourceName]").option("-f, --filename <file
5852
5948
  } = options;
5853
5949
  const actualFilename = filename ?? DEFAULT_DATASOURCES_FILENAME;
5854
5950
  const datasourcesOutputDir = resolveCommandPath(directories.datasources, space, path);
5855
- const { state, initializeSession } = session();
5856
- await initializeSession();
5951
+ const { state } = session();
5857
5952
  if (!requireAuthentication(state, verbose)) {
5858
5953
  return;
5859
5954
  }
@@ -5861,13 +5956,6 @@ datasourcesCommand.command("pull [datasourceName]").option("-f, --filename <file
5861
5956
  handleError(new CommandError(`Please provide the space as argument --space YOUR_SPACE_ID.`), verbose);
5862
5957
  return;
5863
5958
  }
5864
- const { password, region } = state;
5865
- mapiClient({
5866
- token: {
5867
- accessToken: password
5868
- },
5869
- region
5870
- });
5871
5959
  const spinnerDatasources = new Spinner({
5872
5960
  verbose: !isVitest
5873
5961
  });
@@ -5921,15 +6009,14 @@ datasourcesCommand.command("pull [datasourceName]").option("-f, --filename <file
5921
6009
  }
5922
6010
  });
5923
6011
 
5924
- const program$4 = getProgram();
6012
+ const program$6 = getProgram();
5925
6013
  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) => {
5926
6014
  konsola.title(`${commands.DATASOURCES}`, colorPalette.DATASOURCES, datasourceName ? `Pushing datasource ${datasourceName}...` : "Pushing datasources...");
5927
- const verbose = program$4.opts().verbose;
6015
+ const verbose = program$6.opts().verbose;
5928
6016
  const { space, path } = datasourcesCommand.opts();
5929
6017
  const { filter } = options;
5930
6018
  const fromSpace = options.from || space;
5931
- const { state, initializeSession } = session();
5932
- await initializeSession();
6019
+ const { state } = session();
5933
6020
  if (!requireAuthentication(state, verbose)) {
5934
6021
  return;
5935
6022
  }
@@ -5939,13 +6026,6 @@ datasourcesCommand.command("push [datasourceName]").description(`Push your space
5939
6026
  }
5940
6027
  konsola.info(`Attempting to push datasources ${chalk.bold("from")} space ${chalk.hex(colorPalette.DATASOURCES)(fromSpace)} ${chalk.bold("to")} ${chalk.hex(colorPalette.DATASOURCES)(space)}`);
5941
6028
  konsola.br();
5942
- const { password, region } = state;
5943
- mapiClient({
5944
- token: {
5945
- accessToken: password
5946
- },
5947
- region
5948
- });
5949
6029
  try {
5950
6030
  const spaceState = {
5951
6031
  local: await readDatasourcesFiles({
@@ -6032,7 +6112,7 @@ datasourcesCommand.command("push [datasourceName]").description(`Push your space
6032
6112
 
6033
6113
  async function deleteDatasource(spaceId, id) {
6034
6114
  try {
6035
- const client = mapiClient();
6115
+ const client = getMapiClient();
6036
6116
  await client.datasources.delete({
6037
6117
  path: {
6038
6118
  space_id: spaceId,
@@ -6058,8 +6138,7 @@ datasourcesCommand.command("delete [name]").description("Delete a datasource fro
6058
6138
  }
6059
6139
  const { space } = datasourcesCommand.opts();
6060
6140
  const verbose = datasourcesCommand.parent?.opts().verbose;
6061
- const { state, initializeSession } = session();
6062
- await initializeSession();
6141
+ const { state } = session();
6063
6142
  if (!requireAuthentication(state, verbose)) {
6064
6143
  return;
6065
6144
  }
@@ -6067,13 +6146,6 @@ datasourcesCommand.command("delete [name]").description("Delete a datasource fro
6067
6146
  handleError(new CommandError("Please provide the space as argument --space YOUR_SPACE_ID."), verbose);
6068
6147
  return;
6069
6148
  }
6070
- const { password, region } = state;
6071
- mapiClient({
6072
- token: {
6073
- accessToken: password
6074
- },
6075
- region
6076
- });
6077
6149
  const spinner = new Spinner({
6078
6150
  verbose: !isVitest
6079
6151
  });
@@ -6300,13 +6372,13 @@ async function promptForLogin(verbose) {
6300
6372
  return null;
6301
6373
  }
6302
6374
  }
6303
- const program$3 = getProgram();
6304
- program$3.command(`${commands.CREATE} [project-path]`).alias("c").description(`Scaffold a new project using Storyblok`).option("-t, --template <template>", "technology starter template").option("-b, --blueprint <blueprint>", "[DEPRECATED] use --template instead").option("--skip-space", "skip space creation").option("--token <token>", "Storyblok access token (skip space creation and use this token)").option(
6375
+ const program$5 = getProgram();
6376
+ program$5.command(`${commands.CREATE} [project-path]`).alias("c").description(`Scaffold a new project using Storyblok`).option("-t, --template <template>", "technology starter template").option("-b, --blueprint <blueprint>", "[DEPRECATED] use --template instead").option("--skip-space", "skip space creation").option("--token <token>", "Storyblok access token (skip space creation and use this token)").option(
6305
6377
  "-r, --region <region>",
6306
6378
  `The region to apply to the generated project template (does not affect space creation).`
6307
6379
  ).action(async (projectPath, options) => {
6308
6380
  konsola.title(`${commands.CREATE}`, colorPalette.CREATE);
6309
- const verbose = program$3.opts().verbose;
6381
+ const verbose = program$5.opts().verbose;
6310
6382
  const { template, blueprint, token } = options;
6311
6383
  if (options.region && !isRegion(options.region)) {
6312
6384
  handleError(new CommandError(`The provided region: ${options.region} is not valid. Please use one of the following values: ${Object.values(regions).join(" | ")}`));
@@ -6320,7 +6392,6 @@ program$3.command(`${commands.CREATE} [project-path]`).alias("c").description(`S
6320
6392
  konsola.warn(`Both --blueprint and --template provided. Using --template and ignoring --blueprint.`);
6321
6393
  }
6322
6394
  const { state, initializeSession } = session();
6323
- await initializeSession();
6324
6395
  let password;
6325
6396
  let region;
6326
6397
  if (state.region) {
@@ -6341,12 +6412,6 @@ program$3.command(`${commands.CREATE} [project-path]`).alias("c").description(`S
6341
6412
  handleError(new CommandError(`Cannot create space in region "${options.region}". Your account is configured for region "${region}". Space creation must use your account's region.`));
6342
6413
  return;
6343
6414
  }
6344
- mapiClient({
6345
- token: {
6346
- accessToken: password
6347
- },
6348
- region
6349
- });
6350
6415
  } else if (state.isLoggedIn && state.password) {
6351
6416
  password = state.password;
6352
6417
  if (state.region) {
@@ -6532,13 +6597,13 @@ program$3.command(`${commands.CREATE} [project-path]`).alias("c").description(`S
6532
6597
  konsola.br();
6533
6598
  });
6534
6599
 
6535
- const program$2 = getProgram();
6536
- const logsCommand = program$2.command(commands.LOGS).alias("lg").description(`Inspect and manage logs.`).option("-s, --space <space>", "The space ID.").option("-p, --path <path>", "Path to the directory containing the logs directory. Defaults to '.storyblok'.");
6600
+ const program$4 = getProgram();
6601
+ const logsCommand = program$4.command(commands.LOGS).alias("lg").description(`Inspect and manage logs.`).option("-s, --space <space>", "The space ID.").option("-p, --path <path>", "Path to the directory containing the logs directory. Defaults to '.storyblok'.");
6537
6602
 
6538
6603
  logsCommand.command("list").description("List logs").action(async () => {
6539
6604
  const { space, path } = logsCommand.opts();
6540
6605
  const ui = getUI();
6541
- const logsPath = resolveCommandPath(directories.log, space, path);
6606
+ const logsPath = resolveCommandPath(directories.logs, space, path);
6542
6607
  const logFiles = FileTransport.listLogFiles(logsPath);
6543
6608
  if (logFiles.length === 0) {
6544
6609
  ui.info(`No logs found for space "${space}".`);
@@ -6551,18 +6616,18 @@ logsCommand.command("list").description("List logs").action(async () => {
6551
6616
  logsCommand.command("prune").description("Prune logs").option("--keep <number>", "Max number of log files to keep (default `0`, meaning remove all)", Number.parseInt, 0).action(async ({ keep }) => {
6552
6617
  const { space, path } = logsCommand.opts();
6553
6618
  const ui = getUI();
6554
- const logsPath = resolveCommandPath(directories.log, space, path);
6619
+ const logsPath = resolveCommandPath(directories.logs, space, path);
6555
6620
  const deletedFilesCount = FileTransport.pruneLogFiles(logsPath, keep);
6556
6621
  ui.info(`Deleted ${deletedFilesCount} log file${deletedFilesCount === 1 ? "" : "s"}`);
6557
6622
  });
6558
6623
 
6559
- const program$1 = getProgram();
6560
- const reportsCommand = program$1.command(commands.REPORTS).alias("rp").description("Inspect and manage reports.").option("-s, --space <space>", "The space ID.").option("-p, --path <path>", "Path to the directory containing the reports directory. Defaults to '.storyblok'.");
6624
+ const program$3 = getProgram();
6625
+ const reportsCommand = program$3.command(commands.REPORTS).alias("rp").description("Inspect and manage reports.").option("-s, --space <space>", "The space ID.").option("-p, --path <path>", "Path to the directory containing the reports directory. Defaults to '.storyblok'.");
6561
6626
 
6562
6627
  reportsCommand.command("list").description("List reports").action(async () => {
6563
6628
  const { space, path } = reportsCommand.opts();
6564
6629
  const ui = getUI();
6565
- const reportsPath = resolveCommandPath(directories.report, space, path);
6630
+ const reportsPath = resolveCommandPath(directories.reports, space, path);
6566
6631
  const reportFiles = Reporter.listReportFiles(reportsPath, ".jsonl");
6567
6632
  if (reportFiles.length === 0) {
6568
6633
  ui.info(`No reports found for space "${space}".`);
@@ -6575,11 +6640,2218 @@ reportsCommand.command("list").description("List reports").action(async () => {
6575
6640
  reportsCommand.command("prune").description("Prune reports").option("--keep <number>", "Max number of report files to keep (default `0`, meaning remove all)", Number.parseInt, 0).action(async ({ keep }) => {
6576
6641
  const { space, path } = reportsCommand.opts();
6577
6642
  const ui = getUI();
6578
- const reportsPath = resolveCommandPath(directories.report, space, path);
6643
+ const reportsPath = resolveCommandPath(directories.reports, space, path);
6579
6644
  const deletedFilesCount = Reporter.pruneReportFiles(reportsPath, keep, ".jsonl");
6580
6645
  ui.info(`Deleted ${deletedFilesCount} report file${deletedFilesCount === 1 ? "" : "s"}`);
6581
6646
  });
6582
6647
 
6648
+ const program$2 = getProgram();
6649
+ const assetsCommand = program$2.command(commands.ASSETS).description(`Manage your space's assets`).option("-s, --space <space>", "space ID").option("-p, --path <path>", "base path to store assets (default .storyblok)");
6650
+
6651
+ const fetchAssets = async ({ spaceId, params }) => {
6652
+ try {
6653
+ const client = getMapiClient();
6654
+ const { data, response } = await client.assets.list({
6655
+ path: {
6656
+ space_id: spaceId
6657
+ },
6658
+ query: {
6659
+ ...params,
6660
+ per_page: params?.per_page || 100,
6661
+ page: params?.page || 1
6662
+ },
6663
+ throwOnError: true
6664
+ });
6665
+ const assets = (data?.assets || []).filter((asset) => Boolean(asset?.id && asset?.filename));
6666
+ return {
6667
+ assets,
6668
+ headers: response.headers
6669
+ };
6670
+ } catch (maybeError) {
6671
+ handleAPIError("pull_assets", toError(maybeError));
6672
+ throw maybeError;
6673
+ }
6674
+ };
6675
+ const downloadFile = async (filename) => {
6676
+ const response = await fetch(filename);
6677
+ if (!response.ok) {
6678
+ throw new Error(`Failed to download ${filename}`);
6679
+ }
6680
+ return response.arrayBuffer();
6681
+ };
6682
+ const getSignedAssetUrl = async (filename, assetToken, region) => {
6683
+ try {
6684
+ const client = new Storyblok({
6685
+ accessToken: assetToken,
6686
+ region: region || "eu"
6687
+ });
6688
+ const response = await client.get("cdn/assets/me", {
6689
+ filename
6690
+ });
6691
+ return response.data.asset.signed_url;
6692
+ } catch (maybeError) {
6693
+ handleAPIError("pull_asset", toError(maybeError));
6694
+ throw maybeError;
6695
+ }
6696
+ };
6697
+ const fetchAssetFolders = async ({ spaceId }) => {
6698
+ try {
6699
+ const client = getMapiClient();
6700
+ const { data, response } = await client.assetFolders.list({
6701
+ path: {
6702
+ space_id: spaceId
6703
+ },
6704
+ throwOnError: true
6705
+ });
6706
+ return {
6707
+ asset_folders: data.asset_folders || [],
6708
+ headers: response.headers
6709
+ };
6710
+ } catch (maybeError) {
6711
+ handleAPIError("pull_asset_folders", toError(maybeError));
6712
+ throw maybeError;
6713
+ }
6714
+ };
6715
+ const createAssetFolder = async (folder, {
6716
+ spaceId
6717
+ }) => {
6718
+ try {
6719
+ const client = getMapiClient();
6720
+ const { data } = await client.assetFolders.create({
6721
+ path: {
6722
+ space_id: spaceId
6723
+ },
6724
+ body: { asset_folder: folder },
6725
+ throwOnError: true
6726
+ });
6727
+ const { asset_folder } = data;
6728
+ if (!asset_folder) {
6729
+ throw new Error("Failed to create asset folder");
6730
+ }
6731
+ return asset_folder;
6732
+ } catch (maybeError) {
6733
+ handleAPIError("push_asset_folder", toError(maybeError));
6734
+ throw maybeError;
6735
+ }
6736
+ };
6737
+ const updateAssetFolder = async (folder, {
6738
+ spaceId
6739
+ }) => {
6740
+ try {
6741
+ const client = getMapiClient();
6742
+ await client.assetFolders.update({
6743
+ path: {
6744
+ asset_folder_id: folder.id,
6745
+ space_id: spaceId
6746
+ },
6747
+ body: { asset_folder: folder },
6748
+ throwOnError: true
6749
+ });
6750
+ return folder;
6751
+ } catch (maybeError) {
6752
+ handleAPIError("push_asset_folder", toError(maybeError));
6753
+ throw maybeError;
6754
+ }
6755
+ };
6756
+ const requestAssetUpload = async (asset, { spaceId }) => {
6757
+ try {
6758
+ const client = getMapiClient();
6759
+ const { data } = await client.assets.upload({
6760
+ path: {
6761
+ space_id: spaceId
6762
+ },
6763
+ body: {
6764
+ // @ts-expect-error Our types are wrong, id is optional but allowed.
6765
+ id: asset.id,
6766
+ filename: asset.short_filename,
6767
+ asset_folder_id: asset.asset_folder_id ?? void 0,
6768
+ is_private: asset.is_private
6769
+ },
6770
+ throwOnError: true
6771
+ });
6772
+ const signedUpload = data;
6773
+ if (!signedUpload?.id || !signedUpload?.post_url || !signedUpload?.fields) {
6774
+ throw new Error("Failed to request signed upload!");
6775
+ }
6776
+ return signedUpload;
6777
+ } catch (maybeError) {
6778
+ handleAPIError("push_asset_sign", toError(maybeError));
6779
+ throw maybeError;
6780
+ }
6781
+ };
6782
+ const uploadAssetToS3 = async (asset, fileBuffer, {
6783
+ signedUpload
6784
+ }) => {
6785
+ if (!signedUpload?.id || !signedUpload?.post_url || !signedUpload?.fields) {
6786
+ throw new Error("Invalid signed upload!");
6787
+ }
6788
+ const formData = new FormData();
6789
+ for (const [key, value] of Object.entries(signedUpload.fields)) {
6790
+ formData.append(key, value);
6791
+ }
6792
+ const contentType = signedUpload.fields["Content-Type"] || "application/octet-stream";
6793
+ formData.append("file", new File([Buffer.from(fileBuffer)], asset.short_filename, { type: contentType }));
6794
+ const response = await fetch(signedUpload.post_url, {
6795
+ method: "POST",
6796
+ body: formData
6797
+ });
6798
+ if (!response.ok) {
6799
+ handleAPIError("push_asset_upload", new Error("Failed to upload asset to storage"));
6800
+ return;
6801
+ }
6802
+ return response;
6803
+ };
6804
+ const finishAssetUpload = async (assetId, {
6805
+ spaceId
6806
+ }) => {
6807
+ try {
6808
+ const client = getMapiClient();
6809
+ await client.assets.finalize({
6810
+ path: {
6811
+ space_id: spaceId,
6812
+ signed_response_object_id: String(assetId)
6813
+ },
6814
+ throwOnError: true
6815
+ });
6816
+ const { data } = await client.assets.get({
6817
+ path: {
6818
+ space_id: spaceId,
6819
+ asset_id: assetId
6820
+ },
6821
+ throwOnError: true
6822
+ });
6823
+ return data;
6824
+ } catch (maybeError) {
6825
+ handleAPIError("push_asset_finish", toError(maybeError));
6826
+ throw maybeError;
6827
+ }
6828
+ };
6829
+ const uploadAsset = async (asset, fileBuffer, { spaceId }) => {
6830
+ const signed = await requestAssetUpload(asset, {
6831
+ spaceId
6832
+ });
6833
+ const uploadResponse = await uploadAssetToS3(asset, fileBuffer, {
6834
+ signedUpload: signed
6835
+ });
6836
+ if (!uploadResponse?.ok) {
6837
+ throw new Error("Error uploading asset to S3!");
6838
+ }
6839
+ return finishAssetUpload(Number(signed.id), {
6840
+ spaceId
6841
+ });
6842
+ };
6843
+ const sha256 = (data) => {
6844
+ const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
6845
+ return createHash("sha256").update(buffer).digest("hex");
6846
+ };
6847
+ const downloadAssetFile = async (asset, options) => {
6848
+ let url = asset.filename;
6849
+ if (asset.is_private) {
6850
+ if (!options.assetToken) {
6851
+ throw new Error(`Asset ${asset.filename} is private but no asset token was provided. Use --asset-token to provide a token.`);
6852
+ }
6853
+ url = await getSignedAssetUrl(asset.filename, options.assetToken, options.region);
6854
+ }
6855
+ return downloadFile(url);
6856
+ };
6857
+ const updateAsset = async (asset, fileBuffer, {
6858
+ spaceId
6859
+ }) => {
6860
+ try {
6861
+ const assetWithNewFilename = { ...asset };
6862
+ if (fileBuffer) {
6863
+ const uploadedAsset = await uploadAsset({
6864
+ id: asset.id,
6865
+ asset_folder_id: asset.asset_folder_id,
6866
+ short_filename: asset.short_filename || basename(asset.filename)
6867
+ }, fileBuffer, { spaceId });
6868
+ assetWithNewFilename.filename = uploadedAsset.filename;
6869
+ assetWithNewFilename.short_filename = uploadedAsset.short_filename;
6870
+ }
6871
+ const client = getMapiClient();
6872
+ await client.assets.update({
6873
+ path: {
6874
+ space_id: spaceId,
6875
+ asset_id: assetWithNewFilename.id
6876
+ },
6877
+ body: {
6878
+ asset: assetWithNewFilename
6879
+ },
6880
+ throwOnError: true
6881
+ });
6882
+ return assetWithNewFilename;
6883
+ } catch (maybeError) {
6884
+ handleAPIError("push_asset_update", toError(maybeError));
6885
+ throw maybeError;
6886
+ }
6887
+ };
6888
+ const createAsset = async (asset, fileBuffer, { spaceId }) => {
6889
+ const createdAsset = await uploadAsset({
6890
+ asset_folder_id: asset.asset_folder_id,
6891
+ short_filename: asset.short_filename,
6892
+ alt: asset.alt,
6893
+ title: asset.title,
6894
+ copyright: asset.copyright,
6895
+ source: asset.source,
6896
+ is_private: asset.is_private
6897
+ }, fileBuffer, { spaceId });
6898
+ const hasUpdatableMetadata = Boolean(
6899
+ asset.alt || asset.title || asset.copyright || asset.source || asset.is_private || asset.meta_data && Object.keys(asset.meta_data).length > 0
6900
+ );
6901
+ if (hasUpdatableMetadata) {
6902
+ const updatedAsset = await updateAsset({
6903
+ ...asset,
6904
+ id: createdAsset.id,
6905
+ filename: createdAsset.filename
6906
+ }, null, {
6907
+ spaceId
6908
+ });
6909
+ if (!updatedAsset) {
6910
+ throw new Error("Updating the created asset failed!");
6911
+ }
6912
+ return updatedAsset;
6913
+ }
6914
+ return createdAsset;
6915
+ };
6916
+
6917
+ const parseAssetData = (raw) => {
6918
+ if (!raw) {
6919
+ return {};
6920
+ }
6921
+ try {
6922
+ const parsed = JSON.parse(raw);
6923
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
6924
+ throw new Error("Asset data must be a JSON object.");
6925
+ }
6926
+ return parsed;
6927
+ } catch (maybeError) {
6928
+ throw new Error(`Invalid --data JSON: ${toError(maybeError).message}`);
6929
+ }
6930
+ };
6931
+ const getSidecarFilename = (assetBinaryPath) => {
6932
+ return join(dirname$1(assetBinaryPath), `${basename(assetBinaryPath, extname(assetBinaryPath))}.json`);
6933
+ };
6934
+ const loadSidecarAssetData = async (assetBinaryPath) => {
6935
+ const sidecarPath = getSidecarFilename(assetBinaryPath);
6936
+ try {
6937
+ const sidecarRaw = await readFile$1(sidecarPath, "utf8");
6938
+ try {
6939
+ return parseAssetData(sidecarRaw);
6940
+ } catch (maybeError) {
6941
+ throw new Error(`Invalid sidecar JSON: ${toError(maybeError).message}`);
6942
+ }
6943
+ } catch (maybeError) {
6944
+ const error = toError(maybeError);
6945
+ if (error.code === "ENOENT") {
6946
+ return {};
6947
+ }
6948
+ throw new Error(`Failed to read sidecar asset data: ${error.message}`);
6949
+ }
6950
+ };
6951
+ const isRemoteSource = (assetBinaryPath) => {
6952
+ try {
6953
+ const url = new URL(assetBinaryPath);
6954
+ return url.protocol === "http:" || url.protocol === "https:";
6955
+ } catch {
6956
+ return false;
6957
+ }
6958
+ };
6959
+ const isValidManifestEntry = (entry) => Boolean(typeof entry.old_id === "number" && typeof entry.new_id === "number" && entry.old_filename && entry.new_filename);
6960
+ const loadAssetMap = async (manifestFile) => {
6961
+ const manifest = await loadManifest(manifestFile);
6962
+ return new Map([
6963
+ ...manifest.filter(isValidManifestEntry).map((e) => [
6964
+ Number(e.old_id),
6965
+ {
6966
+ old: { id: Number(e.old_id), filename: e.old_filename || "" },
6967
+ new: { id: Number(e.new_id), filename: e.new_filename || "" }
6968
+ }
6969
+ ])
6970
+ ]);
6971
+ };
6972
+ const loadAssetFolderMap = async (manifestFile) => {
6973
+ const manifest = await loadManifest(manifestFile);
6974
+ return new Map(manifest.map((e) => [Number(e.old_id), Number(e.new_id)]));
6975
+ };
6976
+ const getAssetNameAndExt = (asset) => {
6977
+ const filename = asset.short_filename || (asset.filename ? basename(asset.filename) : void 0);
6978
+ if (!filename) {
6979
+ throw new Error(`Filename for asset with id ${asset.id} could not be determined!`);
6980
+ }
6981
+ const ext = extname(filename);
6982
+ const name = sanitizeFilename(filename.replace(ext, ""));
6983
+ return { name, ext };
6984
+ };
6985
+ const getAssetFilename = (asset) => {
6986
+ const { name } = getAssetNameAndExt(asset);
6987
+ return `${name}_${asset.id}.json`;
6988
+ };
6989
+ const getAssetBinaryFilename = (asset) => {
6990
+ const { name, ext } = getAssetNameAndExt(asset);
6991
+ return `${name}_${asset.id}${ext}`;
6992
+ };
6993
+ const getFolderFilename = (folder) => {
6994
+ const sanitizedName = sanitizeFilename(folder.name || "");
6995
+ const baseName = sanitizedName || folder.uuid;
6996
+ return `${baseName}_${folder.uuid}.json`;
6997
+ };
6998
+
6999
+ const apiConcurrencyLock$1 = new Sema(12);
7000
+ const fetchAssetsStream = ({
7001
+ spaceId,
7002
+ params = {},
7003
+ setTotalAssets,
7004
+ setTotalPages,
7005
+ onIncrement,
7006
+ onPageSuccess,
7007
+ onPageError
7008
+ }) => {
7009
+ const listGenerator = async function* assetListIterator() {
7010
+ let perPage = 100;
7011
+ let page = 1;
7012
+ let totalPages = 1;
7013
+ setTotalPages?.(totalPages);
7014
+ while (page <= totalPages) {
7015
+ try {
7016
+ const result = await fetchAssets({
7017
+ spaceId,
7018
+ params: {
7019
+ ...params,
7020
+ per_page: perPage,
7021
+ page
7022
+ }
7023
+ });
7024
+ const { headers, assets } = result;
7025
+ const total = Number(headers.get("Total"));
7026
+ perPage = Number(headers.get("Per-Page")) || perPage;
7027
+ totalPages = Math.max(1, Math.ceil(total / perPage));
7028
+ setTotalAssets?.(total);
7029
+ setTotalPages?.(totalPages);
7030
+ onPageSuccess?.(page, totalPages);
7031
+ for (const asset of assets) {
7032
+ yield asset;
7033
+ }
7034
+ page += 1;
7035
+ } catch (maybeError) {
7036
+ onPageError?.(toError(maybeError), page, totalPages);
7037
+ break;
7038
+ } finally {
7039
+ onIncrement?.();
7040
+ }
7041
+ }
7042
+ };
7043
+ return Readable.from(listGenerator());
7044
+ };
7045
+ const downloadAssetStream = ({
7046
+ assetToken,
7047
+ region,
7048
+ onIncrement,
7049
+ onAssetSuccess,
7050
+ onAssetError
7051
+ }) => {
7052
+ const processing = /* @__PURE__ */ new Set();
7053
+ return new Transform({
7054
+ objectMode: true,
7055
+ async transform(asset, _encoding, callback) {
7056
+ await apiConcurrencyLock$1.acquire();
7057
+ const task = downloadAssetFile(asset, { assetToken, region }).then((fileBuffer) => {
7058
+ if (!fileBuffer) {
7059
+ throw new Error("Invalid asset file!");
7060
+ }
7061
+ onAssetSuccess?.(asset);
7062
+ this.push({ asset, fileBuffer });
7063
+ }).catch((maybeError) => {
7064
+ onAssetError?.(toError(maybeError), asset);
7065
+ }).finally(() => {
7066
+ onIncrement?.();
7067
+ apiConcurrencyLock$1.release();
7068
+ processing.delete(task);
7069
+ });
7070
+ processing.add(task);
7071
+ callback();
7072
+ },
7073
+ flush(callback) {
7074
+ Promise.allSettled(processing).finally(() => callback());
7075
+ }
7076
+ });
7077
+ };
7078
+ const makeWriteAssetFSTransport = ({ directoryPath }) => async (asset, fileBuffer) => {
7079
+ const assetBinaryPath = join(directoryPath, getAssetBinaryFilename(asset));
7080
+ const assetPath = join(directoryPath, getAssetFilename(asset));
7081
+ await saveToFile(assetBinaryPath, Buffer.from(fileBuffer));
7082
+ await saveToFile(assetPath, JSON.stringify(asset, null, 2));
7083
+ return asset;
7084
+ };
7085
+ const writeAssetStream = ({
7086
+ writeAsset,
7087
+ onIncrement,
7088
+ onAssetSuccess,
7089
+ onAssetError
7090
+ }) => {
7091
+ const processing = /* @__PURE__ */ new Set();
7092
+ return new Writable({
7093
+ objectMode: true,
7094
+ async write(payload, _encoding, callback) {
7095
+ await apiConcurrencyLock$1.acquire();
7096
+ const task = (async () => {
7097
+ try {
7098
+ await writeAsset(payload.asset, payload.fileBuffer);
7099
+ onAssetSuccess?.(payload.asset);
7100
+ } catch (maybeError) {
7101
+ onAssetError?.(toError(maybeError), payload.asset);
7102
+ }
7103
+ })();
7104
+ processing.add(task);
7105
+ task.finally(() => {
7106
+ onIncrement?.();
7107
+ apiConcurrencyLock$1.release();
7108
+ processing.delete(task);
7109
+ });
7110
+ callback();
7111
+ },
7112
+ final(callback) {
7113
+ Promise.all(processing).finally(() => callback());
7114
+ }
7115
+ });
7116
+ };
7117
+ const fetchAssetFoldersStream = ({
7118
+ spaceId,
7119
+ setTotalFolders,
7120
+ onSuccess,
7121
+ onError
7122
+ }) => {
7123
+ const listGenerator = async function* folderListIterator() {
7124
+ try {
7125
+ const result = await fetchAssetFolders({ spaceId });
7126
+ const { asset_folders } = result;
7127
+ const total = asset_folders.length;
7128
+ setTotalFolders?.(total);
7129
+ onSuccess?.(asset_folders);
7130
+ for (const folder of asset_folders) {
7131
+ yield folder;
7132
+ }
7133
+ } catch (maybeError) {
7134
+ onError?.(toError(maybeError));
7135
+ }
7136
+ };
7137
+ return Readable.from(listGenerator());
7138
+ };
7139
+ const makeWriteAssetFolderFSTransport = ({ directoryPath }) => async (folder) => {
7140
+ const filename = getFolderFilename(folder);
7141
+ await saveToFile(join(directoryPath, "folders", filename), JSON.stringify(folder, null, 2));
7142
+ return folder;
7143
+ };
7144
+ const writeAssetFolderStream = ({
7145
+ writeAssetFolder,
7146
+ onIncrement,
7147
+ onFolderSuccess,
7148
+ onFolderError
7149
+ }) => {
7150
+ const processing = /* @__PURE__ */ new Set();
7151
+ return new Writable({
7152
+ objectMode: true,
7153
+ async write(folder, _encoding, callback) {
7154
+ await apiConcurrencyLock$1.acquire();
7155
+ const task = (async () => {
7156
+ try {
7157
+ await writeAssetFolder(folder);
7158
+ onFolderSuccess?.(folder);
7159
+ } catch (maybeError) {
7160
+ onFolderError?.(toError(maybeError), folder);
7161
+ }
7162
+ })();
7163
+ processing.add(task);
7164
+ task.finally(() => {
7165
+ onIncrement?.();
7166
+ apiConcurrencyLock$1.release();
7167
+ processing.delete(task);
7168
+ });
7169
+ callback();
7170
+ },
7171
+ final(callback) {
7172
+ Promise.all(processing).finally(() => callback());
7173
+ }
7174
+ });
7175
+ };
7176
+ const readLocalAssetFoldersStream = ({
7177
+ directoryPath,
7178
+ setTotalFolders,
7179
+ onFolderError
7180
+ }) => {
7181
+ const iterator = async function* readFolders() {
7182
+ try {
7183
+ const files = await readdir(directoryPath);
7184
+ const jsonFiles = new Set(files.filter((file) => file.endsWith(".json")));
7185
+ setTotalFolders?.(jsonFiles.size);
7186
+ const processed = /* @__PURE__ */ new Set();
7187
+ let maxIterations = jsonFiles.size * jsonFiles.size;
7188
+ while (jsonFiles.size > 0 && maxIterations-- > 0) {
7189
+ for (const file of jsonFiles) {
7190
+ try {
7191
+ const filePath = join(directoryPath, file);
7192
+ const content = await readFile$1(filePath, "utf8");
7193
+ const folder = JSON.parse(content);
7194
+ jsonFiles.delete(file);
7195
+ if (!folder.parent_id || processed.has(folder.parent_id)) {
7196
+ processed.add(folder.id);
7197
+ yield {
7198
+ folder,
7199
+ context: {
7200
+ localFilePath: filePath
7201
+ }
7202
+ };
7203
+ } else {
7204
+ jsonFiles.add(file);
7205
+ }
7206
+ } catch (maybeError) {
7207
+ onFolderError?.(toError(maybeError));
7208
+ }
7209
+ }
7210
+ }
7211
+ if (jsonFiles.size > 0) {
7212
+ onFolderError?.(new Error(`Unable to resolve folder dependencies for: ${[...jsonFiles].join(", ")}`));
7213
+ }
7214
+ } catch (maybeError) {
7215
+ const error = toError(maybeError);
7216
+ if ("code" in error && error.code === "ENOENT") {
7217
+ return;
7218
+ }
7219
+ onFolderError?.(error);
7220
+ }
7221
+ };
7222
+ return Readable.from(iterator());
7223
+ };
7224
+ const makeCreateAssetFolderAPITransport = ({ spaceId }) => (folder) => createAssetFolder({
7225
+ name: folder.name,
7226
+ parent_id: folder.parent_id ?? void 0
7227
+ }, {
7228
+ spaceId
7229
+ });
7230
+ const makeUpdateAssetFolderAPITransport = ({ spaceId }) => (folder) => updateAssetFolder(folder, { spaceId });
7231
+ const makeGetAssetFolderAPITransport = ({ spaceId }) => async (folderId) => {
7232
+ const { data, response } = await getMapiClient().assetFolders.get({
7233
+ path: {
7234
+ asset_folder_id: folderId,
7235
+ space_id: spaceId
7236
+ }
7237
+ });
7238
+ if (!response.ok && response.status !== 404) {
7239
+ handleAPIError("pull_asset_folder", new FetchError(response.statusText, response));
7240
+ }
7241
+ return data?.asset_folder;
7242
+ };
7243
+ const makeCleanupAssetFolderFSTransport = () => async ({ localFilePath }) => await unlink(localFilePath);
7244
+ const upsertAssetFolderStream = ({
7245
+ transports,
7246
+ maps,
7247
+ onIncrement,
7248
+ onFolderSuccess,
7249
+ onFolderError
7250
+ }) => {
7251
+ return new Writable({
7252
+ objectMode: true,
7253
+ async write({ folder, context }, _encoding, callback) {
7254
+ try {
7255
+ const remoteParentId = folder.parent_id && (maps.assetFolders.get(folder.parent_id) || folder.parent_id);
7256
+ const remoteFolderId = maps.assetFolders.get(folder.id) || folder.id;
7257
+ const upsertFolder = {
7258
+ ...folder,
7259
+ id: remoteFolderId,
7260
+ parent_id: remoteParentId
7261
+ };
7262
+ const existingRemoteFolder = await transports.getAssetFolder(remoteFolderId);
7263
+ const newRemoteFolder = existingRemoteFolder ? await transports.updateAssetFolder({ ...upsertFolder, parent_id: remoteParentId !== null ? remoteParentId : void 0 }) : await transports.createAssetFolder(upsertFolder);
7264
+ if (!maps.assetFolders.get(folder.id)) {
7265
+ await transports.appendAssetFolderManifest(folder, newRemoteFolder);
7266
+ }
7267
+ await transports.cleanupAssetFolder?.({ localFilePath: context.localFilePath });
7268
+ onFolderSuccess?.(folder, newRemoteFolder);
7269
+ } catch (maybeError) {
7270
+ onFolderError?.(toError(maybeError), folder);
7271
+ } finally {
7272
+ onIncrement?.();
7273
+ callback();
7274
+ }
7275
+ }
7276
+ });
7277
+ };
7278
+ const readLocalAssetsStream = ({
7279
+ directoryPath,
7280
+ setTotalAssets,
7281
+ onAssetError
7282
+ }) => {
7283
+ const iterator = async function* readAssets() {
7284
+ try {
7285
+ const files = await readdir(directoryPath);
7286
+ const metadataFiles = files.filter((file) => file.endsWith(".json") && file !== "manifest.jsonl");
7287
+ setTotalAssets?.(metadataFiles.length);
7288
+ for (const file of metadataFiles) {
7289
+ const filePath = join(directoryPath, file);
7290
+ try {
7291
+ const statResult = await stat(filePath);
7292
+ if (!statResult.isFile()) {
7293
+ continue;
7294
+ }
7295
+ const metadataContent = await readFile$1(filePath, "utf8");
7296
+ const assetRaw = JSON.parse(metadataContent);
7297
+ const asset = {
7298
+ ...assetRaw,
7299
+ short_filename: assetRaw.short_filename || basename(assetRaw.filename)
7300
+ };
7301
+ const baseName = parse(file).name;
7302
+ const extFromMetadata = extname(asset.short_filename || asset.filename) || "";
7303
+ const assetBinaryPath = join(directoryPath, `${baseName}${extFromMetadata}`);
7304
+ const fileBuffer = await readFile$1(assetBinaryPath);
7305
+ yield {
7306
+ asset,
7307
+ context: {
7308
+ fileBuffer,
7309
+ assetBinaryPath,
7310
+ assetPath: filePath
7311
+ }
7312
+ };
7313
+ } catch (maybeError) {
7314
+ onAssetError?.(toError(maybeError));
7315
+ }
7316
+ }
7317
+ } catch (maybeError) {
7318
+ onAssetError?.(toError(maybeError));
7319
+ }
7320
+ };
7321
+ return Readable.from(iterator());
7322
+ };
7323
+ const readSingleAssetStream = ({
7324
+ asset,
7325
+ assetBinaryPath,
7326
+ onAssetError
7327
+ }) => {
7328
+ const iterator = async function* readSingleAsset() {
7329
+ try {
7330
+ if (!isRemoteSource(assetBinaryPath) && !await fileExists(assetBinaryPath)) {
7331
+ throw new Error("Asset path must point to a file.");
7332
+ }
7333
+ const fileBuffer = isRemoteSource(assetBinaryPath) ? await downloadFile(assetBinaryPath) : await readFile$1(assetBinaryPath);
7334
+ yield {
7335
+ asset,
7336
+ context: {
7337
+ fileBuffer,
7338
+ assetBinaryPath
7339
+ }
7340
+ };
7341
+ } catch (maybeError) {
7342
+ onAssetError?.(toError(maybeError));
7343
+ }
7344
+ };
7345
+ return Readable.from(iterator());
7346
+ };
7347
+ const makeCreateAssetAPITransport = ({ spaceId }) => (asset, fileBuffer) => createAsset(asset, fileBuffer, { spaceId });
7348
+ const makeUpdateAssetAPITransport = ({
7349
+ spaceId
7350
+ }) => (asset, fileBuffer) => updateAsset(asset, fileBuffer, {
7351
+ spaceId
7352
+ });
7353
+ const makeAppendAssetManifestFSTransport = ({ manifestFile }) => async (localAsset, remoteAsset) => {
7354
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
7355
+ await appendToFile(manifestFile, JSON.stringify({
7356
+ old_id: localAsset.id,
7357
+ new_id: remoteAsset.id,
7358
+ old_filename: localAsset.filename,
7359
+ new_filename: remoteAsset.filename,
7360
+ created_at: createdAt
7361
+ }));
7362
+ };
7363
+ const makeAppendAssetFolderManifestFSTransport = ({ manifestFile }) => async (localFolder, remoteFolder) => {
7364
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
7365
+ await appendToFile(manifestFile, JSON.stringify({
7366
+ old_id: localFolder.id,
7367
+ new_id: remoteFolder.id,
7368
+ created_at: createdAt
7369
+ }));
7370
+ };
7371
+ const makeGetAssetAPITransport = ({ spaceId }) => async (assetId) => {
7372
+ const { data, response } = await getMapiClient().assets.get({
7373
+ path: {
7374
+ space_id: spaceId,
7375
+ asset_id: assetId
7376
+ }
7377
+ });
7378
+ if (!response.ok && response.status !== 404) {
7379
+ handleAPIError("pull_asset", new FetchError(response.statusText, response));
7380
+ }
7381
+ if (data?.deleted_at) {
7382
+ return void 0;
7383
+ }
7384
+ return data;
7385
+ };
7386
+ const saveDelete = async (filePath) => {
7387
+ if (await fileExists(filePath)) {
7388
+ await unlink(filePath);
7389
+ }
7390
+ };
7391
+ const makeCleanupAssetFSTransport = () => async ({ assetBinaryPath, assetPath }) => {
7392
+ const assetOrSidecarPath = assetPath || getSidecarFilename(assetBinaryPath);
7393
+ await Promise.all([
7394
+ assetBinaryPath && saveDelete(assetBinaryPath),
7395
+ assetOrSidecarPath && saveDelete(assetOrSidecarPath)
7396
+ ]);
7397
+ };
7398
+ const hasId = (a) => {
7399
+ return !!a && typeof a === "object" && "id" in a && typeof a.id === "number";
7400
+ };
7401
+ const hasShortFilename = (a) => {
7402
+ return !!a && typeof a === "object" && "short_filename" in a && typeof a.short_filename === "string";
7403
+ };
7404
+ const isDataUnchanged = (localAsset, remoteAsset) => {
7405
+ if (localAsset.asset_folder_id !== remoteAsset.asset_folder_id) {
7406
+ return false;
7407
+ }
7408
+ if (localAsset.alt !== remoteAsset.alt || localAsset.title !== remoteAsset.title || localAsset.copyright !== remoteAsset.copyright || localAsset.source !== remoteAsset.source || localAsset.is_private !== remoteAsset.is_private) {
7409
+ return false;
7410
+ }
7411
+ const localAssetMetadataEntries = Object.entries(localAsset.meta_data || {});
7412
+ const remoteAssetMetadataEntries = Object.entries(remoteAsset.meta_data || {});
7413
+ if (localAssetMetadataEntries.length !== remoteAssetMetadataEntries.length) {
7414
+ return false;
7415
+ }
7416
+ const hasChanges = localAssetMetadataEntries.some(([k, v]) => remoteAsset.meta_data && remoteAsset.meta_data[k] !== v);
7417
+ return !hasChanges;
7418
+ };
7419
+ const isAssetUnchanged = async (localAsset, remoteAsset, localFileBuffer, downloadAssetFileTransport) => {
7420
+ const remoteFileBuffer = await downloadAssetFileTransport(remoteAsset);
7421
+ const isFileUnchanged = sha256(localFileBuffer) === sha256(remoteFileBuffer);
7422
+ if (!isFileUnchanged) {
7423
+ return false;
7424
+ }
7425
+ return isDataUnchanged(localAsset, remoteAsset);
7426
+ };
7427
+ const makeDownloadAssetFileTransport = ({
7428
+ assetToken,
7429
+ region
7430
+ }) => (asset) => downloadAssetFile(asset, { assetToken, region });
7431
+ const processAsset = async ({
7432
+ localAsset,
7433
+ fileBuffer,
7434
+ assetBinaryPath,
7435
+ assetPath,
7436
+ transports,
7437
+ maps
7438
+ }) => {
7439
+ const remoteFolderId = localAsset.asset_folder_id && (maps.assetFolders.get(localAsset.asset_folder_id) || localAsset.asset_folder_id);
7440
+ const remoteAssetId = hasId(localAsset) ? maps.assets.get(localAsset.id)?.new.id || localAsset.id : void 0;
7441
+ const remoteAsset = remoteAssetId ? await transports.getAsset(remoteAssetId) : null;
7442
+ let newRemoteAsset;
7443
+ let status;
7444
+ if (remoteAsset) {
7445
+ const updatePayload = {
7446
+ ...remoteAsset,
7447
+ ...localAsset,
7448
+ id: remoteAsset.id,
7449
+ asset_folder_id: remoteFolderId
7450
+ };
7451
+ const canSkip = await isAssetUnchanged(
7452
+ updatePayload,
7453
+ remoteAsset,
7454
+ fileBuffer,
7455
+ transports.downloadAssetFile
7456
+ );
7457
+ if (canSkip) {
7458
+ newRemoteAsset = remoteAsset;
7459
+ status = "skipped";
7460
+ } else {
7461
+ newRemoteAsset = await transports.updateAsset(updatePayload, fileBuffer);
7462
+ status = "updated";
7463
+ }
7464
+ } else if (hasShortFilename(localAsset)) {
7465
+ const createPayload = {
7466
+ ...localAsset,
7467
+ asset_folder_id: remoteFolderId
7468
+ };
7469
+ newRemoteAsset = await transports.createAsset(createPayload, fileBuffer);
7470
+ status = "created";
7471
+ } else {
7472
+ throw new Error("Could neither create nor update the asset: Missing ID and Filename");
7473
+ }
7474
+ if (hasId(localAsset)) {
7475
+ await transports.appendAssetManifest(localAsset, newRemoteAsset);
7476
+ }
7477
+ await transports.cleanupAsset?.({ assetBinaryPath, assetPath });
7478
+ return { status, remoteAsset: newRemoteAsset };
7479
+ };
7480
+ const upsertAssetStream = ({
7481
+ transports,
7482
+ maps,
7483
+ onIncrement,
7484
+ onAssetSuccess,
7485
+ onAssetSkipped,
7486
+ onAssetError
7487
+ }) => {
7488
+ const processing = /* @__PURE__ */ new Set();
7489
+ return new Writable({
7490
+ objectMode: true,
7491
+ async write({ asset: localAsset, context }, _encoding, callback) {
7492
+ await apiConcurrencyLock$1.acquire();
7493
+ const task = (async () => {
7494
+ try {
7495
+ const { status, remoteAsset } = await processAsset({
7496
+ localAsset,
7497
+ fileBuffer: context.fileBuffer,
7498
+ assetBinaryPath: context.assetBinaryPath,
7499
+ assetPath: context.assetPath,
7500
+ transports,
7501
+ maps
7502
+ });
7503
+ if (status === "skipped") {
7504
+ onAssetSkipped?.(localAsset, remoteAsset);
7505
+ } else {
7506
+ onAssetSuccess?.(localAsset, remoteAsset);
7507
+ }
7508
+ } catch (maybeError) {
7509
+ onAssetError?.(toError(maybeError), localAsset);
7510
+ }
7511
+ })();
7512
+ processing.add(task);
7513
+ task.finally(() => {
7514
+ onIncrement?.();
7515
+ apiConcurrencyLock$1.release();
7516
+ processing.delete(task);
7517
+ });
7518
+ callback();
7519
+ },
7520
+ final(callback) {
7521
+ Promise.all(processing).finally(() => callback());
7522
+ }
7523
+ });
7524
+ };
7525
+
7526
+ assetsCommand.command("pull").option("-d, --dry-run", "Preview changes without applying them to Storyblok").option("-q, --query <query>", 'Filter assets using Storyblok filter query syntax. Example: --query="search=my-file.jpg&with_tags=tag1,tag2"').option("--asset-token <token>", "Asset token for accessing private assets").description(`Download your space's assets as local files.`).action(async (options, command) => {
7527
+ const ui = getUI();
7528
+ const logger = getLogger();
7529
+ ui.title(`${commands.ASSETS}`, colorPalette.ASSETS, "Pulling assets...");
7530
+ logger.info("Pulling assets started");
7531
+ if (options.dryRun) {
7532
+ ui.warn(`DRY RUN MODE ENABLED: No changes will be made.
7533
+ `);
7534
+ logger.warn("Dry run mode enabled");
7535
+ }
7536
+ const { space, path: basePath, verbose } = command.optsWithGlobals();
7537
+ const assetToken = options.assetToken;
7538
+ const { state } = session();
7539
+ if (!requireAuthentication(state, verbose)) {
7540
+ process.exitCode = 2;
7541
+ return;
7542
+ }
7543
+ if (!space) {
7544
+ handleError(new CommandError(`Please provide the space as argument --space YOUR_SPACE_ID.`), verbose);
7545
+ process.exitCode = 2;
7546
+ return;
7547
+ }
7548
+ const { region } = state;
7549
+ const summary = {
7550
+ folderResults: { total: 0, succeeded: 0, failed: 0 },
7551
+ fetchAssetPages: { total: 0, succeeded: 0, failed: 0 },
7552
+ fetchAssets: { total: 0, succeeded: 0, failed: 0 },
7553
+ save: { total: 0, succeeded: 0, failed: 0 }
7554
+ };
7555
+ let fatalError = false;
7556
+ try {
7557
+ const folderProgress = ui.createProgressBar({ title: "Folders...".padEnd(25) });
7558
+ const fetchAssetPagesProgress = ui.createProgressBar({ title: "Fetching Asset Pages...".padEnd(24) });
7559
+ const fetchAssetsProgress = ui.createProgressBar({ title: "Fetching Assets...".padEnd(24) });
7560
+ const saveProgress = ui.createProgressBar({ title: "Saving Assets...".padEnd(24) });
7561
+ await pipeline$1(
7562
+ fetchAssetFoldersStream({
7563
+ spaceId: space,
7564
+ setTotalFolders: (total) => {
7565
+ summary.folderResults.total = total;
7566
+ folderProgress.setTotal(total);
7567
+ },
7568
+ onSuccess: () => {
7569
+ logger.info("Fetched asset folders");
7570
+ },
7571
+ onError: (error) => {
7572
+ summary.folderResults.failed += 1;
7573
+ summary.folderResults.total = summary.folderResults.total || 1;
7574
+ folderProgress.setTotal(summary.folderResults.total);
7575
+ logOnlyError(error);
7576
+ }
7577
+ }),
7578
+ writeAssetFolderStream({
7579
+ writeAssetFolder: options.dryRun ? async (folder) => folder : makeWriteAssetFolderFSTransport({
7580
+ directoryPath: resolveCommandPath(directories.assets, space, basePath)
7581
+ }),
7582
+ onIncrement: () => {
7583
+ folderProgress.increment();
7584
+ },
7585
+ onFolderSuccess: (folder) => {
7586
+ logger.info("Saved folder", { folderId: folder.id });
7587
+ summary.folderResults.succeeded += 1;
7588
+ },
7589
+ onFolderError: (error, folder) => {
7590
+ summary.folderResults.failed += 1;
7591
+ summary.folderResults.total = Math.max(summary.folderResults.total, summary.folderResults.succeeded + summary.folderResults.failed);
7592
+ logOnlyError(error, { folderId: folder.id });
7593
+ }
7594
+ })
7595
+ );
7596
+ await pipeline$1(
7597
+ fetchAssetsStream({
7598
+ spaceId: space,
7599
+ params: options.query ? Object.fromEntries(new URLSearchParams(options.query)) : {},
7600
+ setTotalAssets: (total) => {
7601
+ summary.fetchAssets.total = total;
7602
+ summary.save.total = total;
7603
+ fetchAssetsProgress.setTotal(total);
7604
+ saveProgress.setTotal(total);
7605
+ },
7606
+ setTotalPages: (totalPages) => {
7607
+ summary.fetchAssetPages.total = totalPages;
7608
+ fetchAssetPagesProgress.setTotal(totalPages);
7609
+ },
7610
+ onIncrement: () => {
7611
+ fetchAssetPagesProgress.increment();
7612
+ },
7613
+ onPageSuccess: (page, totalPages) => {
7614
+ logger.info(`Fetched assets page ${page} of ${totalPages}`);
7615
+ summary.fetchAssetPages.succeeded += 1;
7616
+ },
7617
+ onPageError: (error, page, totalPages) => {
7618
+ summary.fetchAssetPages.failed += 1;
7619
+ logOnlyError(error, { page, totalPages });
7620
+ }
7621
+ }),
7622
+ downloadAssetStream({
7623
+ assetToken,
7624
+ region,
7625
+ onIncrement: () => {
7626
+ fetchAssetsProgress.increment();
7627
+ },
7628
+ onAssetSuccess: (asset) => {
7629
+ logger.info("Fetched asset", { assetId: asset.id });
7630
+ summary.fetchAssets.succeeded += 1;
7631
+ },
7632
+ onAssetError: (error, asset) => {
7633
+ summary.fetchAssets.failed += 1;
7634
+ summary.save.total -= 1;
7635
+ saveProgress.setTotal(summary.save.total);
7636
+ logOnlyError(error, { assetId: asset.id });
7637
+ }
7638
+ }),
7639
+ writeAssetStream({
7640
+ writeAsset: options.dryRun ? async (asset) => asset : makeWriteAssetFSTransport({
7641
+ directoryPath: resolveCommandPath(directories.assets, space, basePath)
7642
+ }),
7643
+ onIncrement: () => {
7644
+ saveProgress.increment();
7645
+ },
7646
+ onAssetSuccess: (asset) => {
7647
+ logger.info("Saved asset", { assetId: asset.id });
7648
+ summary.save.succeeded += 1;
7649
+ },
7650
+ onAssetError: (error, asset) => {
7651
+ summary.save.failed += 1;
7652
+ logOnlyError(error, { assetId: asset.id });
7653
+ }
7654
+ })
7655
+ );
7656
+ } catch (maybeError) {
7657
+ fatalError = true;
7658
+ handleError(toError(maybeError));
7659
+ } finally {
7660
+ logger.info("Pulling assets finished", summary);
7661
+ ui.stopAllProgressBars();
7662
+ const failedAssets = Math.max(summary.fetchAssets.failed, summary.save.failed);
7663
+ const folderSummary = {
7664
+ total: summary.folderResults.total,
7665
+ succeeded: summary.folderResults.succeeded,
7666
+ failed: summary.folderResults.failed
7667
+ };
7668
+ ui.info(`Pull results: ${summary.save.total} assets pulled, ${failedAssets} assets failed`);
7669
+ ui.list([
7670
+ `Folders: ${folderSummary.succeeded}/${folderSummary.total} succeeded, ${folderSummary.failed} failed.`,
7671
+ `Fetching pages: ${summary.fetchAssetPages.succeeded}/${summary.fetchAssetPages.total} succeeded, ${summary.fetchAssetPages.failed} failed.`,
7672
+ `Fetching assets: ${summary.fetchAssets.succeeded}/${summary.fetchAssets.total} succeeded, ${summary.fetchAssets.failed} failed.`,
7673
+ `Saving assets: ${summary.save.succeeded}/${summary.save.total} succeeded, ${summary.save.failed} failed.`
7674
+ ]);
7675
+ const reporter = getReporter();
7676
+ reporter.addSummary("folderResults", folderSummary);
7677
+ reporter.addSummary("fetchAssetPagesResults", summary.fetchAssetPages);
7678
+ reporter.addSummary("fetchAssetsResults", summary.fetchAssets);
7679
+ reporter.addSummary("saveResults", summary.save);
7680
+ reporter.finalize();
7681
+ const failedTotal = summary.folderResults.failed + summary.fetchAssetPages.failed + summary.fetchAssets.failed + summary.save.failed;
7682
+ process.exitCode = fatalError ? 2 : failedTotal > 0 ? 1 : 0;
7683
+ }
7684
+ });
7685
+
7686
+ const traverseAndMapBySchema = (data, {
7687
+ schemas,
7688
+ maps,
7689
+ fieldRefMappers: fieldRefMappers2,
7690
+ processedFields,
7691
+ missingSchemas
7692
+ }) => {
7693
+ const schema = schemas[data.component];
7694
+ if (!schema) {
7695
+ missingSchemas.add(data.component);
7696
+ return data;
7697
+ }
7698
+ const dataNew = { ...data };
7699
+ for (const [fieldName, fieldValue] of Object.entries(data)) {
7700
+ const fieldSchema = schema[fieldName.replace(/__i18n__.*/, "")];
7701
+ const fieldType = fieldSchema && typeof fieldSchema === "object" && "type" in fieldSchema && fieldSchema.type;
7702
+ const fieldRefMapper = typeof fieldType === "string" && fieldRefMappers2[fieldType];
7703
+ if (fieldSchema) {
7704
+ processedFields.add(fieldSchema);
7705
+ }
7706
+ if (fieldRefMapper) {
7707
+ dataNew[fieldName] = fieldRefMapper(fieldValue, {
7708
+ schema: fieldSchema,
7709
+ schemas,
7710
+ maps,
7711
+ fieldRefMappers: fieldRefMappers2,
7712
+ processedFields,
7713
+ missingSchemas
7714
+ });
7715
+ }
7716
+ }
7717
+ return dataNew;
7718
+ };
7719
+ const traverseAndMapRichtextDoc = (data, {
7720
+ schemas,
7721
+ maps,
7722
+ fieldRefMappers: fieldRefMappers2,
7723
+ processedFields,
7724
+ missingSchemas
7725
+ }) => {
7726
+ if (Array.isArray(data)) {
7727
+ return data.map((item) => traverseAndMapRichtextDoc(item, {
7728
+ schemas,
7729
+ maps,
7730
+ fieldRefMappers: fieldRefMappers2,
7731
+ processedFields,
7732
+ missingSchemas
7733
+ }));
7734
+ }
7735
+ if (data && typeof data === "object") {
7736
+ if (data.type === "link" && data.attrs.linktype === "story") {
7737
+ return {
7738
+ ...data,
7739
+ attrs: {
7740
+ ...data.attrs,
7741
+ uuid: maps.stories?.get(data.attrs.uuid) || data.attrs.uuid
7742
+ }
7743
+ };
7744
+ }
7745
+ if (data.type === "blok") {
7746
+ return {
7747
+ ...data,
7748
+ attrs: {
7749
+ ...data.attrs,
7750
+ body: data.attrs.body.map((d) => traverseAndMapBySchema(d, {
7751
+ schemas,
7752
+ maps,
7753
+ fieldRefMappers: fieldRefMappers2,
7754
+ processedFields,
7755
+ missingSchemas
7756
+ }))
7757
+ }
7758
+ };
7759
+ }
7760
+ const newData = {};
7761
+ for (const [k, value] of Object.entries(data)) {
7762
+ newData[k] = traverseAndMapRichtextDoc(value, {
7763
+ schemas,
7764
+ maps,
7765
+ fieldRefMappers: fieldRefMappers2,
7766
+ processedFields,
7767
+ missingSchemas
7768
+ });
7769
+ }
7770
+ return newData;
7771
+ }
7772
+ return data;
7773
+ };
7774
+ const richtextFieldRefMapper = (data, { schemas, maps, fieldRefMappers: fieldRefMappers2, processedFields, missingSchemas }) => traverseAndMapRichtextDoc(data, {
7775
+ schemas,
7776
+ maps,
7777
+ fieldRefMappers: fieldRefMappers2,
7778
+ processedFields,
7779
+ missingSchemas
7780
+ });
7781
+ const multilinkFieldRefMapper = (data, { maps }) => {
7782
+ if (data.linktype !== "story") {
7783
+ return data;
7784
+ }
7785
+ return {
7786
+ ...data,
7787
+ id: maps.stories?.get(data.id) || data.id
7788
+ };
7789
+ };
7790
+ const bloksFieldRefMapper = (data, { schemas, maps, fieldRefMappers: fieldRefMappers2, processedFields, missingSchemas }) => {
7791
+ if (!Array.isArray(data)) {
7792
+ throw new TypeError("Invalid data!");
7793
+ }
7794
+ return data.map((d) => traverseAndMapBySchema(d, {
7795
+ schemas,
7796
+ maps,
7797
+ fieldRefMappers: fieldRefMappers2,
7798
+ processedFields,
7799
+ missingSchemas
7800
+ }));
7801
+ };
7802
+ const assetFieldRefMapper = (data, { maps }) => {
7803
+ const mappedAsset = typeof data.id === "number" ? maps.assets?.get(data.id) : void 0;
7804
+ return {
7805
+ ...data,
7806
+ ...mappedAsset?.new
7807
+ };
7808
+ };
7809
+ const multiassetFieldRefMapper = (data, options) => {
7810
+ if (!Array.isArray(data)) {
7811
+ throw new TypeError("Invalid data!");
7812
+ }
7813
+ return data.map((d) => assetFieldRefMapper(d, options));
7814
+ };
7815
+ const optionsFieldRefMapper = (data, { schema, maps }) => {
7816
+ if (schema.source !== "internal_stories" || !Array.isArray(data)) {
7817
+ return data;
7818
+ }
7819
+ return data.map((d) => maps.stories?.get(d) || d);
7820
+ };
7821
+ const fieldRefMappers = {
7822
+ asset: assetFieldRefMapper,
7823
+ bloks: bloksFieldRefMapper,
7824
+ multiasset: multiassetFieldRefMapper,
7825
+ multilink: multilinkFieldRefMapper,
7826
+ options: optionsFieldRefMapper,
7827
+ richtext: richtextFieldRefMapper
7828
+ };
7829
+ const storyRefMapper = (story, { schemas, maps }) => {
7830
+ const processedFields = /* @__PURE__ */ new Set();
7831
+ const missingSchemas = /* @__PURE__ */ new Set();
7832
+ const alternates = story.alternates ? story.alternates.map((a) => ({
7833
+ ...a,
7834
+ id: maps.stories?.get(a.id) || a.id,
7835
+ parent_id: maps.stories?.get(a.parent_id) || a.parent_id
7836
+ })) : story.alternates;
7837
+ const parentId = maps.stories?.get(story.parent_id) || story.parent_id;
7838
+ const mappedStory = {
7839
+ ...story,
7840
+ content: traverseAndMapBySchema(story.content, {
7841
+ schemas,
7842
+ maps,
7843
+ fieldRefMappers,
7844
+ processedFields,
7845
+ missingSchemas
7846
+ }),
7847
+ id: Number(maps.stories?.get(story.id) || story.id),
7848
+ uuid: String(maps.stories?.get(story.uuid) || story.uuid),
7849
+ // @ts-expect-error Our types are wrong.
7850
+ parent_id: parentId ? Number(parentId) : null,
7851
+ alternates
7852
+ };
7853
+ return {
7854
+ mappedStory,
7855
+ processedFields,
7856
+ missingSchemas
7857
+ };
7858
+ };
7859
+
7860
+ const apiConcurrencyLock = new Sema(12);
7861
+ const fetchStoriesStream = ({
7862
+ spaceId,
7863
+ params = {},
7864
+ setTotalStories,
7865
+ setTotalPages,
7866
+ onIncrement,
7867
+ onPageSuccess,
7868
+ onPageError
7869
+ }) => {
7870
+ const listGenerator = async function* storyListIterator() {
7871
+ let perPage = 100;
7872
+ let page = 1;
7873
+ let totalPages = 1;
7874
+ setTotalPages?.(totalPages);
7875
+ while (page <= totalPages) {
7876
+ try {
7877
+ const result = await fetchStories(spaceId, {
7878
+ ...params,
7879
+ per_page: perPage,
7880
+ page
7881
+ });
7882
+ if (!result) {
7883
+ break;
7884
+ }
7885
+ const { headers } = result;
7886
+ const total = Number(headers.get("Total"));
7887
+ perPage = Number(headers.get("Per-Page"));
7888
+ totalPages = Math.ceil(total / perPage);
7889
+ setTotalStories?.(total);
7890
+ setTotalPages?.(totalPages);
7891
+ onPageSuccess?.(page, totalPages);
7892
+ for (const story of result.stories) {
7893
+ yield story;
7894
+ }
7895
+ page += 1;
7896
+ } catch (maybeError) {
7897
+ onPageError?.(toError(maybeError), page, totalPages);
7898
+ break;
7899
+ } finally {
7900
+ onIncrement?.();
7901
+ }
7902
+ }
7903
+ };
7904
+ return Readable.from(listGenerator());
7905
+ };
7906
+ const fetchStoryStream = ({
7907
+ spaceId,
7908
+ onIncrement,
7909
+ onStorySuccess,
7910
+ onStoryError
7911
+ }) => {
7912
+ const processing = /* @__PURE__ */ new Set();
7913
+ return new Transform({
7914
+ objectMode: true,
7915
+ async transform(listStory, _encoding, callback) {
7916
+ await apiConcurrencyLock.acquire();
7917
+ const task = fetchStory(spaceId, listStory.id.toString()).then((story) => {
7918
+ if (typeof story === "undefined") {
7919
+ throw new TypeError("Invalid story!");
7920
+ }
7921
+ onStorySuccess?.(story);
7922
+ this.push(story);
7923
+ }).catch((maybeError) => {
7924
+ onStoryError?.(toError(maybeError), listStory);
7925
+ }).finally(() => {
7926
+ onIncrement?.();
7927
+ apiConcurrencyLock.release();
7928
+ processing.delete(task);
7929
+ });
7930
+ processing.add(task);
7931
+ callback();
7932
+ },
7933
+ // Ensure all pending requests finish before closing the stream
7934
+ flush(callback) {
7935
+ Promise.all(processing).finally(() => callback());
7936
+ }
7937
+ });
7938
+ };
7939
+ const getUUIDFromFilename = (filename) => {
7940
+ const uuid = basename(filename, extname(filename)).split("_").at(-1);
7941
+ if (!uuid) {
7942
+ throw new Error(`Unable to extract UUID from local story "${filename}"`);
7943
+ }
7944
+ return uuid;
7945
+ };
7946
+ const readLocalStoriesStream = ({
7947
+ directoryPath,
7948
+ fileFilter = () => true,
7949
+ setTotalStories,
7950
+ onIncrement,
7951
+ onStorySuccess,
7952
+ onStoryError
7953
+ }) => {
7954
+ const listGenerator = async function* localStoryIterator() {
7955
+ const files = (await readDirectory(directoryPath)).filter((f) => extname(f) === ".json" && fileFilter({ uuid: getUUIDFromFilename(f) }));
7956
+ setTotalStories?.(files.length);
7957
+ for (const file of files) {
7958
+ try {
7959
+ const filePath = join(directoryPath, file);
7960
+ const fileContent = await readFile$1(filePath, "utf-8");
7961
+ const story = JSON.parse(fileContent);
7962
+ onStorySuccess?.(story);
7963
+ yield story;
7964
+ } catch (maybeError) {
7965
+ onStoryError?.(toError(maybeError), file);
7966
+ } finally {
7967
+ onIncrement?.();
7968
+ }
7969
+ }
7970
+ };
7971
+ return Readable.from(listGenerator());
7972
+ };
7973
+ const mapReferencesStream = ({
7974
+ schemas,
7975
+ maps,
7976
+ onIncrement,
7977
+ onStorySuccess,
7978
+ onStoryError
7979
+ }) => {
7980
+ return new Transform({
7981
+ objectMode: true,
7982
+ transform(localStory, _encoding, callback) {
7983
+ try {
7984
+ const { mappedStory, processedFields, missingSchemas } = storyRefMapper(localStory, { schemas, maps });
7985
+ onStorySuccess?.(mappedStory, processedFields, missingSchemas);
7986
+ this.push(mappedStory);
7987
+ } catch (maybeError) {
7988
+ onStoryError?.(toError(maybeError), localStory);
7989
+ } finally {
7990
+ onIncrement?.();
7991
+ callback();
7992
+ }
7993
+ }
7994
+ });
7995
+ };
7996
+ const getRemoteStory = async ({ spaceId, storyId }) => {
7997
+ const { data, response } = await getMapiClient().stories.get({
7998
+ path: {
7999
+ space_id: spaceId,
8000
+ story_id: storyId
8001
+ }
8002
+ });
8003
+ if (!response.ok && response.status !== 404) {
8004
+ handleAPIError("pull_story", new FetchError(response.statusText, response));
8005
+ }
8006
+ if (data?.story?.deleted_at) {
8007
+ return void 0;
8008
+ }
8009
+ return data?.story;
8010
+ };
8011
+ const makeCreateStoryAPITransport = ({ spaceId }) => async (localStory) => {
8012
+ const { id: _id, uuid: _uuid, content, parent_id: _p, ...newStoryData } = localStory;
8013
+ const remoteStory = await createStory(spaceId, {
8014
+ story: {
8015
+ ...newStoryData,
8016
+ content: {
8017
+ _uid: "",
8018
+ component: "__tmp__"
8019
+ }
8020
+ },
8021
+ publish: 0
8022
+ });
8023
+ if (!remoteStory) {
8024
+ throw new Error("No response!");
8025
+ }
8026
+ return remoteStory;
8027
+ };
8028
+ const makeAppendToManifestFSTransport = ({ manifestFile }) => async (localStory, remoteStory) => {
8029
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
8030
+ await appendToFile(manifestFile, JSON.stringify({
8031
+ old_id: localStory.uuid,
8032
+ new_id: remoteStory.uuid,
8033
+ created_at: createdAt
8034
+ }));
8035
+ await appendToFile(manifestFile, JSON.stringify({
8036
+ old_id: localStory.id,
8037
+ new_id: remoteStory.id,
8038
+ created_at: createdAt
8039
+ }));
8040
+ };
8041
+ const createStoryPlaceholderStream = ({
8042
+ maps,
8043
+ spaceId,
8044
+ transports,
8045
+ onIncrement,
8046
+ onStorySuccess,
8047
+ onStorySkipped,
8048
+ onStoryError
8049
+ }) => {
8050
+ const processing = /* @__PURE__ */ new Set();
8051
+ return new Writable({
8052
+ objectMode: true,
8053
+ async write(localStory, _encoding, callback) {
8054
+ await apiConcurrencyLock.acquire();
8055
+ const task = (async () => {
8056
+ try {
8057
+ const mappedStoryId = maps.stories?.get(localStory.id);
8058
+ const mappedRemoteStory = mappedStoryId && await getRemoteStory({ spaceId, storyId: Number(mappedStoryId) });
8059
+ if (mappedRemoteStory) {
8060
+ onStorySkipped?.(localStory, mappedRemoteStory);
8061
+ return;
8062
+ }
8063
+ const existingRemoteStory = await getRemoteStory({ spaceId, storyId: localStory.id });
8064
+ if (existingRemoteStory && existingRemoteStory.uuid === localStory.uuid) {
8065
+ await transports.appendStoryManifest(localStory, existingRemoteStory);
8066
+ onStorySkipped?.(localStory, existingRemoteStory);
8067
+ return;
8068
+ }
8069
+ const newRemoteStory = await transports.createStory(localStory);
8070
+ await transports.appendStoryManifest(localStory, newRemoteStory);
8071
+ onStorySuccess?.(localStory, newRemoteStory);
8072
+ } catch (maybeError) {
8073
+ onStoryError?.(toError(maybeError), localStory);
8074
+ }
8075
+ })();
8076
+ processing.add(task);
8077
+ task.finally(() => {
8078
+ onIncrement?.();
8079
+ apiConcurrencyLock.release();
8080
+ processing.delete(task);
8081
+ });
8082
+ callback();
8083
+ },
8084
+ final(callback) {
8085
+ Promise.all(processing).finally(() => callback());
8086
+ }
8087
+ });
8088
+ };
8089
+ const makeWriteStoryFSTransport = ({ directoryPath }) => async (story) => {
8090
+ await saveToFile(resolve$1(directoryPath, getStoryFilename(story)), JSON.stringify(story, null, 2));
8091
+ return story;
8092
+ };
8093
+ const makeWriteStoryAPITransport = ({ spaceId, publish }) => (mappedLocalStory) => updateStory(spaceId, mappedLocalStory.id, {
8094
+ story: mappedLocalStory,
8095
+ publish: publish ?? (mappedLocalStory.published ? 1 : 0)
8096
+ });
8097
+ const makeCleanupStoryFSTransport = ({ directoryPath, maps }) => async (mappedStory) => {
8098
+ const mapEntry = maps.stories?.entries().find(([_, v]) => v === mappedStory.uuid);
8099
+ const originalUuid = mapEntry?.[0] && typeof mapEntry?.[0] === "string" ? mapEntry?.[0] : mappedStory.uuid;
8100
+ const storyFilename = getStoryFilename({
8101
+ slug: mappedStory.slug,
8102
+ uuid: originalUuid
8103
+ });
8104
+ const storyFilePath = resolve$1(directoryPath, storyFilename);
8105
+ await unlink(storyFilePath);
8106
+ };
8107
+ const writeStoryStream = ({
8108
+ transports,
8109
+ onIncrement,
8110
+ onStorySuccess,
8111
+ onStoryError
8112
+ }) => {
8113
+ const processing = /* @__PURE__ */ new Set();
8114
+ return new Writable({
8115
+ objectMode: true,
8116
+ async write(mappedLocalStory, _encoding, callback) {
8117
+ await apiConcurrencyLock.acquire();
8118
+ const task = (async () => {
8119
+ try {
8120
+ const remoteStory = await transports.writeStory(mappedLocalStory);
8121
+ await transports.cleanupStory?.(remoteStory);
8122
+ onStorySuccess?.(mappedLocalStory, remoteStory);
8123
+ } catch (maybeError) {
8124
+ onStoryError?.(toError(maybeError), mappedLocalStory);
8125
+ }
8126
+ })();
8127
+ processing.add(task);
8128
+ task.finally(() => {
8129
+ onIncrement?.();
8130
+ apiConcurrencyLock.release();
8131
+ processing.delete(task);
8132
+ });
8133
+ callback();
8134
+ },
8135
+ final(callback) {
8136
+ Promise.all(processing).finally(() => callback());
8137
+ }
8138
+ });
8139
+ };
8140
+
8141
+ const PROGRESS_BAR_PADDING = 23;
8142
+ const upsertAssetFoldersPipeline = async ({
8143
+ directoryPath,
8144
+ logger,
8145
+ maps,
8146
+ transports,
8147
+ ui
8148
+ }) => {
8149
+ const folderProgress = ui.createProgressBar({ title: "Folders...".padEnd(PROGRESS_BAR_PADDING) });
8150
+ const summary = { total: 0, succeeded: 0, failed: 0 };
8151
+ await pipeline$1(
8152
+ readLocalAssetFoldersStream({
8153
+ directoryPath,
8154
+ setTotalFolders: (total) => {
8155
+ summary.total = total;
8156
+ folderProgress.setTotal(total);
8157
+ },
8158
+ onFolderError: (error) => {
8159
+ summary.failed += 1;
8160
+ logOnlyError(error);
8161
+ }
8162
+ }),
8163
+ upsertAssetFolderStream({
8164
+ transports,
8165
+ maps,
8166
+ onIncrement: () => folderProgress.increment(),
8167
+ onFolderSuccess: (localFolder, remoteFolder) => {
8168
+ summary.succeeded += 1;
8169
+ maps.assetFolders.set(localFolder.id, remoteFolder.id);
8170
+ logger.info("Created asset folder", { folderId: remoteFolder.id });
8171
+ },
8172
+ onFolderError: (error, folder) => {
8173
+ summary.failed += 1;
8174
+ logOnlyError(error, { folderId: folder.id });
8175
+ }
8176
+ })
8177
+ );
8178
+ return [["assetFolderResults", summary]];
8179
+ };
8180
+ const upsertAssetsPipeline = async ({
8181
+ assetBinaryPath,
8182
+ assetData,
8183
+ directoryPath,
8184
+ logger,
8185
+ maps,
8186
+ transports,
8187
+ ui
8188
+ }) => {
8189
+ const assetProgress = ui.createProgressBar({ title: "Assets...".padEnd(PROGRESS_BAR_PADDING) });
8190
+ const summary = { total: 0, succeeded: 0, failed: 0, skipped: 0 };
8191
+ const steps = [];
8192
+ if (assetBinaryPath && assetData) {
8193
+ summary.total = 1;
8194
+ assetProgress.setTotal(1);
8195
+ steps.push(readSingleAssetStream({
8196
+ asset: assetData,
8197
+ assetBinaryPath,
8198
+ onAssetError: (error) => {
8199
+ summary.failed += 1;
8200
+ assetProgress.increment();
8201
+ logOnlyError(error);
8202
+ }
8203
+ }));
8204
+ } else {
8205
+ steps.push(readLocalAssetsStream({
8206
+ directoryPath,
8207
+ setTotalAssets: (total) => {
8208
+ summary.total = total;
8209
+ assetProgress.setTotal(total);
8210
+ },
8211
+ onAssetError: (error) => {
8212
+ summary.failed += 1;
8213
+ assetProgress.increment();
8214
+ logOnlyError(error);
8215
+ }
8216
+ }));
8217
+ }
8218
+ steps.push(upsertAssetStream({
8219
+ transports,
8220
+ maps,
8221
+ onIncrement: () => assetProgress.increment(),
8222
+ onAssetSuccess: (localAssetResult, remoteAsset) => {
8223
+ if ("id" in localAssetResult && localAssetResult.id) {
8224
+ maps.assets.set(localAssetResult.id, {
8225
+ old: localAssetResult,
8226
+ new: {
8227
+ id: remoteAsset.id,
8228
+ filename: remoteAsset.filename,
8229
+ meta_data: remoteAsset.meta_data
8230
+ }
8231
+ });
8232
+ }
8233
+ summary.succeeded += 1;
8234
+ logger.info("Uploaded asset", { assetId: remoteAsset.id });
8235
+ },
8236
+ onAssetSkipped: (localAssetResult, remoteAsset) => {
8237
+ if ("id" in localAssetResult && localAssetResult.id) {
8238
+ maps.assets.set(localAssetResult.id, {
8239
+ old: localAssetResult,
8240
+ new: {
8241
+ id: remoteAsset.id,
8242
+ filename: remoteAsset.filename,
8243
+ meta_data: remoteAsset.meta_data
8244
+ }
8245
+ });
8246
+ }
8247
+ summary.skipped += 1;
8248
+ logger.debug("Skipped asset (unchanged)", { assetId: remoteAsset.id });
8249
+ },
8250
+ onAssetError: (error, asset) => {
8251
+ summary.failed += 1;
8252
+ logOnlyError(error, { assetId: asset.id });
8253
+ }
8254
+ }));
8255
+ await pipeline$1(steps);
8256
+ return [["assetResults", summary]];
8257
+ };
8258
+ const mapAssetReferencesInStoriesPipeline = async ({
8259
+ logger,
8260
+ maps,
8261
+ schemas,
8262
+ space,
8263
+ transports,
8264
+ ui
8265
+ }) => {
8266
+ if (Object.keys(schemas).length === 0) {
8267
+ const message = "No components found. Please run `storyblok components pull` to fetch the latest components.";
8268
+ ui.error(message);
8269
+ logger.error(message);
8270
+ return [];
8271
+ }
8272
+ const fetchStoryPagesProgress = ui.createProgressBar({ title: "Fetching Story Pages...".padEnd(PROGRESS_BAR_PADDING) });
8273
+ const fetchStoriesProgress = ui.createProgressBar({ title: "Fetching Stories...".padEnd(PROGRESS_BAR_PADDING) });
8274
+ const processProgress = ui.createProgressBar({ title: "Processing Stories...".padEnd(PROGRESS_BAR_PADDING) });
8275
+ const updateProgress = ui.createProgressBar({ title: "Updating Stories...".padEnd(PROGRESS_BAR_PADDING) });
8276
+ const summaries = {
8277
+ fetchStoryPages: { total: 0, succeeded: 0, failed: 0 },
8278
+ fetchStories: { total: 0, succeeded: 0, failed: 0 },
8279
+ storyProcessResults: { total: 0, succeeded: 0, failed: 0 },
8280
+ storyUpdateResults: { total: 0, succeeded: 0, failed: 0 }
8281
+ };
8282
+ const warnAboutMissingSchemas = (missingSchemas, story) => {
8283
+ const missingSchemaWarnings = /* @__PURE__ */ new Set();
8284
+ for (const schemaName of missingSchemas) {
8285
+ if (missingSchemaWarnings.has(schemaName)) {
8286
+ continue;
8287
+ }
8288
+ const message = `The component "${schemaName}" was not found. Please run \`storyblok components pull\` to fetch the latest components.`;
8289
+ logger.warn(message, { storyId: story.uuid });
8290
+ missingSchemaWarnings.add(schemaName);
8291
+ }
8292
+ };
8293
+ const assetMapValues = [...maps.assets.values()];
8294
+ const reference_search = assetMapValues.length === 1 ? assetMapValues[0].new.filename : void 0;
8295
+ await pipeline$1(
8296
+ fetchStoriesStream({
8297
+ spaceId: space,
8298
+ params: {
8299
+ reference_search
8300
+ },
8301
+ setTotalPages: (totalPages) => {
8302
+ summaries.fetchStoryPages.total = totalPages;
8303
+ fetchStoryPagesProgress.setTotal(totalPages);
8304
+ },
8305
+ setTotalStories: (total) => {
8306
+ summaries.fetchStories.total = total;
8307
+ summaries.storyProcessResults.total = total;
8308
+ summaries.storyUpdateResults.total = total;
8309
+ fetchStoriesProgress.setTotal(total);
8310
+ processProgress.setTotal(total);
8311
+ updateProgress.setTotal(total);
8312
+ },
8313
+ onIncrement: () => fetchStoryPagesProgress.increment(),
8314
+ onPageSuccess: (page, total) => {
8315
+ logger.info(`Fetched stories page ${page} of ${total}`);
8316
+ summaries.fetchStoryPages.succeeded += 1;
8317
+ },
8318
+ onPageError: (error, page, total) => {
8319
+ summaries.fetchStoryPages.failed += 1;
8320
+ logOnlyError(error, { page, total });
8321
+ }
8322
+ }),
8323
+ fetchStoryStream({
8324
+ spaceId: space,
8325
+ onIncrement: () => {
8326
+ fetchStoriesProgress.increment();
8327
+ },
8328
+ onStorySuccess: (story) => {
8329
+ logger.info("Fetched story", { storyId: story.id });
8330
+ summaries.fetchStories.succeeded += 1;
8331
+ },
8332
+ onStoryError: (error, story) => {
8333
+ summaries.fetchStories.failed += 1;
8334
+ summaries.storyProcessResults.total -= 1;
8335
+ summaries.storyUpdateResults.total -= 1;
8336
+ processProgress.setTotal(summaries.storyProcessResults.total);
8337
+ updateProgress.setTotal(summaries.storyProcessResults.total);
8338
+ logOnlyError(error, { storyId: story.id });
8339
+ }
8340
+ }),
8341
+ // Map all references to numeric ids and uuids.
8342
+ mapReferencesStream({
8343
+ schemas,
8344
+ maps: { stories: /* @__PURE__ */ new Map(), ...maps },
8345
+ onIncrement() {
8346
+ processProgress.increment();
8347
+ },
8348
+ onStorySuccess(localStory, _, missingSchemas) {
8349
+ warnAboutMissingSchemas(missingSchemas, localStory);
8350
+ logger.info("Processed story", { storyId: localStory.uuid });
8351
+ summaries.storyProcessResults.succeeded += 1;
8352
+ },
8353
+ onStoryError(error, localStory) {
8354
+ summaries.storyProcessResults.failed += 1;
8355
+ summaries.storyUpdateResults.total -= 1;
8356
+ updateProgress.setTotal(summaries.storyUpdateResults.total);
8357
+ logOnlyError(error, { storyId: localStory.id });
8358
+ }
8359
+ }),
8360
+ // Update remote stories with correct references.
8361
+ writeStoryStream({
8362
+ transports: {
8363
+ writeStory: transports.writeStory
8364
+ },
8365
+ onIncrement() {
8366
+ updateProgress.increment();
8367
+ },
8368
+ onStorySuccess(localStory) {
8369
+ logger.info("Updated story", { storyId: localStory.uuid });
8370
+ summaries.storyUpdateResults.succeeded += 1;
8371
+ },
8372
+ onStoryError(error, localStory) {
8373
+ summaries.storyUpdateResults.failed += 1;
8374
+ logOnlyError(error, { storyId: localStory.id });
8375
+ }
8376
+ })
8377
+ );
8378
+ return Object.entries(summaries);
8379
+ };
8380
+
8381
+ assetsCommand.command("push").argument("[asset]", "path or URL of a single asset to push").option("-f, --from <from>", "source space id").option("--data <data>", "inline asset data as JSON").option("--short-filename <short-filename>", "override the asset filename").option("--folder <folderId>", "destination asset folder ID").option("--cleanup", "delete local assets and metadata after a successful push (note: does not cleanup manifests)").option("--update-stories", "update file references in stories if necessary", false).option("--asset-token <token>", "asset token for accessing private assets").option("-d, --dry-run", "Preview changes without applying them to Storyblok").description(`Push local assets to a Storyblok space.`).action(async (assetInput, options, command) => {
8382
+ const ui = getUI();
8383
+ const logger = getLogger();
8384
+ const reporter = getReporter();
8385
+ ui.title(`${commands.ASSETS}`, colorPalette.ASSETS, "Pushing assets...");
8386
+ logger.info("Pushing assets started");
8387
+ if (options.dryRun) {
8388
+ ui.warn(`DRY RUN MODE ENABLED: No changes will be made.
8389
+ `);
8390
+ logger.warn("Dry run mode enabled");
8391
+ }
8392
+ const { space: targetSpace, path: basePath, verbose } = command.optsWithGlobals();
8393
+ const fromSpace = options.from || targetSpace;
8394
+ const assetToken = options.assetToken;
8395
+ const { state } = session();
8396
+ if (!requireAuthentication(state, verbose)) {
8397
+ process.exitCode = 2;
8398
+ return;
8399
+ }
8400
+ if (!targetSpace) {
8401
+ handleError(new CommandError(`Please provide the space as argument --space YOUR_SPACE_ID.`), verbose);
8402
+ process.exitCode = 2;
8403
+ return;
8404
+ }
8405
+ const { region } = state;
8406
+ const summaries = [];
8407
+ let fatalError = false;
8408
+ const manifestFile = join(resolveCommandPath(directories.assets, fromSpace, basePath), "manifest.jsonl");
8409
+ const folderManifestFile = join(resolveCommandPath(directories.assets, fromSpace, basePath), "folders", "manifest.jsonl");
8410
+ try {
8411
+ const [assetMap, assetFolderMap] = await Promise.all([
8412
+ loadAssetMap(manifestFile),
8413
+ loadAssetFolderMap(folderManifestFile)
8414
+ ]);
8415
+ const maps = { assets: assetMap, assetFolders: assetFolderMap };
8416
+ const assetsDirectoryPath = resolveCommandPath(directories.assets, fromSpace, basePath);
8417
+ const assetFolderGetTransport = makeGetAssetFolderAPITransport({ spaceId: targetSpace });
8418
+ const assetFolderCreateTransport = options.dryRun ? async (folder) => folder : makeCreateAssetFolderAPITransport({ spaceId: targetSpace });
8419
+ const assetFolderUpdateTransport = options.dryRun ? async (folder) => folder : makeUpdateAssetFolderAPITransport({ spaceId: targetSpace });
8420
+ const assetFolderManifestTransport = options.dryRun ? () => Promise.resolve() : makeAppendAssetFolderManifestFSTransport({ manifestFile: folderManifestFile });
8421
+ const cleanupAssetFolderTransport = options.cleanup && !options.dryRun ? makeCleanupAssetFolderFSTransport() : () => Promise.resolve();
8422
+ summaries.push(...await upsertAssetFoldersPipeline({
8423
+ directoryPath: join(assetsDirectoryPath, "folders"),
8424
+ logger,
8425
+ maps,
8426
+ transports: {
8427
+ getAssetFolder: assetFolderGetTransport,
8428
+ createAssetFolder: assetFolderCreateTransport,
8429
+ updateAssetFolder: assetFolderUpdateTransport,
8430
+ appendAssetFolderManifest: assetFolderManifestTransport,
8431
+ cleanupAssetFolder: cleanupAssetFolderTransport
8432
+ },
8433
+ ui
8434
+ }));
8435
+ const assetBinaryPath = typeof assetInput === "string" && assetInput.trim().length > 0 ? assetInput : void 0;
8436
+ let assetData;
8437
+ if (assetBinaryPath) {
8438
+ const assetDataPartial = options.data ? parseAssetData(options.data) : !isRemoteSource(assetBinaryPath) ? await loadSidecarAssetData(assetBinaryPath) : {};
8439
+ const sourceBasename = isRemoteSource(assetBinaryPath) ? basename(new URL(assetBinaryPath).pathname) : basename(assetBinaryPath);
8440
+ const shortFilename = options.shortFilename || assetDataPartial.short_filename || sourceBasename;
8441
+ const folderId = options.folder ? Number(options.folder) : void 0;
8442
+ assetData = {
8443
+ ...assetDataPartial,
8444
+ short_filename: shortFilename,
8445
+ asset_folder_id: folderId
8446
+ };
8447
+ }
8448
+ const getAssetTransport = makeGetAssetAPITransport({ spaceId: targetSpace });
8449
+ const createAssetTransport = options.dryRun ? async (asset) => asset : makeCreateAssetAPITransport({ spaceId: targetSpace });
8450
+ const updateAssetTransport = options.dryRun ? async (asset) => asset : makeUpdateAssetAPITransport({ spaceId: targetSpace });
8451
+ const downloadAssetFileTransport = makeDownloadAssetFileTransport({
8452
+ assetToken,
8453
+ region
8454
+ });
8455
+ const assetManifestTransport = options.dryRun ? () => Promise.resolve() : makeAppendAssetManifestFSTransport({ manifestFile });
8456
+ const cleanupAssetTransport = options.cleanup && !options.dryRun ? makeCleanupAssetFSTransport() : () => Promise.resolve();
8457
+ summaries.push(...await upsertAssetsPipeline({
8458
+ assetBinaryPath,
8459
+ assetData,
8460
+ directoryPath: assetsDirectoryPath,
8461
+ logger,
8462
+ maps,
8463
+ transports: {
8464
+ getAsset: getAssetTransport,
8465
+ createAsset: createAssetTransport,
8466
+ updateAsset: updateAssetTransport,
8467
+ downloadAssetFile: downloadAssetFileTransport,
8468
+ appendAssetManifest: assetManifestTransport,
8469
+ cleanupAsset: cleanupAssetTransport
8470
+ },
8471
+ ui
8472
+ }));
8473
+ const hasUpdatedFilename = (entry) => "filename" in entry.old && entry.old.filename !== entry.new.filename;
8474
+ const hasMetadata = (entry) => "meta_data" in entry.new && entry.new.meta_data;
8475
+ const hasUpdatedAssets = maps.assets.values().some((v) => hasUpdatedFilename(v) || hasMetadata(v));
8476
+ if (hasUpdatedAssets && options.updateStories) {
8477
+ const schemas = await findComponentSchemas(resolveCommandPath(directories.components, fromSpace, basePath));
8478
+ const writeStoryTransport = options.dryRun ? async (story) => story : makeWriteStoryAPITransport({ spaceId: targetSpace });
8479
+ summaries.push(...await mapAssetReferencesInStoriesPipeline({
8480
+ logger,
8481
+ maps,
8482
+ schemas,
8483
+ space: targetSpace,
8484
+ transports: {
8485
+ writeStory: writeStoryTransport
8486
+ },
8487
+ ui
8488
+ }));
8489
+ }
8490
+ if (!options.dryRun) {
8491
+ await deduplicateManifest(manifestFile);
8492
+ }
8493
+ } catch (maybeError) {
8494
+ fatalError = true;
8495
+ handleError(toError(maybeError), verbose);
8496
+ } finally {
8497
+ ui.stopAllProgressBars();
8498
+ const summary = Object.fromEntries(summaries);
8499
+ logger.info("Pushing assets finished", { summary });
8500
+ const assetsTotal = summary.assetResults?.total ?? 0;
8501
+ const assetsSucceeded = summary.assetResults?.succeeded ?? 0;
8502
+ const assetsSkipped = summary.assetResults?.skipped ?? 0;
8503
+ const assetsFailed = summary.assetResults?.failed ?? 0;
8504
+ ui.info(`Push results: ${assetsTotal} processed, ${assetsFailed} assets failed`);
8505
+ ui.list([
8506
+ `Folders: ${summary.assetFolderResults?.succeeded ?? 0}/${summary.assetFolderResults?.total ?? 0} succeeded, ${summary.assetFolderResults?.failed ?? 0} failed.`,
8507
+ `Assets: ${assetsSucceeded}/${assetsTotal} succeeded, ${assetsSkipped} skipped, ${assetsFailed} failed.`
8508
+ ]);
8509
+ for (const [name, reportSummary] of summaries) {
8510
+ reporter.addSummary(name, reportSummary);
8511
+ }
8512
+ reporter.finalize();
8513
+ const failedTotal = Object.values(summary).reduce((total, entry) => {
8514
+ if (!entry || typeof entry.failed !== "number") {
8515
+ return total;
8516
+ }
8517
+ return total + entry.failed;
8518
+ }, 0);
8519
+ process.exitCode = fatalError ? 2 : failedTotal > 0 ? 1 : 0;
8520
+ }
8521
+ });
8522
+
8523
+ const program$1 = getProgram();
8524
+ const storiesCommand = program$1.command(commands.STORIES).description(`Manage your space's stories`).option("-s, --space <space>", "space ID");
8525
+
8526
+ storiesCommand.command("pull").option("-d, --dry-run", "Preview changes without applying them to Storyblok").option("-p, --path <path>", "base path to store stories (default .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/"').description(`Download your space's stories as separate json files.`).action(async (options, command) => {
8527
+ const ui = getUI();
8528
+ const logger = getLogger();
8529
+ const reporter = getReporter();
8530
+ ui.title(`${commands.STORIES}`, colorPalette.STORIES, "Pulling stories...");
8531
+ logger.info("Pulling stories started");
8532
+ if (options.dryRun) {
8533
+ ui.warn(`DRY RUN MODE ENABLED: No changes will be made.
8534
+ `);
8535
+ logger.warn("Dry run mode enabled");
8536
+ }
8537
+ const { space, path: basePath, verbose } = command.optsWithGlobals();
8538
+ const { state } = session();
8539
+ if (!requireAuthentication(state, verbose)) {
8540
+ return;
8541
+ }
8542
+ if (!space) {
8543
+ handleError(new CommandError(`Please provide the space as argument --space YOUR_SPACE_ID.`), verbose);
8544
+ return;
8545
+ }
8546
+ const summary = {
8547
+ fetchStoryPages: { total: 0, succeeded: 0, failed: 0 },
8548
+ fetchStories: { total: 0, succeeded: 0, failed: 0 },
8549
+ save: { total: 0, succeeded: 0, failed: 0 }
8550
+ };
8551
+ try {
8552
+ const fetchStoryPagesProgress = ui.createProgressBar({ title: "Fetching Story Pages...".padEnd(23) });
8553
+ const fetchStoriesProgress = ui.createProgressBar({ title: "Fetching Stories...".padEnd(23) });
8554
+ const saveProgress = ui.createProgressBar({ title: "Saving Stories...".padEnd(23) });
8555
+ await pipeline$1(
8556
+ fetchStoriesStream({
8557
+ spaceId: space,
8558
+ params: {
8559
+ filter_query: options.query,
8560
+ starts_with: options.startsWith
8561
+ },
8562
+ setTotalPages: (totalPages) => {
8563
+ summary.fetchStoryPages.total = totalPages;
8564
+ fetchStoryPagesProgress.setTotal(totalPages);
8565
+ },
8566
+ setTotalStories: (total) => {
8567
+ summary.fetchStories.total = total;
8568
+ summary.save.total = total;
8569
+ fetchStoriesProgress.setTotal(total);
8570
+ saveProgress.setTotal(total);
8571
+ },
8572
+ onIncrement: () => {
8573
+ fetchStoryPagesProgress.increment();
8574
+ },
8575
+ onPageSuccess: (page, total) => {
8576
+ logger.info(`Fetched stories page ${page} of ${total}`);
8577
+ summary.fetchStoryPages.succeeded += 1;
8578
+ },
8579
+ onPageError: (error, page, total) => {
8580
+ summary.fetchStoryPages.failed += 1;
8581
+ handleError(error, verbose, { page, total });
8582
+ }
8583
+ }),
8584
+ fetchStoryStream({
8585
+ spaceId: space,
8586
+ onIncrement: () => {
8587
+ fetchStoriesProgress.increment();
8588
+ },
8589
+ onStorySuccess: (story) => {
8590
+ logger.info("Fetched story", { storyId: story.id });
8591
+ summary.fetchStories.succeeded += 1;
8592
+ },
8593
+ onStoryError: (error, story) => {
8594
+ summary.fetchStories.failed += 1;
8595
+ summary.save.total -= 1;
8596
+ saveProgress.setTotal(summary.save.total);
8597
+ handleError(error, verbose, { storyId: story.id });
8598
+ }
8599
+ }),
8600
+ writeStoryStream({
8601
+ transports: {
8602
+ writeStory: options.dryRun ? async (story) => story : makeWriteStoryFSTransport({ directoryPath: resolveCommandPath(directories.stories, space, basePath) })
8603
+ },
8604
+ onIncrement: () => {
8605
+ saveProgress.increment();
8606
+ },
8607
+ onStorySuccess: (story) => {
8608
+ logger.info("Saved story", { storyId: story.id });
8609
+ summary.save.succeeded += 1;
8610
+ },
8611
+ onStoryError: (error, story) => {
8612
+ summary.save.failed += 1;
8613
+ handleError(error, verbose, { storyId: story.id });
8614
+ }
8615
+ })
8616
+ );
8617
+ } catch (maybeError) {
8618
+ handleError(toError(maybeError));
8619
+ } finally {
8620
+ logger.info("Pulling stories finished", summary);
8621
+ ui.stopAllProgressBars();
8622
+ ui.info(`Pull results: ${summary.save.total} stories pulled, ${Math.max(summary.fetchStories.failed, summary.save.failed)} stories failed`);
8623
+ ui.list([
8624
+ `Fetching pages: ${summary.fetchStoryPages.succeeded}/${summary.fetchStoryPages.total} succeeded, ${summary.fetchStoryPages.failed} failed.`,
8625
+ `Fetching stories: ${summary.fetchStories.succeeded}/${summary.fetchStories.total} succeeded, ${summary.fetchStories.failed} failed.`,
8626
+ `Saving stories: ${summary.save.succeeded}/${summary.save.total} succeeded, ${summary.save.failed} failed.`
8627
+ ]);
8628
+ reporter.addSummary("fetchStoryPagesResults", summary.fetchStoryPages);
8629
+ reporter.addSummary("fetchStoriesResults", summary.fetchStories);
8630
+ reporter.addSummary("saveResults", summary.save);
8631
+ reporter.finalize();
8632
+ }
8633
+ });
8634
+
8635
+ storiesCommand.command("push").option("-f, --from <from>", "source space id").option("-p, --path <path>", "base path for stories and components (default .storyblok)").option("-d, --dry-run", "Preview changes without applying them to Storyblok").option("--publish", "Publish stories after pushing").option("--cleanup", "delete local stories after a successful push (note: does not cleanup manifests)").description(`Push local stories to a Storyblok space.`).action(async (options, command) => {
8636
+ const ui = getUI();
8637
+ const logger = getLogger();
8638
+ const reporter = getReporter();
8639
+ ui.title(`${commands.STORIES}`, colorPalette.STORIES, "Pushing stories...");
8640
+ logger.info("Pushing stories started");
8641
+ if (options.dryRun) {
8642
+ ui.warn(`DRY RUN MODE ENABLED: No changes will be made.
8643
+ `);
8644
+ logger.warn("Dry run mode enabled");
8645
+ }
8646
+ const { space, path: basePath, verbose } = command.optsWithGlobals();
8647
+ const fromSpace = options.from || space;
8648
+ const { state } = session();
8649
+ if (!requireAuthentication(state, verbose)) {
8650
+ return;
8651
+ }
8652
+ if (!space) {
8653
+ handleError(new CommandError(`Please provide the space as argument --space YOUR_SPACE_ID.`), verbose);
8654
+ return;
8655
+ }
8656
+ const warnAboutCustomPlugins = (fields, story) => {
8657
+ const warnedPlugins = /* @__PURE__ */ new Set();
8658
+ for (const field of fields) {
8659
+ if (field.type === "custom" && typeof field.field_type === "string") {
8660
+ if (warnedPlugins.has(field.field_type)) {
8661
+ continue;
8662
+ }
8663
+ warnedPlugins.add(field.field_type);
8664
+ const message = `The custom plugin "${field.field_type}" may contain references that require manual updates.`;
8665
+ ui.warn(message);
8666
+ logger.warn(message, { storyId: story.uuid });
8667
+ }
8668
+ }
8669
+ };
8670
+ const warnAboutMissingSchemas = (missingSchemas, story) => {
8671
+ const missingSchemaWarnings = /* @__PURE__ */ new Set();
8672
+ for (const schemaName of missingSchemas) {
8673
+ if (missingSchemaWarnings.has(schemaName)) {
8674
+ continue;
8675
+ }
8676
+ const message = `The component "${schemaName}" was not found. Please run \`storyblok components pull\` to fetch the latest components.`;
8677
+ ui.warn(message);
8678
+ logger.warn(message, { storyId: story.uuid });
8679
+ missingSchemaWarnings.add(schemaName);
8680
+ }
8681
+ };
8682
+ const summary = {
8683
+ creationResults: { total: 0, succeeded: 0, skipped: 0, failed: 0 },
8684
+ processResults: { total: 0, succeeded: 0, failed: 0 },
8685
+ updateResults: { total: 0, succeeded: 0, failed: 0 }
8686
+ };
8687
+ try {
8688
+ const manifestFile = join(resolveCommandPath(directories.stories, fromSpace, basePath), "manifest.jsonl");
8689
+ const manifest = await loadManifest(manifestFile);
8690
+ const assetManifestFile = join(resolveCommandPath(directories.assets, fromSpace, basePath), "manifest.jsonl");
8691
+ const maps = {
8692
+ assets: await loadAssetMap(assetManifestFile),
8693
+ stories: new Map(manifest.map((e) => [e.old_id, e.new_id]))
8694
+ };
8695
+ const schemas = await findComponentSchemas(resolveCommandPath(directories.components, fromSpace, basePath));
8696
+ if (Object.keys(schemas).length === 0) {
8697
+ const message = "No components found. Please run `storyblok components pull` to fetch the latest components.";
8698
+ ui.error(message);
8699
+ logger.error(message);
8700
+ return;
8701
+ }
8702
+ const storiesDirectoryPath = resolveCommandPath(directories.stories, fromSpace, basePath);
8703
+ const creationProgress = ui.createProgressBar({ title: "Creating Stories...".padEnd(21) });
8704
+ const processProgress = ui.createProgressBar({ title: "Processing Stories...".padEnd(21) });
8705
+ const updateProgress = ui.createProgressBar({ title: "Updating Stories...".padEnd(21) });
8706
+ await pipeline$1(
8707
+ // Read local stories from `.json` files.
8708
+ readLocalStoriesStream({
8709
+ directoryPath: storiesDirectoryPath,
8710
+ setTotalStories(total) {
8711
+ summary.creationResults.total = total;
8712
+ summary.processResults.total = total;
8713
+ summary.updateResults.total = total;
8714
+ creationProgress.setTotal(total);
8715
+ processProgress.setTotal(total);
8716
+ updateProgress.setTotal(total);
8717
+ },
8718
+ onStoryError(error) {
8719
+ summary.creationResults.failed += 1;
8720
+ summary.processResults.total -= 1;
8721
+ summary.updateResults.total -= 1;
8722
+ processProgress.setTotal(summary.processResults.total);
8723
+ updateProgress.setTotal(summary.updateResults.total);
8724
+ creationProgress.increment();
8725
+ handleError(error, verbose);
8726
+ }
8727
+ }),
8728
+ // Create remote stories.
8729
+ createStoryPlaceholderStream({
8730
+ maps,
8731
+ spaceId: space,
8732
+ transports: {
8733
+ createStory: options.dryRun ? async (story) => story : makeCreateStoryAPITransport({
8734
+ maps,
8735
+ spaceId: space
8736
+ }),
8737
+ appendStoryManifest: options.dryRun ? () => Promise.resolve() : makeAppendToManifestFSTransport({
8738
+ manifestFile
8739
+ })
8740
+ },
8741
+ onStorySuccess(localStory, remoteStory) {
8742
+ if (!localStory.uuid || !remoteStory.uuid) {
8743
+ throw new Error("Invalid story provided!");
8744
+ }
8745
+ maps.stories.set(localStory.id, remoteStory.id);
8746
+ maps.stories.set(localStory.uuid, remoteStory.uuid);
8747
+ logger.info("Created story", { storyId: remoteStory.uuid });
8748
+ summary.creationResults.succeeded += 1;
8749
+ },
8750
+ onStorySkipped(localStory, remoteStory) {
8751
+ if (!localStory.uuid || !remoteStory.uuid) {
8752
+ throw new Error("Invalid story provided!");
8753
+ }
8754
+ maps.stories.set(localStory.id, remoteStory.id);
8755
+ maps.stories.set(localStory.uuid, remoteStory.uuid);
8756
+ logger.info("Skipped creating story", { storyId: localStory.uuid });
8757
+ summary.creationResults.skipped += 1;
8758
+ },
8759
+ onStoryError(error) {
8760
+ summary.creationResults.failed += 1;
8761
+ summary.processResults.total -= 1;
8762
+ summary.updateResults.total -= 1;
8763
+ processProgress.setTotal(summary.processResults.total);
8764
+ updateProgress.setTotal(summary.updateResults.total);
8765
+ handleError(error, verbose);
8766
+ },
8767
+ onIncrement() {
8768
+ creationProgress.increment();
8769
+ }
8770
+ })
8771
+ );
8772
+ await pipeline$1(
8773
+ // Read local stories from `.json` files.
8774
+ readLocalStoriesStream({
8775
+ directoryPath: storiesDirectoryPath,
8776
+ fileFilter({ uuid }) {
8777
+ return Boolean(maps.stories.get(uuid));
8778
+ },
8779
+ setTotalStories(total) {
8780
+ summary.processResults.total = total;
8781
+ summary.updateResults.total = total;
8782
+ processProgress.setTotal(total);
8783
+ updateProgress.setTotal(total);
8784
+ },
8785
+ onStoryError(error) {
8786
+ summary.creationResults.failed += 1;
8787
+ summary.processResults.total -= 1;
8788
+ summary.updateResults.total -= 1;
8789
+ processProgress.setTotal(summary.processResults.total);
8790
+ updateProgress.setTotal(summary.updateResults.total);
8791
+ handleError(error, verbose);
8792
+ }
8793
+ }),
8794
+ // Map all references to numeric ids and uuids.
8795
+ mapReferencesStream({
8796
+ schemas,
8797
+ maps,
8798
+ onIncrement() {
8799
+ processProgress.increment();
8800
+ },
8801
+ onStorySuccess(localStory, processedFields, missingSchemas) {
8802
+ warnAboutCustomPlugins(processedFields, localStory);
8803
+ warnAboutMissingSchemas(missingSchemas, localStory);
8804
+ logger.info("Processed story", { storyId: localStory.uuid });
8805
+ summary.processResults.succeeded += 1;
8806
+ },
8807
+ onStoryError(error, localStory) {
8808
+ summary.processResults.failed += 1;
8809
+ summary.updateResults.total -= 1;
8810
+ updateProgress.setTotal(summary.updateResults.total);
8811
+ handleError(error, verbose, { storyId: localStory.uuid });
8812
+ }
8813
+ }),
8814
+ // Update remote stories with correct references.
8815
+ writeStoryStream({
8816
+ transports: {
8817
+ writeStory: options.dryRun ? async (story) => story : makeWriteStoryAPITransport({
8818
+ spaceId: space,
8819
+ publish: options.publish ? 1 : void 0
8820
+ }),
8821
+ cleanupStory: options.cleanup && !options.dryRun ? makeCleanupStoryFSTransport({ directoryPath: storiesDirectoryPath, maps }) : void 0
8822
+ },
8823
+ onIncrement() {
8824
+ updateProgress.increment();
8825
+ },
8826
+ onStorySuccess(localStory) {
8827
+ logger.info("Updated story", { storyId: localStory.uuid });
8828
+ summary.updateResults.succeeded += 1;
8829
+ },
8830
+ onStoryError(error, localStory) {
8831
+ summary.updateResults.failed += 1;
8832
+ handleError(error, verbose, { storyId: localStory.uuid });
8833
+ }
8834
+ })
8835
+ );
8836
+ } catch (maybeError) {
8837
+ handleError(toError(maybeError));
8838
+ } finally {
8839
+ logger.info("Pushing stories finished", summary);
8840
+ ui.stopAllProgressBars();
8841
+ const failedStories = Math.max(summary.creationResults.failed, summary.processResults.failed, summary.updateResults.failed);
8842
+ ui.info(`Push results: ${summary.creationResults.total} ${summary.creationResults.total === 1 ? "story" : "stories"} pushed, ${failedStories} ${failedStories === 1 ? "story" : "stories"} failed`);
8843
+ ui.list([
8844
+ `Creating stories: ${summary.creationResults.succeeded + summary.creationResults.skipped}/${summary.creationResults.total} succeeded, ${summary.creationResults.failed} failed.`,
8845
+ `Processing stories: ${summary.processResults.succeeded}/${summary.processResults.total} succeeded, ${summary.processResults.failed} failed.`,
8846
+ `Updating stories: ${summary.updateResults.succeeded}/${summary.updateResults.total} succeeded, ${summary.updateResults.failed} failed.`
8847
+ ]);
8848
+ reporter.addSummary("creationResults", summary.creationResults);
8849
+ reporter.addSummary("processResults", summary.processResults);
8850
+ reporter.addSummary("updateResults", summary.updateResults);
8851
+ reporter.finalize();
8852
+ }
8853
+ });
8854
+
6583
8855
  const program = getProgram();
6584
8856
  konsola.br();
6585
8857
  konsola.br();