storyblok 4.12.1 → 4.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -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,10 +8,10 @@ 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
16
  import { select, password, input, confirm } from '@inquirer/prompts';
17
17
  import { ManagementApiClient } from '@storyblok/management-api-client';
@@ -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
  }
@@ -1558,7 +1625,7 @@ function getProgram() {
1558
1625
  }
1559
1626
  if (resolvedConfig.log.file.enabled) {
1560
1627
  const logsPath = resolveCommandPath(
1561
- directories.log,
1628
+ directories.logs,
1562
1629
  options.space,
1563
1630
  options.path
1564
1631
  );
@@ -1582,7 +1649,7 @@ function getProgram() {
1582
1649
  getUI({ enabled: resolvedConfig.ui.enabled });
1583
1650
  if (resolvedConfig.report.enabled) {
1584
1651
  const reportPath = resolveCommandPath(
1585
- directories.report,
1652
+ directories.reports,
1586
1653
  options.space,
1587
1654
  options.path
1588
1655
  );
@@ -1629,21 +1696,25 @@ function configsAreEqual(config1, config2) {
1629
1696
  function mapiClient(options) {
1630
1697
  if (!instance && options) {
1631
1698
  instance = new ManagementApiClient(options);
1632
- instance.interceptors.request.use(async (request) => {
1633
- const limit = resolveLimiter();
1634
- await limit();
1635
- return request;
1636
- });
1699
+ if (getActiveConfig().api.maxConcurrency > 0) {
1700
+ instance.interceptors.request.use(async (request) => {
1701
+ const limit = resolveLimiter();
1702
+ await limit();
1703
+ return request;
1704
+ });
1705
+ }
1637
1706
  storedConfig = options;
1638
1707
  } else if (!instance) {
1639
1708
  throw new Error("MAPI client not initialized. Call mapiClient with configuration first.");
1640
1709
  } else if (options && storedConfig && !configsAreEqual(options, storedConfig)) {
1641
1710
  instance = new ManagementApiClient(options);
1642
- instance.interceptors.request.use(async (request) => {
1643
- const limit = resolveLimiter();
1644
- await limit();
1645
- return request;
1646
- });
1711
+ if (getActiveConfig().api.maxConcurrency > 0) {
1712
+ instance.interceptors.request.use(async (request) => {
1713
+ const limit = resolveLimiter();
1714
+ await limit();
1715
+ return request;
1716
+ });
1717
+ }
1647
1718
  storedConfig = options;
1648
1719
  }
1649
1720
  return instance;
@@ -1862,29 +1933,123 @@ function session() {
1862
1933
  return sessionInstance;
1863
1934
  }
1864
1935
 
1865
- const program$h = getProgram();
1866
- const allRegionsText = Object.values(regions).join(",");
1867
- const loginStrategy = {
1868
- message: "How would you like to login?",
1869
- choices: [
1870
- {
1871
- name: "With email",
1872
- value: "login-with-email",
1873
- short: "Email"
1874
- },
1875
- {
1876
- name: "With Token (Personal Access Token \u2013 works also for SSO accounts)",
1877
- value: "login-with-token",
1878
- short: "Token"
1936
+ async function performInteractiveLogin(options) {
1937
+ const { verbose = false, preSelectedRegion, showWelcomeMessage = true } = options || {};
1938
+ const spinner = new Spinner({
1939
+ verbose: !isVitest
1940
+ });
1941
+ try {
1942
+ const strategy = await select({
1943
+ message: "How would you like to login?",
1944
+ choices: [
1945
+ {
1946
+ name: "With email",
1947
+ value: "login-with-email",
1948
+ short: "Email"
1949
+ },
1950
+ {
1951
+ name: "With Token (Personal Access Token \u2013 works also for SSO accounts)",
1952
+ value: "login-with-token",
1953
+ short: "Token"
1954
+ }
1955
+ ]
1956
+ });
1957
+ let userToken;
1958
+ let userRegion;
1959
+ if (strategy === "login-with-token") {
1960
+ konsola.info([
1961
+ "\u{1F511} You can use a Personal Access Token to log in.",
1962
+ "This works for all accounts, including SSO accounts.",
1963
+ `Generate one in your Storyblok account settings: ${chalk.underline.blue("https://app.storyblok.com/#/me/account?tab=token")}`
1964
+ ].join("\n"));
1965
+ userToken = await password({
1966
+ message: "Please enter your Personal Access Token:",
1967
+ validate: (value) => {
1968
+ return value.length > 0;
1969
+ }
1970
+ });
1971
+ userRegion = preSelectedRegion || await select({
1972
+ message: "Please select the region you would like to work in:",
1973
+ choices: Object.values(regions).map((region) => ({
1974
+ name: regionNames[region],
1975
+ value: region
1976
+ })),
1977
+ default: regions.EU
1978
+ });
1979
+ spinner.start(`Logging in with token`);
1980
+ const user = await loginWithToken(userToken, userRegion);
1981
+ spinner.succeed();
1982
+ if (user) {
1983
+ const { updateSession, persistCredentials } = session();
1984
+ updateSession(user.email, userToken, userRegion);
1985
+ await persistCredentials(userRegion);
1986
+ if (showWelcomeMessage) {
1987
+ konsola.ok(`Successfully logged in to region ${chalk.hex(colorPalette.PRIMARY)(`${regionNames[userRegion]} (${userRegion})`)}. Welcome ${chalk.hex(colorPalette.PRIMARY)(user.friendly_name)}.`, true);
1988
+ }
1989
+ return { token: userToken, region: userRegion };
1990
+ }
1991
+ } else {
1992
+ const userEmail = await input({
1993
+ message: "Please enter your email address:",
1994
+ required: true,
1995
+ validate: (value) => {
1996
+ const emailRegex = /^[^\s@]+@[^\s@][^\s.@]*\.[^\s@]+$/;
1997
+ return emailRegex.test(value);
1998
+ }
1999
+ });
2000
+ const userPassword = await password({
2001
+ message: "Please enter your password:"
2002
+ });
2003
+ userRegion = preSelectedRegion || await select({
2004
+ message: "Please select the region you would like to work in:",
2005
+ choices: Object.values(regions).map((region) => ({
2006
+ name: regionNames[region],
2007
+ value: region
2008
+ })),
2009
+ default: regions.EU
2010
+ });
2011
+ spinner.start(`Logging in with email`);
2012
+ spinner.succeed();
2013
+ const response = await loginWithEmailAndPassword(userEmail, userPassword, userRegion);
2014
+ if (response?.otp_required) {
2015
+ const otp = await input({
2016
+ message: "Add the code from your Authenticator app, or the one we sent to your e-mail / phone:",
2017
+ required: true
2018
+ });
2019
+ const otpResponse = await loginWithOtp(userEmail, userPassword, otp, userRegion);
2020
+ if (otpResponse?.access_token) {
2021
+ userToken = otpResponse.access_token;
2022
+ }
2023
+ } else if (response?.access_token) {
2024
+ userToken = response.access_token;
2025
+ }
2026
+ if (userToken) {
2027
+ const { updateSession, persistCredentials } = session();
2028
+ updateSession(userEmail, userToken, userRegion);
2029
+ await persistCredentials(userRegion);
2030
+ if (showWelcomeMessage) {
2031
+ konsola.ok(`Successfully logged in to region ${chalk.hex(colorPalette.PRIMARY)(`${regionNames[userRegion]} (${userRegion})`)}. Welcome ${chalk.hex(colorPalette.PRIMARY)(userEmail)}.`, true);
2032
+ }
2033
+ return { token: userToken, region: userRegion };
2034
+ }
1879
2035
  }
1880
- ]
1881
- };
1882
- 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(
2036
+ return null;
2037
+ } catch (error) {
2038
+ spinner.failed();
2039
+ konsola.br();
2040
+ handleError(error, verbose);
2041
+ return null;
2042
+ }
2043
+ }
2044
+
2045
+ const program$j = getProgram();
2046
+ const allRegionsText = Object.values(regions).join(",");
2047
+ 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(
1883
2048
  "-r, --region <region>",
1884
2049
  `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}.`
1885
2050
  ).action(async (options) => {
1886
2051
  konsola.title(`${commands.LOGIN}`, colorPalette.LOGIN);
1887
- const verbose = program$h.opts().verbose;
2052
+ const verbose = program$j.opts().verbose;
1888
2053
  const { token, region } = options;
1889
2054
  const { state, updateSession, persistCredentials, initializeSession } = session();
1890
2055
  await initializeSession();
@@ -1926,85 +2091,16 @@ program$h.command(commands.LOGIN).description("Login to the Storyblok CLI").opti
1926
2091
  handleError(error, verbose);
1927
2092
  }
1928
2093
  } else {
1929
- const spinner = new Spinner({
1930
- verbose: !isVitest
1931
- });
1932
2094
  try {
1933
- const strategy = await select(loginStrategy);
1934
- if (strategy === "login-with-token") {
1935
- konsola.info([
1936
- "\u{1F511} You can use a Personal Access Token to log in.",
1937
- "This works for all accounts, including SSO accounts.",
1938
- `Generate one in your Storyblok account settings: ${chalk.underline.blue("https://app.storyblok.com/#/me/account?tab=token")}`
1939
- ].join("\n"));
1940
- const userToken = await password({
1941
- message: "Please enter your Personal Access Token:",
1942
- validate: (value) => {
1943
- return value.length > 0;
1944
- }
1945
- });
1946
- let userRegion = region;
1947
- if (!userRegion) {
1948
- userRegion = await select({
1949
- message: "Please select the region you would like to work in:",
1950
- choices: Object.values(regions).map((region2) => ({
1951
- name: regionNames[region2],
1952
- value: region2
1953
- })),
1954
- default: regions.EU
1955
- });
1956
- }
1957
- spinner.start(`Logging in with token`);
1958
- const user = await loginWithToken(userToken, userRegion);
1959
- spinner.succeed();
1960
- if (user) {
1961
- updateSession(user.email, userToken, userRegion);
1962
- await persistCredentials(userRegion);
1963
- konsola.ok(`Successfully logged in to region ${chalk.hex(colorPalette.PRIMARY)(`${regionNames[userRegion]} (${userRegion})`)}. Welcome ${chalk.hex(colorPalette.PRIMARY)(user.friendly_name)}.`, true);
1964
- }
1965
- } else {
1966
- const userEmail = await input({
1967
- message: "Please enter your email address:",
1968
- required: true,
1969
- validate: (value) => {
1970
- const emailRegex = /^[^\s@]+@[^\s@][^\s.@]*\.[^\s@]+$/;
1971
- return emailRegex.test(value);
1972
- }
1973
- });
1974
- const userPassword = await password({
1975
- message: "Please enter your password:"
1976
- });
1977
- let userRegion = region;
1978
- if (!userRegion) {
1979
- userRegion = await select({
1980
- message: "Please select the region you would like to work in:",
1981
- choices: Object.values(regions).map((region2) => ({
1982
- name: regionNames[region2],
1983
- value: region2
1984
- })),
1985
- default: regions.EU
1986
- });
1987
- }
1988
- spinner.start(`Logging in with email`);
1989
- spinner.succeed();
1990
- const response = await loginWithEmailAndPassword(userEmail, userPassword, userRegion);
1991
- if (response?.otp_required) {
1992
- const otp = await input({
1993
- message: "Add the code from your Authenticator app, or the one we sent to your e-mail / phone:",
1994
- required: true
1995
- });
1996
- const otpResponse = await loginWithOtp(userEmail, userPassword, otp, userRegion);
1997
- if (otpResponse?.access_token) {
1998
- updateSession(userEmail, otpResponse?.access_token, userRegion);
1999
- }
2000
- } else if (response?.access_token) {
2001
- updateSession(userEmail, response.access_token, userRegion);
2002
- }
2003
- await persistCredentials(region);
2004
- konsola.ok(`Successfully logged in to region ${chalk.hex(colorPalette.PRIMARY)(`${regionNames[userRegion]} (${userRegion})`)}. Welcome ${chalk.hex(colorPalette.PRIMARY)(userEmail)}.`, true);
2095
+ const result = await performInteractiveLogin({
2096
+ verbose,
2097
+ preSelectedRegion: region,
2098
+ showWelcomeMessage: true
2099
+ });
2100
+ if (!result) {
2101
+ konsola.warn("Login cancelled or failed.");
2005
2102
  }
2006
2103
  } catch (error) {
2007
- spinner.failed();
2008
2104
  konsola.br();
2009
2105
  handleError(error, verbose);
2010
2106
  }
@@ -2012,10 +2108,10 @@ program$h.command(commands.LOGIN).description("Login to the Storyblok CLI").opti
2012
2108
  konsola.br();
2013
2109
  });
2014
2110
 
2015
- const program$g = getProgram();
2016
- program$g.command(commands.LOGOUT).description("Logout from the Storyblok CLI").action(async () => {
2111
+ const program$i = getProgram();
2112
+ program$i.command(commands.LOGOUT).description("Logout from the Storyblok CLI").action(async () => {
2017
2113
  konsola.title(`${commands.LOGOUT}`, colorPalette.LOGOUT);
2018
- const verbose = program$g.opts().verbose;
2114
+ const verbose = program$i.opts().verbose;
2019
2115
  try {
2020
2116
  const { state, initializeSession } = session();
2021
2117
  await initializeSession();
@@ -2063,10 +2159,10 @@ async function openSignupInBrowser(url) {
2063
2159
  }
2064
2160
  }
2065
2161
 
2066
- const program$f = getProgram();
2067
- program$f.command(commands.SIGNUP).description("Sign up for Storyblok").action(async () => {
2162
+ const program$h = getProgram();
2163
+ program$h.command(commands.SIGNUP).description("Sign up for Storyblok").action(async () => {
2068
2164
  konsola.title(`${commands.SIGNUP}`, colorPalette.SIGNUP);
2069
- const verbose = program$f.opts().verbose;
2165
+ const verbose = program$h.opts().verbose;
2070
2166
  const { state, initializeSession } = session();
2071
2167
  await initializeSession();
2072
2168
  if (state.isLoggedIn && !state.envLogin) {
@@ -2088,10 +2184,10 @@ program$f.command(commands.SIGNUP).description("Sign up for Storyblok").action(a
2088
2184
  konsola.br();
2089
2185
  });
2090
2186
 
2091
- const program$e = getProgram();
2092
- program$e.command(commands.USER).description("Get the current user").action(async () => {
2187
+ const program$g = getProgram();
2188
+ program$g.command(commands.USER).description("Get the current user").action(async () => {
2093
2189
  konsola.title(`${commands.USER}`, colorPalette.USER);
2094
- const verbose = program$e.opts().verbose;
2190
+ const verbose = program$g.opts().verbose;
2095
2191
  const { state, initializeSession } = session();
2096
2192
  await initializeSession();
2097
2193
  if (!requireAuthentication(state)) {
@@ -2120,8 +2216,8 @@ program$e.command(commands.USER).description("Get the current user").action(asyn
2120
2216
  konsola.br();
2121
2217
  });
2122
2218
 
2123
- const program$d = getProgram();
2124
- 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");
2219
+ const program$f = getProgram();
2220
+ 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");
2125
2221
 
2126
2222
  const DEFAULT_COMPONENTS_FILENAME = "components";
2127
2223
  const DEFAULT_GROUPS_FILENAME = "groups";
@@ -2506,10 +2602,10 @@ async function readConsolidatedFiles$1(resolvedPath, suffix) {
2506
2602
  };
2507
2603
  }
2508
2604
 
2509
- const program$c = getProgram();
2605
+ const program$e = getProgram();
2510
2606
  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) => {
2511
2607
  konsola.title(`${commands.COMPONENTS}`, colorPalette.COMPONENTS, componentName ? `Pulling component ${componentName}...` : "Pulling components...");
2512
- const verbose = program$c.opts().verbose;
2608
+ const verbose = program$e.opts().verbose;
2513
2609
  const { space, path } = componentsCommand.opts();
2514
2610
  const {
2515
2611
  separateFiles = false,
@@ -3551,10 +3647,10 @@ async function pushWithDependencyGraph(space, spaceState, maxConcurrency = getAc
3551
3647
  return results;
3552
3648
  }
3553
3649
 
3554
- const program$b = getProgram();
3650
+ const program$d = getProgram();
3555
3651
  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) => {
3556
3652
  konsola.title(`${commands.COMPONENTS}`, colorPalette.COMPONENTS, componentName ? `Pushing component ${componentName}...` : "Pushing components...");
3557
- const verbose = program$b.opts().verbose;
3653
+ const verbose = program$d.opts().verbose;
3558
3654
  const { space, path } = componentsCommand.opts();
3559
3655
  const { filter } = options;
3560
3656
  const fromSpace = options.from || space;
@@ -3750,11 +3846,11 @@ const saveLanguagesToFile = async (space, internationalizationOptions, options)
3750
3846
  }
3751
3847
  };
3752
3848
 
3753
- const program$a = getProgram();
3754
- 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");
3849
+ const program$c = getProgram();
3850
+ 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");
3755
3851
  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) => {
3756
3852
  konsola.title(`${commands.LANGUAGES}`, colorPalette.LANGUAGES);
3757
- const verbose = program$a.opts().verbose;
3853
+ const verbose = program$c.opts().verbose;
3758
3854
  const { space, path } = languagesCommand.opts();
3759
3855
  const { filename = "languages", suffix = options.space } = options;
3760
3856
  const { state, initializeSession } = session();
@@ -3801,8 +3897,8 @@ languagesCommand.command("pull").description(`Download your space's languages sc
3801
3897
  konsola.br();
3802
3898
  });
3803
3899
 
3804
- const program$9 = getProgram();
3805
- 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");
3900
+ const program$b = getProgram();
3901
+ 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");
3806
3902
 
3807
3903
  const getMigrationTemplate = () => {
3808
3904
  return `export default function (block) {
@@ -3920,11 +4016,29 @@ const fetchStory = async (spaceId, storyId) => {
3920
4016
  },
3921
4017
  throwOnError: true
3922
4018
  });
3923
- return data?.story;
4019
+ return data.story;
3924
4020
  } catch (error) {
3925
4021
  handleAPIError("pull_story", error);
3926
4022
  }
3927
4023
  };
4024
+ const createStory = async (spaceId, payload) => {
4025
+ try {
4026
+ const client = mapiClient();
4027
+ const { data } = await client.stories.create({
4028
+ path: {
4029
+ space_id: spaceId
4030
+ },
4031
+ body: {
4032
+ story: payload.story,
4033
+ publish: payload.publish
4034
+ },
4035
+ throwOnError: true
4036
+ });
4037
+ return data?.story;
4038
+ } catch (maybeError) {
4039
+ handleAPIError("create_story", toError(maybeError));
4040
+ }
4041
+ };
3928
4042
  const updateStory = async (spaceId, storyId, payload) => {
3929
4043
  try {
3930
4044
  const client = mapiClient();
@@ -3940,9 +4054,14 @@ const updateStory = async (spaceId, storyId, payload) => {
3940
4054
  },
3941
4055
  throwOnError: true
3942
4056
  });
3943
- return data?.story;
3944
- } catch (error) {
3945
- handleAPIError("update_story", error);
4057
+ const { story } = data;
4058
+ if (!story) {
4059
+ throw new Error("Failed to update story");
4060
+ }
4061
+ return story;
4062
+ } catch (maybeError) {
4063
+ handleAPIError("update_story", toError(maybeError));
4064
+ throw maybeError;
3946
4065
  }
3947
4066
  };
3948
4067
 
@@ -4399,6 +4518,49 @@ const isStoryPublishedWithoutChanges = (story) => {
4399
4518
  const isStoryWithUnpublishedChanges = (story) => {
4400
4519
  return story.published && story.unpublished_changes;
4401
4520
  };
4521
+ const toComponent = (maybeComponent) => {
4522
+ if (maybeComponent.component_group_uuid === void 0) {
4523
+ return null;
4524
+ }
4525
+ return maybeComponent;
4526
+ };
4527
+ const findComponentSchemas = async (directoryPath) => {
4528
+ const files = await readdir(directoryPath).catch((error) => {
4529
+ if (error.code === "ENOENT") {
4530
+ return [];
4531
+ }
4532
+ throw error;
4533
+ });
4534
+ const fileContents = files.filter((f) => path.extname(f) === ".json").map((f) => {
4535
+ const filePath = path.join(directoryPath, f);
4536
+ const fileContent = readFileSync(filePath, "utf-8");
4537
+ return JSON.parse(fileContent);
4538
+ });
4539
+ const components = [];
4540
+ for (const content of fileContents) {
4541
+ if (Array.isArray(content)) {
4542
+ for (const maybeComponent of content) {
4543
+ const component2 = toComponent(maybeComponent);
4544
+ if (component2) {
4545
+ components.push(component2);
4546
+ }
4547
+ }
4548
+ continue;
4549
+ }
4550
+ const component = toComponent(content);
4551
+ if (component) {
4552
+ components.push(component);
4553
+ }
4554
+ }
4555
+ const schemas = {};
4556
+ for (const component of components) {
4557
+ schemas[component.name] = component.schema;
4558
+ }
4559
+ return schemas;
4560
+ };
4561
+ const getStoryFilename = (story) => {
4562
+ return `${story.slug}_${story.uuid}.json`;
4563
+ };
4402
4564
 
4403
4565
  class UpdateStream extends Writable {
4404
4566
  constructor(options) {
@@ -4738,8 +4900,8 @@ migrationsCommand.command("rollback [migrationFile]").description("Rollback a mi
4738
4900
  }
4739
4901
  });
4740
4902
 
4741
- const program$8 = getProgram();
4742
- 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");
4903
+ const program$a = getProgram();
4904
+ 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");
4743
4905
 
4744
4906
  const getAssetJSONSchema = (title) => ({
4745
4907
  $id: "#/asset",
@@ -5649,13 +5811,13 @@ async function readConsolidatedFiles(resolvedPath, suffix) {
5649
5811
  };
5650
5812
  }
5651
5813
 
5652
- const program$7 = getProgram();
5814
+ const program$9 = getProgram();
5653
5815
  typesCommand.command("generate").description("Generate types d.ts for your component schemas").option(
5654
5816
  "--filename <name>",
5655
5817
  "Base file name for all component types when generating a single declarations file (e.g. components.d.ts). Ignored when using --separate-files."
5656
5818
  ).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) => {
5657
5819
  konsola.title(`${commands.TYPES}`, colorPalette.TYPES, "Generating types...");
5658
- const verbose = program$7.opts().verbose;
5820
+ const verbose = program$9.opts().verbose;
5659
5821
  const { space, path } = typesCommand.opts();
5660
5822
  const spinner = new Spinner({
5661
5823
  verbose: !isVitest
@@ -5708,8 +5870,8 @@ typesCommand.command("generate").description("Generate types d.ts for your compo
5708
5870
  }
5709
5871
  });
5710
5872
 
5711
- const program$6 = getProgram();
5712
- 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");
5873
+ const program$8 = getProgram();
5874
+ 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");
5713
5875
 
5714
5876
  async function fetchAllPages(fetchFunction, extractDataFunction, page = 1, collectedItems = []) {
5715
5877
  const { data, response } = await fetchFunction(page);
@@ -5815,10 +5977,10 @@ const saveDatasourcesToFiles = async (space, datasources, options) => {
5815
5977
  }
5816
5978
  };
5817
5979
 
5818
- const program$5 = getProgram();
5980
+ const program$7 = getProgram();
5819
5981
  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) => {
5820
5982
  konsola.title(`${commands.DATASOURCES}`, colorPalette.DATASOURCES, datasourceName ? `Pulling datasource ${datasourceName}...` : "Pulling datasources...");
5821
- const verbose = program$5.opts().verbose;
5983
+ const verbose = program$7.opts().verbose;
5822
5984
  const { space, path } = datasourcesCommand.opts();
5823
5985
  const {
5824
5986
  separateFiles = false,
@@ -5896,10 +6058,10 @@ datasourcesCommand.command("pull [datasourceName]").option("-f, --filename <file
5896
6058
  }
5897
6059
  });
5898
6060
 
5899
- const program$4 = getProgram();
6061
+ const program$6 = getProgram();
5900
6062
  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) => {
5901
6063
  konsola.title(`${commands.DATASOURCES}`, colorPalette.DATASOURCES, datasourceName ? `Pushing datasource ${datasourceName}...` : "Pushing datasources...");
5902
- const verbose = program$4.opts().verbose;
6064
+ const verbose = program$6.opts().verbose;
5903
6065
  const { space, path } = datasourcesCommand.opts();
5904
6066
  const { filter } = options;
5905
6067
  const fromSpace = options.from || space;
@@ -6257,13 +6419,31 @@ function showNextSteps(technologyTemplate, finalProjectPath) {
6257
6419
  `);
6258
6420
  konsola.info(`Or check the dedicated guide at: ${chalk.hex(colorPalette.PRIMARY)(`https://www.storyblok.com/docs/guides/${technologyTemplate}`)}`);
6259
6421
  }
6260
- const program$3 = getProgram();
6261
- 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(
6422
+ async function promptForLogin(verbose) {
6423
+ try {
6424
+ konsola.br();
6425
+ const shouldLogin = await confirm({
6426
+ message: "Would you like to login now?",
6427
+ default: true
6428
+ });
6429
+ if (!shouldLogin) {
6430
+ konsola.warn('Login cancelled. You can login later using the "storyblok login" command.');
6431
+ return null;
6432
+ }
6433
+ return await performInteractiveLogin({ verbose, showWelcomeMessage: true });
6434
+ } catch (error) {
6435
+ konsola.br();
6436
+ handleError(error, verbose);
6437
+ return null;
6438
+ }
6439
+ }
6440
+ const program$5 = getProgram();
6441
+ 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(
6262
6442
  "-r, --region <region>",
6263
6443
  `The region to apply to the generated project template (does not affect space creation).`
6264
6444
  ).action(async (projectPath, options) => {
6265
6445
  konsola.title(`${commands.CREATE}`, colorPalette.CREATE);
6266
- const verbose = program$3.opts().verbose;
6446
+ const verbose = program$5.opts().verbose;
6267
6447
  const { template, blueprint, token } = options;
6268
6448
  if (options.region && !isRegion(options.region)) {
6269
6449
  handleError(new CommandError(`The provided region: ${options.region} is not valid. Please use one of the following values: ${Object.values(regions).join(" | ")}`));
@@ -6278,20 +6458,38 @@ program$3.command(`${commands.CREATE} [project-path]`).alias("c").description(`S
6278
6458
  }
6279
6459
  const { state, initializeSession } = session();
6280
6460
  await initializeSession();
6281
- if (!requireAuthentication(state, verbose)) {
6282
- return;
6283
- }
6284
- const { password, region } = state;
6285
- if (options.region && options.region !== region && !options.skipSpace && !token) {
6286
- 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.`));
6287
- return;
6461
+ let password;
6462
+ let region;
6463
+ if (state.region) {
6464
+ region = state.region;
6465
+ }
6466
+ if (!token && !options.skipSpace) {
6467
+ if (!requireAuthentication(state, verbose)) {
6468
+ const loginResult = await promptForLogin(verbose);
6469
+ if (!loginResult) {
6470
+ return;
6471
+ }
6472
+ await initializeSession();
6473
+ }
6474
+ const authenticatedState = state;
6475
+ password = authenticatedState.password;
6476
+ region = authenticatedState.region;
6477
+ if (options.region && options.region !== region) {
6478
+ 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.`));
6479
+ return;
6480
+ }
6481
+ mapiClient({
6482
+ token: {
6483
+ accessToken: password
6484
+ },
6485
+ region
6486
+ });
6487
+ } else if (state.isLoggedIn && state.password) {
6488
+ password = state.password;
6489
+ if (state.region) {
6490
+ region = state.region;
6491
+ }
6288
6492
  }
6289
- mapiClient({
6290
- token: {
6291
- accessToken: password
6292
- },
6293
- region
6294
- });
6295
6493
  const spinnerBlueprints = new Spinner({
6296
6494
  verbose: !isVitest
6297
6495
  });
@@ -6374,10 +6572,26 @@ program$3.command(`${commands.CREATE} [project-path]`).alias("c").description(`S
6374
6572
  throw new Error("User data is undefined");
6375
6573
  }
6376
6574
  userData = user;
6377
- } catch (error) {
6378
- konsola.error("Failed to fetch user info. Please login again.", error);
6379
- konsola.br();
6380
- return;
6575
+ } catch {
6576
+ konsola.error("Failed to fetch user info. Your session may have expired.");
6577
+ const loginResult = await promptForLogin(verbose);
6578
+ if (!loginResult) {
6579
+ konsola.br();
6580
+ return;
6581
+ }
6582
+ await initializeSession();
6583
+ const { password: newPassword, region: newRegion } = session().state;
6584
+ try {
6585
+ const user = await getUser(newPassword, newRegion);
6586
+ if (!user) {
6587
+ throw new Error("User data is undefined");
6588
+ }
6589
+ userData = user;
6590
+ } catch (retryError) {
6591
+ konsola.error("Failed to fetch user info after login.", retryError);
6592
+ konsola.br();
6593
+ return;
6594
+ }
6381
6595
  }
6382
6596
  const choices = [
6383
6597
  { name: "My personal account", value: "personal" }
@@ -6455,13 +6669,13 @@ program$3.command(`${commands.CREATE} [project-path]`).alias("c").description(`S
6455
6669
  konsola.br();
6456
6670
  });
6457
6671
 
6458
- const program$2 = getProgram();
6459
- 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'.");
6672
+ const program$4 = getProgram();
6673
+ 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'.");
6460
6674
 
6461
6675
  logsCommand.command("list").description("List logs").action(async () => {
6462
6676
  const { space, path } = logsCommand.opts();
6463
6677
  const ui = getUI();
6464
- const logsPath = resolveCommandPath(directories.log, space, path);
6678
+ const logsPath = resolveCommandPath(directories.logs, space, path);
6465
6679
  const logFiles = FileTransport.listLogFiles(logsPath);
6466
6680
  if (logFiles.length === 0) {
6467
6681
  ui.info(`No logs found for space "${space}".`);
@@ -6474,18 +6688,18 @@ logsCommand.command("list").description("List logs").action(async () => {
6474
6688
  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 }) => {
6475
6689
  const { space, path } = logsCommand.opts();
6476
6690
  const ui = getUI();
6477
- const logsPath = resolveCommandPath(directories.log, space, path);
6691
+ const logsPath = resolveCommandPath(directories.logs, space, path);
6478
6692
  const deletedFilesCount = FileTransport.pruneLogFiles(logsPath, keep);
6479
6693
  ui.info(`Deleted ${deletedFilesCount} log file${deletedFilesCount === 1 ? "" : "s"}`);
6480
6694
  });
6481
6695
 
6482
- const program$1 = getProgram();
6483
- 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'.");
6696
+ const program$3 = getProgram();
6697
+ 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'.");
6484
6698
 
6485
6699
  reportsCommand.command("list").description("List reports").action(async () => {
6486
6700
  const { space, path } = reportsCommand.opts();
6487
6701
  const ui = getUI();
6488
- const reportsPath = resolveCommandPath(directories.report, space, path);
6702
+ const reportsPath = resolveCommandPath(directories.reports, space, path);
6489
6703
  const reportFiles = Reporter.listReportFiles(reportsPath, ".jsonl");
6490
6704
  if (reportFiles.length === 0) {
6491
6705
  ui.info(`No reports found for space "${space}".`);
@@ -6498,11 +6712,2248 @@ reportsCommand.command("list").description("List reports").action(async () => {
6498
6712
  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 }) => {
6499
6713
  const { space, path } = reportsCommand.opts();
6500
6714
  const ui = getUI();
6501
- const reportsPath = resolveCommandPath(directories.report, space, path);
6715
+ const reportsPath = resolveCommandPath(directories.reports, space, path);
6502
6716
  const deletedFilesCount = Reporter.pruneReportFiles(reportsPath, keep, ".jsonl");
6503
6717
  ui.info(`Deleted ${deletedFilesCount} report file${deletedFilesCount === 1 ? "" : "s"}`);
6504
6718
  });
6505
6719
 
6720
+ const program$2 = getProgram();
6721
+ 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)");
6722
+
6723
+ const fetchAssets = async ({ spaceId, params }) => {
6724
+ try {
6725
+ const client = mapiClient();
6726
+ const { data, response } = await client.assets.list({
6727
+ path: {
6728
+ space_id: spaceId
6729
+ },
6730
+ query: {
6731
+ ...params,
6732
+ per_page: params?.per_page || 100,
6733
+ page: params?.page || 1
6734
+ },
6735
+ throwOnError: true
6736
+ });
6737
+ const assets = (data?.assets || []).filter((asset) => Boolean(asset?.id && asset?.filename));
6738
+ return {
6739
+ assets,
6740
+ headers: response.headers
6741
+ };
6742
+ } catch (maybeError) {
6743
+ handleAPIError("pull_assets", toError(maybeError));
6744
+ throw maybeError;
6745
+ }
6746
+ };
6747
+ const downloadFile = async (filename) => {
6748
+ const response = await fetch(filename);
6749
+ if (!response.ok) {
6750
+ throw new Error(`Failed to download ${filename}`);
6751
+ }
6752
+ return response.arrayBuffer();
6753
+ };
6754
+ const getSignedAssetUrl = async (filename, assetToken, region) => {
6755
+ try {
6756
+ const client = new Storyblok({
6757
+ accessToken: assetToken,
6758
+ region: region || "eu"
6759
+ });
6760
+ const response = await client.get("cdn/assets/me", {
6761
+ filename
6762
+ });
6763
+ return response.data.asset.signed_url;
6764
+ } catch (maybeError) {
6765
+ handleAPIError("pull_asset", toError(maybeError));
6766
+ throw maybeError;
6767
+ }
6768
+ };
6769
+ const fetchAssetFolders = async ({ spaceId }) => {
6770
+ try {
6771
+ const client = mapiClient();
6772
+ const { data, response } = await client.assetFolders.list({
6773
+ path: {
6774
+ space_id: spaceId
6775
+ },
6776
+ throwOnError: true
6777
+ });
6778
+ return {
6779
+ asset_folders: data.asset_folders || [],
6780
+ headers: response.headers
6781
+ };
6782
+ } catch (maybeError) {
6783
+ handleAPIError("pull_asset_folders", toError(maybeError));
6784
+ throw maybeError;
6785
+ }
6786
+ };
6787
+ const createAssetFolder = async (folder, {
6788
+ spaceId
6789
+ }) => {
6790
+ try {
6791
+ const client = mapiClient();
6792
+ const { data } = await client.assetFolders.create({
6793
+ path: {
6794
+ space_id: spaceId
6795
+ },
6796
+ body: { asset_folder: folder },
6797
+ throwOnError: true
6798
+ });
6799
+ const { asset_folder } = data;
6800
+ if (!asset_folder) {
6801
+ throw new Error("Failed to create asset folder");
6802
+ }
6803
+ return asset_folder;
6804
+ } catch (maybeError) {
6805
+ handleAPIError("push_asset_folder", toError(maybeError));
6806
+ throw maybeError;
6807
+ }
6808
+ };
6809
+ const updateAssetFolder = async (folder, {
6810
+ spaceId
6811
+ }) => {
6812
+ try {
6813
+ const client = mapiClient();
6814
+ await client.assetFolders.update({
6815
+ path: {
6816
+ asset_folder_id: folder.id,
6817
+ space_id: spaceId
6818
+ },
6819
+ body: { asset_folder: folder },
6820
+ throwOnError: true
6821
+ });
6822
+ return folder;
6823
+ } catch (maybeError) {
6824
+ handleAPIError("push_asset_folder", toError(maybeError));
6825
+ throw maybeError;
6826
+ }
6827
+ };
6828
+ const requestAssetUpload = async (asset, { spaceId }) => {
6829
+ try {
6830
+ const client = mapiClient();
6831
+ const { data } = await client.assets.upload({
6832
+ path: {
6833
+ space_id: spaceId
6834
+ },
6835
+ body: {
6836
+ // @ts-expect-error Our types are wrong, id is optional but allowed.
6837
+ id: asset.id,
6838
+ filename: asset.short_filename,
6839
+ asset_folder_id: asset.asset_folder_id ?? void 0,
6840
+ is_private: asset.is_private
6841
+ },
6842
+ throwOnError: true
6843
+ });
6844
+ const signedUpload = data;
6845
+ if (!signedUpload?.id || !signedUpload?.post_url || !signedUpload?.fields) {
6846
+ throw new Error("Failed to request signed upload!");
6847
+ }
6848
+ return signedUpload;
6849
+ } catch (maybeError) {
6850
+ handleAPIError("push_asset_sign", toError(maybeError));
6851
+ throw maybeError;
6852
+ }
6853
+ };
6854
+ const uploadAssetToS3 = async (asset, fileBuffer, {
6855
+ signedUpload
6856
+ }) => {
6857
+ if (!signedUpload?.id || !signedUpload?.post_url || !signedUpload?.fields) {
6858
+ throw new Error("Invalid signed upload!");
6859
+ }
6860
+ const formData = new FormData();
6861
+ for (const [key, value] of Object.entries(signedUpload.fields)) {
6862
+ formData.append(key, value);
6863
+ }
6864
+ const contentType = signedUpload.fields["Content-Type"] || "application/octet-stream";
6865
+ formData.append("file", new File([Buffer.from(fileBuffer)], asset.short_filename, { type: contentType }));
6866
+ const response = await fetch(signedUpload.post_url, {
6867
+ method: "POST",
6868
+ body: formData
6869
+ });
6870
+ if (!response.ok) {
6871
+ handleAPIError("push_asset_upload", new Error("Failed to upload asset to storage"));
6872
+ return;
6873
+ }
6874
+ return response;
6875
+ };
6876
+ const finishAssetUpload = async (assetId, {
6877
+ spaceId
6878
+ }) => {
6879
+ try {
6880
+ const client = mapiClient();
6881
+ await client.assets.finalize({
6882
+ path: {
6883
+ space_id: spaceId,
6884
+ signed_response_object_id: String(assetId)
6885
+ },
6886
+ throwOnError: true
6887
+ });
6888
+ const { data } = await client.assets.get({
6889
+ path: {
6890
+ space_id: spaceId,
6891
+ asset_id: assetId
6892
+ },
6893
+ throwOnError: true
6894
+ });
6895
+ return data;
6896
+ } catch (maybeError) {
6897
+ handleAPIError("push_asset_finish", toError(maybeError));
6898
+ throw maybeError;
6899
+ }
6900
+ };
6901
+ const uploadAsset = async (asset, fileBuffer, { spaceId }) => {
6902
+ const signed = await requestAssetUpload(asset, {
6903
+ spaceId
6904
+ });
6905
+ const uploadResponse = await uploadAssetToS3(asset, fileBuffer, {
6906
+ signedUpload: signed
6907
+ });
6908
+ if (!uploadResponse?.ok) {
6909
+ throw new Error("Error uploading asset to S3!");
6910
+ }
6911
+ return finishAssetUpload(Number(signed.id), {
6912
+ spaceId
6913
+ });
6914
+ };
6915
+ const sha256 = (data) => {
6916
+ const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
6917
+ return createHash("sha256").update(buffer).digest("hex");
6918
+ };
6919
+ const downloadAssetFile = async (asset, options) => {
6920
+ let url = asset.filename;
6921
+ if (asset.is_private) {
6922
+ if (!options.assetToken) {
6923
+ throw new Error(`Asset ${asset.filename} is private but no asset token was provided. Use --asset-token to provide a token.`);
6924
+ }
6925
+ url = await getSignedAssetUrl(asset.filename, options.assetToken, options.region);
6926
+ }
6927
+ return downloadFile(url);
6928
+ };
6929
+ const updateAsset = async (asset, fileBuffer, {
6930
+ spaceId
6931
+ }) => {
6932
+ try {
6933
+ const assetWithNewFilename = { ...asset };
6934
+ if (fileBuffer) {
6935
+ const uploadedAsset = await uploadAsset({
6936
+ id: asset.id,
6937
+ asset_folder_id: asset.asset_folder_id,
6938
+ short_filename: asset.short_filename || basename(asset.filename)
6939
+ }, fileBuffer, { spaceId });
6940
+ assetWithNewFilename.filename = uploadedAsset.filename;
6941
+ assetWithNewFilename.short_filename = uploadedAsset.short_filename;
6942
+ }
6943
+ const client = mapiClient();
6944
+ await client.assets.update({
6945
+ path: {
6946
+ space_id: spaceId,
6947
+ asset_id: assetWithNewFilename.id
6948
+ },
6949
+ body: {
6950
+ asset: assetWithNewFilename
6951
+ },
6952
+ throwOnError: true
6953
+ });
6954
+ return assetWithNewFilename;
6955
+ } catch (maybeError) {
6956
+ handleAPIError("push_asset_update", toError(maybeError));
6957
+ throw maybeError;
6958
+ }
6959
+ };
6960
+ const createAsset = async (asset, fileBuffer, { spaceId }) => {
6961
+ const createdAsset = await uploadAsset({
6962
+ asset_folder_id: asset.asset_folder_id,
6963
+ short_filename: asset.short_filename,
6964
+ alt: asset.alt,
6965
+ title: asset.title,
6966
+ copyright: asset.copyright,
6967
+ source: asset.source,
6968
+ is_private: asset.is_private
6969
+ }, fileBuffer, { spaceId });
6970
+ const hasUpdatableMetadata = Boolean(
6971
+ asset.alt || asset.title || asset.copyright || asset.source || asset.is_private || asset.meta_data && Object.keys(asset.meta_data).length > 0
6972
+ );
6973
+ if (hasUpdatableMetadata) {
6974
+ const updatedAsset = await updateAsset({
6975
+ ...asset,
6976
+ id: createdAsset.id,
6977
+ filename: createdAsset.filename
6978
+ }, null, {
6979
+ spaceId
6980
+ });
6981
+ if (!updatedAsset) {
6982
+ throw new Error("Updating the created asset failed!");
6983
+ }
6984
+ return updatedAsset;
6985
+ }
6986
+ return createdAsset;
6987
+ };
6988
+
6989
+ const parseAssetData = (raw) => {
6990
+ if (!raw) {
6991
+ return {};
6992
+ }
6993
+ try {
6994
+ const parsed = JSON.parse(raw);
6995
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
6996
+ throw new Error("Asset data must be a JSON object.");
6997
+ }
6998
+ return parsed;
6999
+ } catch (maybeError) {
7000
+ throw new Error(`Invalid --data JSON: ${toError(maybeError).message}`);
7001
+ }
7002
+ };
7003
+ const getSidecarFilename = (assetBinaryPath) => {
7004
+ return join(dirname$1(assetBinaryPath), `${basename(assetBinaryPath, extname(assetBinaryPath))}.json`);
7005
+ };
7006
+ const loadSidecarAssetData = async (assetBinaryPath) => {
7007
+ const sidecarPath = getSidecarFilename(assetBinaryPath);
7008
+ try {
7009
+ const sidecarRaw = await readFile$1(sidecarPath, "utf8");
7010
+ try {
7011
+ return parseAssetData(sidecarRaw);
7012
+ } catch (maybeError) {
7013
+ throw new Error(`Invalid sidecar JSON: ${toError(maybeError).message}`);
7014
+ }
7015
+ } catch (maybeError) {
7016
+ const error = toError(maybeError);
7017
+ if (error.code === "ENOENT") {
7018
+ return {};
7019
+ }
7020
+ throw new Error(`Failed to read sidecar asset data: ${error.message}`);
7021
+ }
7022
+ };
7023
+ const isRemoteSource = (assetBinaryPath) => {
7024
+ try {
7025
+ const url = new URL(assetBinaryPath);
7026
+ return url.protocol === "http:" || url.protocol === "https:";
7027
+ } catch {
7028
+ return false;
7029
+ }
7030
+ };
7031
+ const isValidManifestEntry = (entry) => Boolean(typeof entry.old_id === "number" && typeof entry.new_id === "number" && entry.old_filename && entry.new_filename);
7032
+ const loadAssetMap = async (manifestFile) => {
7033
+ const manifest = await loadManifest(manifestFile);
7034
+ return new Map([
7035
+ ...manifest.filter(isValidManifestEntry).map((e) => [
7036
+ Number(e.old_id),
7037
+ {
7038
+ old: { id: Number(e.old_id), filename: e.old_filename || "" },
7039
+ new: { id: Number(e.new_id), filename: e.new_filename || "" }
7040
+ }
7041
+ ])
7042
+ ]);
7043
+ };
7044
+ const loadAssetFolderMap = async (manifestFile) => {
7045
+ const manifest = await loadManifest(manifestFile);
7046
+ return new Map(manifest.map((e) => [Number(e.old_id), Number(e.new_id)]));
7047
+ };
7048
+ const getAssetNameAndExt = (asset) => {
7049
+ const filename = asset.short_filename || (asset.filename ? basename(asset.filename) : void 0);
7050
+ if (!filename) {
7051
+ throw new Error(`Filename for asset with id ${asset.id} could not be determined!`);
7052
+ }
7053
+ const ext = extname(filename);
7054
+ const name = sanitizeFilename(filename.replace(ext, ""));
7055
+ return { name, ext };
7056
+ };
7057
+ const getAssetFilename = (asset) => {
7058
+ const { name } = getAssetNameAndExt(asset);
7059
+ return `${name}_${asset.id}.json`;
7060
+ };
7061
+ const getAssetBinaryFilename = (asset) => {
7062
+ const { name, ext } = getAssetNameAndExt(asset);
7063
+ return `${name}_${asset.id}${ext}`;
7064
+ };
7065
+ const getFolderFilename = (folder) => {
7066
+ const sanitizedName = sanitizeFilename(folder.name || "");
7067
+ const baseName = sanitizedName || folder.uuid;
7068
+ return `${baseName}_${folder.uuid}.json`;
7069
+ };
7070
+
7071
+ const apiConcurrencyLock$1 = new Sema(12);
7072
+ const fetchAssetsStream = ({
7073
+ spaceId,
7074
+ params = {},
7075
+ setTotalAssets,
7076
+ setTotalPages,
7077
+ onIncrement,
7078
+ onPageSuccess,
7079
+ onPageError
7080
+ }) => {
7081
+ const listGenerator = async function* assetListIterator() {
7082
+ let perPage = 100;
7083
+ let page = 1;
7084
+ let totalPages = 1;
7085
+ setTotalPages?.(totalPages);
7086
+ while (page <= totalPages) {
7087
+ try {
7088
+ const result = await fetchAssets({
7089
+ spaceId,
7090
+ params: {
7091
+ ...params,
7092
+ per_page: perPage,
7093
+ page
7094
+ }
7095
+ });
7096
+ const { headers, assets } = result;
7097
+ const total = Number(headers.get("Total"));
7098
+ perPage = Number(headers.get("Per-Page")) || perPage;
7099
+ totalPages = Math.max(1, Math.ceil(total / perPage));
7100
+ setTotalAssets?.(total);
7101
+ setTotalPages?.(totalPages);
7102
+ onPageSuccess?.(page, totalPages);
7103
+ for (const asset of assets) {
7104
+ yield asset;
7105
+ }
7106
+ page += 1;
7107
+ } catch (maybeError) {
7108
+ onPageError?.(toError(maybeError), page, totalPages);
7109
+ break;
7110
+ } finally {
7111
+ onIncrement?.();
7112
+ }
7113
+ }
7114
+ };
7115
+ return Readable.from(listGenerator());
7116
+ };
7117
+ const downloadAssetStream = ({
7118
+ assetToken,
7119
+ region,
7120
+ onIncrement,
7121
+ onAssetSuccess,
7122
+ onAssetError
7123
+ }) => {
7124
+ const processing = /* @__PURE__ */ new Set();
7125
+ return new Transform({
7126
+ objectMode: true,
7127
+ async transform(asset, _encoding, callback) {
7128
+ await apiConcurrencyLock$1.acquire();
7129
+ const task = downloadAssetFile(asset, { assetToken, region }).then((fileBuffer) => {
7130
+ if (!fileBuffer) {
7131
+ throw new Error("Invalid asset file!");
7132
+ }
7133
+ onAssetSuccess?.(asset);
7134
+ this.push({ asset, fileBuffer });
7135
+ }).catch((maybeError) => {
7136
+ onAssetError?.(toError(maybeError), asset);
7137
+ }).finally(() => {
7138
+ onIncrement?.();
7139
+ apiConcurrencyLock$1.release();
7140
+ processing.delete(task);
7141
+ });
7142
+ processing.add(task);
7143
+ callback();
7144
+ },
7145
+ flush(callback) {
7146
+ Promise.allSettled(processing).finally(() => callback());
7147
+ }
7148
+ });
7149
+ };
7150
+ const makeWriteAssetFSTransport = ({ directoryPath }) => async (asset, fileBuffer) => {
7151
+ const assetBinaryPath = join(directoryPath, getAssetBinaryFilename(asset));
7152
+ const assetPath = join(directoryPath, getAssetFilename(asset));
7153
+ await saveToFile(assetBinaryPath, Buffer.from(fileBuffer));
7154
+ await saveToFile(assetPath, JSON.stringify(asset, null, 2));
7155
+ return asset;
7156
+ };
7157
+ const writeAssetStream = ({
7158
+ writeAsset,
7159
+ onIncrement,
7160
+ onAssetSuccess,
7161
+ onAssetError
7162
+ }) => {
7163
+ const processing = /* @__PURE__ */ new Set();
7164
+ return new Writable({
7165
+ objectMode: true,
7166
+ async write(payload, _encoding, callback) {
7167
+ await apiConcurrencyLock$1.acquire();
7168
+ const task = (async () => {
7169
+ try {
7170
+ await writeAsset(payload.asset, payload.fileBuffer);
7171
+ onAssetSuccess?.(payload.asset);
7172
+ } catch (maybeError) {
7173
+ onAssetError?.(toError(maybeError), payload.asset);
7174
+ }
7175
+ })();
7176
+ processing.add(task);
7177
+ task.finally(() => {
7178
+ onIncrement?.();
7179
+ apiConcurrencyLock$1.release();
7180
+ processing.delete(task);
7181
+ });
7182
+ callback();
7183
+ },
7184
+ final(callback) {
7185
+ Promise.all(processing).finally(() => callback());
7186
+ }
7187
+ });
7188
+ };
7189
+ const fetchAssetFoldersStream = ({
7190
+ spaceId,
7191
+ setTotalFolders,
7192
+ onSuccess,
7193
+ onError
7194
+ }) => {
7195
+ const listGenerator = async function* folderListIterator() {
7196
+ try {
7197
+ const result = await fetchAssetFolders({ spaceId });
7198
+ const { asset_folders } = result;
7199
+ const total = asset_folders.length;
7200
+ setTotalFolders?.(total);
7201
+ onSuccess?.(asset_folders);
7202
+ for (const folder of asset_folders) {
7203
+ yield folder;
7204
+ }
7205
+ } catch (maybeError) {
7206
+ onError?.(toError(maybeError));
7207
+ }
7208
+ };
7209
+ return Readable.from(listGenerator());
7210
+ };
7211
+ const makeWriteAssetFolderFSTransport = ({ directoryPath }) => async (folder) => {
7212
+ const filename = getFolderFilename(folder);
7213
+ await saveToFile(join(directoryPath, "folders", filename), JSON.stringify(folder, null, 2));
7214
+ return folder;
7215
+ };
7216
+ const writeAssetFolderStream = ({
7217
+ writeAssetFolder,
7218
+ onIncrement,
7219
+ onFolderSuccess,
7220
+ onFolderError
7221
+ }) => {
7222
+ const processing = /* @__PURE__ */ new Set();
7223
+ return new Writable({
7224
+ objectMode: true,
7225
+ async write(folder, _encoding, callback) {
7226
+ await apiConcurrencyLock$1.acquire();
7227
+ const task = (async () => {
7228
+ try {
7229
+ await writeAssetFolder(folder);
7230
+ onFolderSuccess?.(folder);
7231
+ } catch (maybeError) {
7232
+ onFolderError?.(toError(maybeError), folder);
7233
+ }
7234
+ })();
7235
+ processing.add(task);
7236
+ task.finally(() => {
7237
+ onIncrement?.();
7238
+ apiConcurrencyLock$1.release();
7239
+ processing.delete(task);
7240
+ });
7241
+ callback();
7242
+ },
7243
+ final(callback) {
7244
+ Promise.all(processing).finally(() => callback());
7245
+ }
7246
+ });
7247
+ };
7248
+ const readLocalAssetFoldersStream = ({
7249
+ directoryPath,
7250
+ setTotalFolders,
7251
+ onFolderError
7252
+ }) => {
7253
+ const iterator = async function* readFolders() {
7254
+ try {
7255
+ const files = await readdir(directoryPath);
7256
+ const jsonFiles = new Set(files.filter((file) => file.endsWith(".json")));
7257
+ setTotalFolders?.(jsonFiles.size);
7258
+ const processed = /* @__PURE__ */ new Set();
7259
+ let maxIterations = jsonFiles.size * jsonFiles.size;
7260
+ while (jsonFiles.size > 0 && maxIterations-- > 0) {
7261
+ for (const file of jsonFiles) {
7262
+ try {
7263
+ const filePath = join(directoryPath, file);
7264
+ const content = await readFile$1(filePath, "utf8");
7265
+ const folder = JSON.parse(content);
7266
+ jsonFiles.delete(file);
7267
+ if (!folder.parent_id || processed.has(folder.parent_id)) {
7268
+ processed.add(folder.id);
7269
+ yield {
7270
+ folder,
7271
+ context: {
7272
+ localFilePath: filePath
7273
+ }
7274
+ };
7275
+ } else {
7276
+ jsonFiles.add(file);
7277
+ }
7278
+ } catch (maybeError) {
7279
+ onFolderError?.(toError(maybeError));
7280
+ }
7281
+ }
7282
+ }
7283
+ if (jsonFiles.size > 0) {
7284
+ onFolderError?.(new Error(`Unable to resolve folder dependencies for: ${[...jsonFiles].join(", ")}`));
7285
+ }
7286
+ } catch (maybeError) {
7287
+ const error = toError(maybeError);
7288
+ if ("code" in error && error.code === "ENOENT") {
7289
+ return;
7290
+ }
7291
+ onFolderError?.(error);
7292
+ }
7293
+ };
7294
+ return Readable.from(iterator());
7295
+ };
7296
+ const makeCreateAssetFolderAPITransport = ({ spaceId }) => (folder) => createAssetFolder({
7297
+ name: folder.name,
7298
+ parent_id: folder.parent_id ?? void 0
7299
+ }, {
7300
+ spaceId
7301
+ });
7302
+ const makeUpdateAssetFolderAPITransport = ({ spaceId }) => (folder) => updateAssetFolder(folder, { spaceId });
7303
+ const makeGetAssetFolderAPITransport = ({ spaceId }) => async (folderId) => {
7304
+ const { data, response } = await mapiClient().assetFolders.get({
7305
+ path: {
7306
+ asset_folder_id: folderId,
7307
+ space_id: spaceId
7308
+ }
7309
+ });
7310
+ if (!response.ok && response.status !== 404) {
7311
+ handleAPIError("pull_asset_folder", new FetchError(response.statusText, response));
7312
+ }
7313
+ return data?.asset_folder;
7314
+ };
7315
+ const makeCleanupAssetFolderFSTransport = () => async ({ localFilePath }) => await unlink(localFilePath);
7316
+ const upsertAssetFolderStream = ({
7317
+ transports,
7318
+ maps,
7319
+ onIncrement,
7320
+ onFolderSuccess,
7321
+ onFolderError
7322
+ }) => {
7323
+ return new Writable({
7324
+ objectMode: true,
7325
+ async write({ folder, context }, _encoding, callback) {
7326
+ try {
7327
+ const remoteParentId = folder.parent_id && (maps.assetFolders.get(folder.parent_id) || folder.parent_id);
7328
+ const remoteFolderId = maps.assetFolders.get(folder.id) || folder.id;
7329
+ const upsertFolder = {
7330
+ ...folder,
7331
+ id: remoteFolderId,
7332
+ parent_id: remoteParentId
7333
+ };
7334
+ const existingRemoteFolder = await transports.getAssetFolder(remoteFolderId);
7335
+ const newRemoteFolder = existingRemoteFolder ? await transports.updateAssetFolder({ ...upsertFolder, parent_id: remoteParentId !== null ? remoteParentId : void 0 }) : await transports.createAssetFolder(upsertFolder);
7336
+ if (!maps.assetFolders.get(folder.id)) {
7337
+ await transports.appendAssetFolderManifest(folder, newRemoteFolder);
7338
+ }
7339
+ await transports.cleanupAssetFolder?.({ localFilePath: context.localFilePath });
7340
+ onFolderSuccess?.(folder, newRemoteFolder);
7341
+ } catch (maybeError) {
7342
+ onFolderError?.(toError(maybeError), folder);
7343
+ } finally {
7344
+ onIncrement?.();
7345
+ callback();
7346
+ }
7347
+ }
7348
+ });
7349
+ };
7350
+ const readLocalAssetsStream = ({
7351
+ directoryPath,
7352
+ setTotalAssets,
7353
+ onAssetError
7354
+ }) => {
7355
+ const iterator = async function* readAssets() {
7356
+ try {
7357
+ const files = await readdir(directoryPath);
7358
+ const metadataFiles = files.filter((file) => file.endsWith(".json") && file !== "manifest.jsonl");
7359
+ setTotalAssets?.(metadataFiles.length);
7360
+ for (const file of metadataFiles) {
7361
+ const filePath = join(directoryPath, file);
7362
+ try {
7363
+ const statResult = await stat(filePath);
7364
+ if (!statResult.isFile()) {
7365
+ continue;
7366
+ }
7367
+ const metadataContent = await readFile$1(filePath, "utf8");
7368
+ const assetRaw = JSON.parse(metadataContent);
7369
+ const asset = {
7370
+ ...assetRaw,
7371
+ short_filename: assetRaw.short_filename || basename(assetRaw.filename)
7372
+ };
7373
+ const baseName = parse(file).name;
7374
+ const extFromMetadata = extname(asset.short_filename || asset.filename) || "";
7375
+ const assetBinaryPath = join(directoryPath, `${baseName}${extFromMetadata}`);
7376
+ const fileBuffer = await readFile$1(assetBinaryPath);
7377
+ yield {
7378
+ asset,
7379
+ context: {
7380
+ fileBuffer,
7381
+ assetBinaryPath,
7382
+ assetPath: filePath
7383
+ }
7384
+ };
7385
+ } catch (maybeError) {
7386
+ onAssetError?.(toError(maybeError));
7387
+ }
7388
+ }
7389
+ } catch (maybeError) {
7390
+ onAssetError?.(toError(maybeError));
7391
+ }
7392
+ };
7393
+ return Readable.from(iterator());
7394
+ };
7395
+ const readSingleAssetStream = ({
7396
+ asset,
7397
+ assetBinaryPath,
7398
+ onAssetError
7399
+ }) => {
7400
+ const iterator = async function* readSingleAsset() {
7401
+ try {
7402
+ if (!isRemoteSource(assetBinaryPath) && !await fileExists(assetBinaryPath)) {
7403
+ throw new Error("Asset path must point to a file.");
7404
+ }
7405
+ const fileBuffer = isRemoteSource(assetBinaryPath) ? await downloadFile(assetBinaryPath) : await readFile$1(assetBinaryPath);
7406
+ yield {
7407
+ asset,
7408
+ context: {
7409
+ fileBuffer,
7410
+ assetBinaryPath
7411
+ }
7412
+ };
7413
+ } catch (maybeError) {
7414
+ onAssetError?.(toError(maybeError));
7415
+ }
7416
+ };
7417
+ return Readable.from(iterator());
7418
+ };
7419
+ const makeCreateAssetAPITransport = ({ spaceId }) => (asset, fileBuffer) => createAsset(asset, fileBuffer, { spaceId });
7420
+ const makeUpdateAssetAPITransport = ({
7421
+ spaceId
7422
+ }) => (asset, fileBuffer) => updateAsset(asset, fileBuffer, {
7423
+ spaceId
7424
+ });
7425
+ const makeAppendAssetManifestFSTransport = ({ manifestFile }) => async (localAsset, remoteAsset) => {
7426
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
7427
+ await appendToFile(manifestFile, JSON.stringify({
7428
+ old_id: localAsset.id,
7429
+ new_id: remoteAsset.id,
7430
+ old_filename: localAsset.filename,
7431
+ new_filename: remoteAsset.filename,
7432
+ created_at: createdAt
7433
+ }));
7434
+ };
7435
+ const makeAppendAssetFolderManifestFSTransport = ({ manifestFile }) => async (localFolder, remoteFolder) => {
7436
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
7437
+ await appendToFile(manifestFile, JSON.stringify({
7438
+ old_id: localFolder.id,
7439
+ new_id: remoteFolder.id,
7440
+ created_at: createdAt
7441
+ }));
7442
+ };
7443
+ const makeGetAssetAPITransport = ({ spaceId }) => async (assetId) => {
7444
+ const { data, response } = await mapiClient().assets.get({
7445
+ path: {
7446
+ space_id: spaceId,
7447
+ asset_id: assetId
7448
+ }
7449
+ });
7450
+ if (!response.ok && response.status !== 404) {
7451
+ handleAPIError("pull_asset", new FetchError(response.statusText, response));
7452
+ }
7453
+ if (data?.deleted_at) {
7454
+ return void 0;
7455
+ }
7456
+ return data;
7457
+ };
7458
+ const saveDelete = async (filePath) => {
7459
+ if (await fileExists(filePath)) {
7460
+ await unlink(filePath);
7461
+ }
7462
+ };
7463
+ const makeCleanupAssetFSTransport = () => async ({ assetBinaryPath, assetPath }) => {
7464
+ const assetOrSidecarPath = assetPath || getSidecarFilename(assetBinaryPath);
7465
+ await Promise.all([
7466
+ assetBinaryPath && saveDelete(assetBinaryPath),
7467
+ assetOrSidecarPath && saveDelete(assetOrSidecarPath)
7468
+ ]);
7469
+ };
7470
+ const hasId = (a) => {
7471
+ return !!a && typeof a === "object" && "id" in a && typeof a.id === "number";
7472
+ };
7473
+ const hasShortFilename = (a) => {
7474
+ return !!a && typeof a === "object" && "short_filename" in a && typeof a.short_filename === "string";
7475
+ };
7476
+ const isDataUnchanged = (localAsset, remoteAsset) => {
7477
+ if (localAsset.asset_folder_id !== remoteAsset.asset_folder_id) {
7478
+ return false;
7479
+ }
7480
+ if (localAsset.alt !== remoteAsset.alt || localAsset.title !== remoteAsset.title || localAsset.copyright !== remoteAsset.copyright || localAsset.source !== remoteAsset.source || localAsset.is_private !== remoteAsset.is_private) {
7481
+ return false;
7482
+ }
7483
+ const localAssetMetadataEntries = Object.entries(localAsset.meta_data || {});
7484
+ const remoteAssetMetadataEntries = Object.entries(remoteAsset.meta_data || {});
7485
+ if (localAssetMetadataEntries.length !== remoteAssetMetadataEntries.length) {
7486
+ return false;
7487
+ }
7488
+ const hasChanges = localAssetMetadataEntries.some(([k, v]) => remoteAsset.meta_data && remoteAsset.meta_data[k] !== v);
7489
+ return !hasChanges;
7490
+ };
7491
+ const isAssetUnchanged = async (localAsset, remoteAsset, localFileBuffer, downloadAssetFileTransport) => {
7492
+ const remoteFileBuffer = await downloadAssetFileTransport(remoteAsset);
7493
+ const isFileUnchanged = sha256(localFileBuffer) === sha256(remoteFileBuffer);
7494
+ if (!isFileUnchanged) {
7495
+ return false;
7496
+ }
7497
+ return isDataUnchanged(localAsset, remoteAsset);
7498
+ };
7499
+ const makeDownloadAssetFileTransport = ({
7500
+ assetToken,
7501
+ region
7502
+ }) => (asset) => downloadAssetFile(asset, { assetToken, region });
7503
+ const processAsset = async ({
7504
+ localAsset,
7505
+ fileBuffer,
7506
+ assetBinaryPath,
7507
+ assetPath,
7508
+ transports,
7509
+ maps
7510
+ }) => {
7511
+ const remoteFolderId = localAsset.asset_folder_id && (maps.assetFolders.get(localAsset.asset_folder_id) || localAsset.asset_folder_id);
7512
+ const remoteAssetId = hasId(localAsset) ? maps.assets.get(localAsset.id)?.new.id || localAsset.id : void 0;
7513
+ const remoteAsset = remoteAssetId ? await transports.getAsset(remoteAssetId) : null;
7514
+ let newRemoteAsset;
7515
+ let status;
7516
+ if (remoteAsset) {
7517
+ const updatePayload = {
7518
+ ...remoteAsset,
7519
+ ...localAsset,
7520
+ id: remoteAsset.id,
7521
+ asset_folder_id: remoteFolderId
7522
+ };
7523
+ const canSkip = await isAssetUnchanged(
7524
+ updatePayload,
7525
+ remoteAsset,
7526
+ fileBuffer,
7527
+ transports.downloadAssetFile
7528
+ );
7529
+ if (canSkip) {
7530
+ newRemoteAsset = remoteAsset;
7531
+ status = "skipped";
7532
+ } else {
7533
+ newRemoteAsset = await transports.updateAsset(updatePayload, fileBuffer);
7534
+ status = "updated";
7535
+ }
7536
+ } else if (hasShortFilename(localAsset)) {
7537
+ const createPayload = {
7538
+ ...localAsset,
7539
+ asset_folder_id: remoteFolderId
7540
+ };
7541
+ newRemoteAsset = await transports.createAsset(createPayload, fileBuffer);
7542
+ status = "created";
7543
+ } else {
7544
+ throw new Error("Could neither create nor update the asset: Missing ID and Filename");
7545
+ }
7546
+ if (hasId(localAsset)) {
7547
+ await transports.appendAssetManifest(localAsset, newRemoteAsset);
7548
+ }
7549
+ await transports.cleanupAsset?.({ assetBinaryPath, assetPath });
7550
+ return { status, remoteAsset: newRemoteAsset };
7551
+ };
7552
+ const upsertAssetStream = ({
7553
+ transports,
7554
+ maps,
7555
+ onIncrement,
7556
+ onAssetSuccess,
7557
+ onAssetSkipped,
7558
+ onAssetError
7559
+ }) => {
7560
+ const processing = /* @__PURE__ */ new Set();
7561
+ return new Writable({
7562
+ objectMode: true,
7563
+ async write({ asset: localAsset, context }, _encoding, callback) {
7564
+ await apiConcurrencyLock$1.acquire();
7565
+ const task = (async () => {
7566
+ try {
7567
+ const { status, remoteAsset } = await processAsset({
7568
+ localAsset,
7569
+ fileBuffer: context.fileBuffer,
7570
+ assetBinaryPath: context.assetBinaryPath,
7571
+ assetPath: context.assetPath,
7572
+ transports,
7573
+ maps
7574
+ });
7575
+ if (status === "skipped") {
7576
+ onAssetSkipped?.(localAsset, remoteAsset);
7577
+ } else {
7578
+ onAssetSuccess?.(localAsset, remoteAsset);
7579
+ }
7580
+ } catch (maybeError) {
7581
+ onAssetError?.(toError(maybeError), localAsset);
7582
+ }
7583
+ })();
7584
+ processing.add(task);
7585
+ task.finally(() => {
7586
+ onIncrement?.();
7587
+ apiConcurrencyLock$1.release();
7588
+ processing.delete(task);
7589
+ });
7590
+ callback();
7591
+ },
7592
+ final(callback) {
7593
+ Promise.all(processing).finally(() => callback());
7594
+ }
7595
+ });
7596
+ };
7597
+
7598
+ 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) => {
7599
+ const ui = getUI();
7600
+ const logger = getLogger();
7601
+ ui.title(`${commands.ASSETS}`, colorPalette.ASSETS, "Pulling assets...");
7602
+ logger.info("Pulling assets started");
7603
+ if (options.dryRun) {
7604
+ ui.warn(`DRY RUN MODE ENABLED: No changes will be made.
7605
+ `);
7606
+ logger.warn("Dry run mode enabled");
7607
+ }
7608
+ const { space, path: basePath, verbose } = command.optsWithGlobals();
7609
+ const assetToken = options.assetToken;
7610
+ const { state, initializeSession } = session();
7611
+ await initializeSession();
7612
+ if (!requireAuthentication(state, verbose)) {
7613
+ process.exitCode = 2;
7614
+ return;
7615
+ }
7616
+ if (!space) {
7617
+ handleError(new CommandError(`Please provide the space as argument --space YOUR_SPACE_ID.`), verbose);
7618
+ process.exitCode = 2;
7619
+ return;
7620
+ }
7621
+ const { password, region } = state;
7622
+ mapiClient({
7623
+ token: {
7624
+ accessToken: password
7625
+ },
7626
+ region
7627
+ });
7628
+ const summary = {
7629
+ folderResults: { total: 0, succeeded: 0, failed: 0 },
7630
+ fetchAssetPages: { total: 0, succeeded: 0, failed: 0 },
7631
+ fetchAssets: { total: 0, succeeded: 0, failed: 0 },
7632
+ save: { total: 0, succeeded: 0, failed: 0 }
7633
+ };
7634
+ let fatalError = false;
7635
+ try {
7636
+ const folderProgress = ui.createProgressBar({ title: "Folders...".padEnd(25) });
7637
+ const fetchAssetPagesProgress = ui.createProgressBar({ title: "Fetching Asset Pages...".padEnd(24) });
7638
+ const fetchAssetsProgress = ui.createProgressBar({ title: "Fetching Assets...".padEnd(24) });
7639
+ const saveProgress = ui.createProgressBar({ title: "Saving Assets...".padEnd(24) });
7640
+ await pipeline$1(
7641
+ fetchAssetFoldersStream({
7642
+ spaceId: space,
7643
+ setTotalFolders: (total) => {
7644
+ summary.folderResults.total = total;
7645
+ folderProgress.setTotal(total);
7646
+ },
7647
+ onSuccess: () => {
7648
+ logger.info("Fetched asset folders");
7649
+ },
7650
+ onError: (error) => {
7651
+ summary.folderResults.failed += 1;
7652
+ summary.folderResults.total = summary.folderResults.total || 1;
7653
+ folderProgress.setTotal(summary.folderResults.total);
7654
+ logOnlyError(error);
7655
+ }
7656
+ }),
7657
+ writeAssetFolderStream({
7658
+ writeAssetFolder: options.dryRun ? async (folder) => folder : makeWriteAssetFolderFSTransport({
7659
+ directoryPath: resolveCommandPath(directories.assets, space, basePath)
7660
+ }),
7661
+ onIncrement: () => {
7662
+ folderProgress.increment();
7663
+ },
7664
+ onFolderSuccess: (folder) => {
7665
+ logger.info("Saved folder", { folderId: folder.id });
7666
+ summary.folderResults.succeeded += 1;
7667
+ },
7668
+ onFolderError: (error, folder) => {
7669
+ summary.folderResults.failed += 1;
7670
+ summary.folderResults.total = Math.max(summary.folderResults.total, summary.folderResults.succeeded + summary.folderResults.failed);
7671
+ logOnlyError(error, { folderId: folder.id });
7672
+ }
7673
+ })
7674
+ );
7675
+ await pipeline$1(
7676
+ fetchAssetsStream({
7677
+ spaceId: space,
7678
+ params: options.query ? Object.fromEntries(new URLSearchParams(options.query)) : {},
7679
+ setTotalAssets: (total) => {
7680
+ summary.fetchAssets.total = total;
7681
+ summary.save.total = total;
7682
+ fetchAssetsProgress.setTotal(total);
7683
+ saveProgress.setTotal(total);
7684
+ },
7685
+ setTotalPages: (totalPages) => {
7686
+ summary.fetchAssetPages.total = totalPages;
7687
+ fetchAssetPagesProgress.setTotal(totalPages);
7688
+ },
7689
+ onIncrement: () => {
7690
+ fetchAssetPagesProgress.increment();
7691
+ },
7692
+ onPageSuccess: (page, totalPages) => {
7693
+ logger.info(`Fetched assets page ${page} of ${totalPages}`);
7694
+ summary.fetchAssetPages.succeeded += 1;
7695
+ },
7696
+ onPageError: (error, page, totalPages) => {
7697
+ summary.fetchAssetPages.failed += 1;
7698
+ logOnlyError(error, { page, totalPages });
7699
+ }
7700
+ }),
7701
+ downloadAssetStream({
7702
+ assetToken,
7703
+ region,
7704
+ onIncrement: () => {
7705
+ fetchAssetsProgress.increment();
7706
+ },
7707
+ onAssetSuccess: (asset) => {
7708
+ logger.info("Fetched asset", { assetId: asset.id });
7709
+ summary.fetchAssets.succeeded += 1;
7710
+ },
7711
+ onAssetError: (error, asset) => {
7712
+ summary.fetchAssets.failed += 1;
7713
+ summary.save.total -= 1;
7714
+ saveProgress.setTotal(summary.save.total);
7715
+ logOnlyError(error, { assetId: asset.id });
7716
+ }
7717
+ }),
7718
+ writeAssetStream({
7719
+ writeAsset: options.dryRun ? async (asset) => asset : makeWriteAssetFSTransport({
7720
+ directoryPath: resolveCommandPath(directories.assets, space, basePath)
7721
+ }),
7722
+ onIncrement: () => {
7723
+ saveProgress.increment();
7724
+ },
7725
+ onAssetSuccess: (asset) => {
7726
+ logger.info("Saved asset", { assetId: asset.id });
7727
+ summary.save.succeeded += 1;
7728
+ },
7729
+ onAssetError: (error, asset) => {
7730
+ summary.save.failed += 1;
7731
+ logOnlyError(error, { assetId: asset.id });
7732
+ }
7733
+ })
7734
+ );
7735
+ } catch (maybeError) {
7736
+ fatalError = true;
7737
+ handleError(toError(maybeError));
7738
+ } finally {
7739
+ logger.info("Pulling assets finished", summary);
7740
+ ui.stopAllProgressBars();
7741
+ const failedAssets = Math.max(summary.fetchAssets.failed, summary.save.failed);
7742
+ const folderSummary = {
7743
+ total: summary.folderResults.total,
7744
+ succeeded: summary.folderResults.succeeded,
7745
+ failed: summary.folderResults.failed
7746
+ };
7747
+ ui.info(`Pull results: ${summary.save.total} assets pulled, ${failedAssets} assets failed`);
7748
+ ui.list([
7749
+ `Folders: ${folderSummary.succeeded}/${folderSummary.total} succeeded, ${folderSummary.failed} failed.`,
7750
+ `Fetching pages: ${summary.fetchAssetPages.succeeded}/${summary.fetchAssetPages.total} succeeded, ${summary.fetchAssetPages.failed} failed.`,
7751
+ `Fetching assets: ${summary.fetchAssets.succeeded}/${summary.fetchAssets.total} succeeded, ${summary.fetchAssets.failed} failed.`,
7752
+ `Saving assets: ${summary.save.succeeded}/${summary.save.total} succeeded, ${summary.save.failed} failed.`
7753
+ ]);
7754
+ const reporter = getReporter();
7755
+ reporter.addSummary("folderResults", folderSummary);
7756
+ reporter.addSummary("fetchAssetPagesResults", summary.fetchAssetPages);
7757
+ reporter.addSummary("fetchAssetsResults", summary.fetchAssets);
7758
+ reporter.addSummary("saveResults", summary.save);
7759
+ reporter.finalize();
7760
+ const failedTotal = summary.folderResults.failed + summary.fetchAssetPages.failed + summary.fetchAssets.failed + summary.save.failed;
7761
+ process.exitCode = fatalError ? 2 : failedTotal > 0 ? 1 : 0;
7762
+ }
7763
+ });
7764
+
7765
+ const traverseAndMapBySchema = (data, {
7766
+ schemas,
7767
+ maps,
7768
+ fieldRefMappers: fieldRefMappers2,
7769
+ processedFields,
7770
+ missingSchemas
7771
+ }) => {
7772
+ const schema = schemas[data.component];
7773
+ if (!schema) {
7774
+ missingSchemas.add(data.component);
7775
+ return data;
7776
+ }
7777
+ const dataNew = { ...data };
7778
+ for (const [fieldName, fieldValue] of Object.entries(data)) {
7779
+ const fieldSchema = schema[fieldName.replace(/__i18n__.*/, "")];
7780
+ const fieldType = fieldSchema && typeof fieldSchema === "object" && "type" in fieldSchema && fieldSchema.type;
7781
+ const fieldRefMapper = typeof fieldType === "string" && fieldRefMappers2[fieldType];
7782
+ if (fieldSchema) {
7783
+ processedFields.add(fieldSchema);
7784
+ }
7785
+ if (fieldRefMapper) {
7786
+ dataNew[fieldName] = fieldRefMapper(fieldValue, {
7787
+ schema: fieldSchema,
7788
+ schemas,
7789
+ maps,
7790
+ fieldRefMappers: fieldRefMappers2,
7791
+ processedFields,
7792
+ missingSchemas
7793
+ });
7794
+ }
7795
+ }
7796
+ return dataNew;
7797
+ };
7798
+ const traverseAndMapRichtextDoc = (data, {
7799
+ schemas,
7800
+ maps,
7801
+ fieldRefMappers: fieldRefMappers2,
7802
+ processedFields,
7803
+ missingSchemas
7804
+ }) => {
7805
+ if (Array.isArray(data)) {
7806
+ return data.map((item) => traverseAndMapRichtextDoc(item, {
7807
+ schemas,
7808
+ maps,
7809
+ fieldRefMappers: fieldRefMappers2,
7810
+ processedFields,
7811
+ missingSchemas
7812
+ }));
7813
+ }
7814
+ if (data && typeof data === "object") {
7815
+ if (data.type === "link" && data.attrs.linktype === "story") {
7816
+ return {
7817
+ ...data,
7818
+ attrs: {
7819
+ ...data.attrs,
7820
+ uuid: maps.stories?.get(data.attrs.uuid) || data.attrs.uuid
7821
+ }
7822
+ };
7823
+ }
7824
+ if (data.type === "blok") {
7825
+ return {
7826
+ ...data,
7827
+ attrs: {
7828
+ ...data.attrs,
7829
+ body: data.attrs.body.map((d) => traverseAndMapBySchema(d, {
7830
+ schemas,
7831
+ maps,
7832
+ fieldRefMappers: fieldRefMappers2,
7833
+ processedFields,
7834
+ missingSchemas
7835
+ }))
7836
+ }
7837
+ };
7838
+ }
7839
+ const newData = {};
7840
+ for (const [k, value] of Object.entries(data)) {
7841
+ newData[k] = traverseAndMapRichtextDoc(value, {
7842
+ schemas,
7843
+ maps,
7844
+ fieldRefMappers: fieldRefMappers2,
7845
+ processedFields,
7846
+ missingSchemas
7847
+ });
7848
+ }
7849
+ return newData;
7850
+ }
7851
+ return data;
7852
+ };
7853
+ const richtextFieldRefMapper = (data, { schemas, maps, fieldRefMappers: fieldRefMappers2, processedFields, missingSchemas }) => traverseAndMapRichtextDoc(data, {
7854
+ schemas,
7855
+ maps,
7856
+ fieldRefMappers: fieldRefMappers2,
7857
+ processedFields,
7858
+ missingSchemas
7859
+ });
7860
+ const multilinkFieldRefMapper = (data, { maps }) => {
7861
+ if (data.linktype !== "story") {
7862
+ return data;
7863
+ }
7864
+ return {
7865
+ ...data,
7866
+ id: maps.stories?.get(data.id) || data.id
7867
+ };
7868
+ };
7869
+ const bloksFieldRefMapper = (data, { schemas, maps, fieldRefMappers: fieldRefMappers2, processedFields, missingSchemas }) => {
7870
+ if (!Array.isArray(data)) {
7871
+ throw new TypeError("Invalid data!");
7872
+ }
7873
+ return data.map((d) => traverseAndMapBySchema(d, {
7874
+ schemas,
7875
+ maps,
7876
+ fieldRefMappers: fieldRefMappers2,
7877
+ processedFields,
7878
+ missingSchemas
7879
+ }));
7880
+ };
7881
+ const assetFieldRefMapper = (data, { maps }) => {
7882
+ const mappedAsset = typeof data.id === "number" ? maps.assets?.get(data.id) : void 0;
7883
+ return {
7884
+ ...data,
7885
+ ...mappedAsset?.new
7886
+ };
7887
+ };
7888
+ const multiassetFieldRefMapper = (data, options) => {
7889
+ if (!Array.isArray(data)) {
7890
+ throw new TypeError("Invalid data!");
7891
+ }
7892
+ return data.map((d) => assetFieldRefMapper(d, options));
7893
+ };
7894
+ const optionsFieldRefMapper = (data, { schema, maps }) => {
7895
+ if (schema.source !== "internal_stories" || !Array.isArray(data)) {
7896
+ return data;
7897
+ }
7898
+ return data.map((d) => maps.stories?.get(d) || d);
7899
+ };
7900
+ const fieldRefMappers = {
7901
+ asset: assetFieldRefMapper,
7902
+ bloks: bloksFieldRefMapper,
7903
+ multiasset: multiassetFieldRefMapper,
7904
+ multilink: multilinkFieldRefMapper,
7905
+ options: optionsFieldRefMapper,
7906
+ richtext: richtextFieldRefMapper
7907
+ };
7908
+ const storyRefMapper = (story, { schemas, maps }) => {
7909
+ const processedFields = /* @__PURE__ */ new Set();
7910
+ const missingSchemas = /* @__PURE__ */ new Set();
7911
+ const alternates = story.alternates ? story.alternates.map((a) => ({
7912
+ ...a,
7913
+ id: maps.stories?.get(a.id) || a.id,
7914
+ parent_id: maps.stories?.get(a.parent_id) || a.parent_id
7915
+ })) : story.alternates;
7916
+ const parentId = maps.stories?.get(story.parent_id) || story.parent_id;
7917
+ const mappedStory = {
7918
+ ...story,
7919
+ content: traverseAndMapBySchema(story.content, {
7920
+ schemas,
7921
+ maps,
7922
+ fieldRefMappers,
7923
+ processedFields,
7924
+ missingSchemas
7925
+ }),
7926
+ id: Number(maps.stories?.get(story.id) || story.id),
7927
+ uuid: String(maps.stories?.get(story.uuid) || story.uuid),
7928
+ // @ts-expect-error Our types are wrong.
7929
+ parent_id: parentId ? Number(parentId) : null,
7930
+ alternates
7931
+ };
7932
+ return {
7933
+ mappedStory,
7934
+ processedFields,
7935
+ missingSchemas
7936
+ };
7937
+ };
7938
+
7939
+ const apiConcurrencyLock = new Sema(12);
7940
+ const fetchStoriesStream = ({
7941
+ spaceId,
7942
+ params = {},
7943
+ setTotalStories,
7944
+ setTotalPages,
7945
+ onIncrement,
7946
+ onPageSuccess,
7947
+ onPageError
7948
+ }) => {
7949
+ const listGenerator = async function* storyListIterator() {
7950
+ let perPage = 100;
7951
+ let page = 1;
7952
+ let totalPages = 1;
7953
+ setTotalPages?.(totalPages);
7954
+ while (page <= totalPages) {
7955
+ try {
7956
+ const result = await fetchStories(spaceId, {
7957
+ ...params,
7958
+ per_page: perPage,
7959
+ page
7960
+ });
7961
+ if (!result) {
7962
+ break;
7963
+ }
7964
+ const { headers } = result;
7965
+ const total = Number(headers.get("Total"));
7966
+ perPage = Number(headers.get("Per-Page"));
7967
+ totalPages = Math.ceil(total / perPage);
7968
+ setTotalStories?.(total);
7969
+ setTotalPages?.(totalPages);
7970
+ onPageSuccess?.(page, totalPages);
7971
+ for (const story of result.stories) {
7972
+ yield story;
7973
+ }
7974
+ page += 1;
7975
+ } catch (maybeError) {
7976
+ onPageError?.(toError(maybeError), page, totalPages);
7977
+ break;
7978
+ } finally {
7979
+ onIncrement?.();
7980
+ }
7981
+ }
7982
+ };
7983
+ return Readable.from(listGenerator());
7984
+ };
7985
+ const fetchStoryStream = ({
7986
+ spaceId,
7987
+ onIncrement,
7988
+ onStorySuccess,
7989
+ onStoryError
7990
+ }) => {
7991
+ const processing = /* @__PURE__ */ new Set();
7992
+ return new Transform({
7993
+ objectMode: true,
7994
+ async transform(listStory, _encoding, callback) {
7995
+ await apiConcurrencyLock.acquire();
7996
+ const task = fetchStory(spaceId, listStory.id.toString()).then((story) => {
7997
+ if (typeof story === "undefined") {
7998
+ throw new TypeError("Invalid story!");
7999
+ }
8000
+ onStorySuccess?.(story);
8001
+ this.push(story);
8002
+ }).catch((maybeError) => {
8003
+ onStoryError?.(toError(maybeError), listStory);
8004
+ }).finally(() => {
8005
+ onIncrement?.();
8006
+ apiConcurrencyLock.release();
8007
+ processing.delete(task);
8008
+ });
8009
+ processing.add(task);
8010
+ callback();
8011
+ },
8012
+ // Ensure all pending requests finish before closing the stream
8013
+ flush(callback) {
8014
+ Promise.all(processing).finally(() => callback());
8015
+ }
8016
+ });
8017
+ };
8018
+ const getUUIDFromFilename = (filename) => {
8019
+ const uuid = basename(filename, extname(filename)).split("_").at(-1);
8020
+ if (!uuid) {
8021
+ throw new Error(`Unable to extract UUID from local story "${filename}"`);
8022
+ }
8023
+ return uuid;
8024
+ };
8025
+ const readLocalStoriesStream = ({
8026
+ directoryPath,
8027
+ fileFilter = () => true,
8028
+ setTotalStories,
8029
+ onIncrement,
8030
+ onStorySuccess,
8031
+ onStoryError
8032
+ }) => {
8033
+ const listGenerator = async function* localStoryIterator() {
8034
+ const files = (await readDirectory(directoryPath)).filter((f) => extname(f) === ".json" && fileFilter({ uuid: getUUIDFromFilename(f) }));
8035
+ setTotalStories?.(files.length);
8036
+ for (const file of files) {
8037
+ try {
8038
+ const filePath = join(directoryPath, file);
8039
+ const fileContent = await readFile$1(filePath, "utf-8");
8040
+ const story = JSON.parse(fileContent);
8041
+ onStorySuccess?.(story);
8042
+ yield story;
8043
+ } catch (maybeError) {
8044
+ onStoryError?.(toError(maybeError), file);
8045
+ } finally {
8046
+ onIncrement?.();
8047
+ }
8048
+ }
8049
+ };
8050
+ return Readable.from(listGenerator());
8051
+ };
8052
+ const mapReferencesStream = ({
8053
+ schemas,
8054
+ maps,
8055
+ onIncrement,
8056
+ onStorySuccess,
8057
+ onStoryError
8058
+ }) => {
8059
+ return new Transform({
8060
+ objectMode: true,
8061
+ transform(localStory, _encoding, callback) {
8062
+ try {
8063
+ const { mappedStory, processedFields, missingSchemas } = storyRefMapper(localStory, { schemas, maps });
8064
+ onStorySuccess?.(mappedStory, processedFields, missingSchemas);
8065
+ this.push(mappedStory);
8066
+ } catch (maybeError) {
8067
+ onStoryError?.(toError(maybeError), localStory);
8068
+ } finally {
8069
+ onIncrement?.();
8070
+ callback();
8071
+ }
8072
+ }
8073
+ });
8074
+ };
8075
+ const getRemoteStory = async ({ spaceId, storyId }) => {
8076
+ const { data, response } = await mapiClient().stories.get({
8077
+ path: {
8078
+ space_id: spaceId,
8079
+ story_id: storyId
8080
+ }
8081
+ });
8082
+ if (!response.ok && response.status !== 404) {
8083
+ handleAPIError("pull_story", new FetchError(response.statusText, response));
8084
+ }
8085
+ if (data?.story?.deleted_at) {
8086
+ return void 0;
8087
+ }
8088
+ return data?.story;
8089
+ };
8090
+ const makeCreateStoryAPITransport = ({ spaceId }) => async (localStory) => {
8091
+ const { id: _id, uuid: _uuid, content, parent_id: _p, ...newStoryData } = localStory;
8092
+ const remoteStory = await createStory(spaceId, {
8093
+ story: {
8094
+ ...newStoryData,
8095
+ content: {
8096
+ // @ts-expect-error Our types are wrong.
8097
+ component: content?.component
8098
+ }
8099
+ },
8100
+ publish: 0
8101
+ });
8102
+ if (!remoteStory) {
8103
+ throw new Error("No response!");
8104
+ }
8105
+ return remoteStory;
8106
+ };
8107
+ const makeAppendToManifestFSTransport = ({ manifestFile }) => async (localStory, remoteStory) => {
8108
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
8109
+ await appendToFile(manifestFile, JSON.stringify({
8110
+ old_id: localStory.uuid,
8111
+ new_id: remoteStory.uuid,
8112
+ created_at: createdAt
8113
+ }));
8114
+ await appendToFile(manifestFile, JSON.stringify({
8115
+ old_id: localStory.id,
8116
+ new_id: remoteStory.id,
8117
+ created_at: createdAt
8118
+ }));
8119
+ };
8120
+ const createStoryPlaceholderStream = ({
8121
+ maps,
8122
+ spaceId,
8123
+ transports,
8124
+ onIncrement,
8125
+ onStorySuccess,
8126
+ onStorySkipped,
8127
+ onStoryError
8128
+ }) => {
8129
+ const processing = /* @__PURE__ */ new Set();
8130
+ return new Writable({
8131
+ objectMode: true,
8132
+ async write(localStory, _encoding, callback) {
8133
+ await apiConcurrencyLock.acquire();
8134
+ const task = (async () => {
8135
+ try {
8136
+ const mappedStoryId = maps.stories?.get(localStory.id);
8137
+ const mappedRemoteStory = mappedStoryId && await getRemoteStory({ spaceId, storyId: Number(mappedStoryId) });
8138
+ if (mappedRemoteStory) {
8139
+ onStorySkipped?.(localStory, mappedRemoteStory);
8140
+ return;
8141
+ }
8142
+ const existingRemoteStory = await getRemoteStory({ spaceId, storyId: localStory.id });
8143
+ if (existingRemoteStory && existingRemoteStory.uuid === localStory.uuid) {
8144
+ await transports.appendStoryManifest(localStory, existingRemoteStory);
8145
+ onStorySkipped?.(localStory, existingRemoteStory);
8146
+ return;
8147
+ }
8148
+ const newRemoteStory = await transports.createStory(localStory);
8149
+ await transports.appendStoryManifest(localStory, newRemoteStory);
8150
+ onStorySuccess?.(localStory, newRemoteStory);
8151
+ } catch (maybeError) {
8152
+ onStoryError?.(toError(maybeError), localStory);
8153
+ }
8154
+ })();
8155
+ processing.add(task);
8156
+ task.finally(() => {
8157
+ onIncrement?.();
8158
+ apiConcurrencyLock.release();
8159
+ processing.delete(task);
8160
+ });
8161
+ callback();
8162
+ },
8163
+ final(callback) {
8164
+ Promise.all(processing).finally(() => callback());
8165
+ }
8166
+ });
8167
+ };
8168
+ const makeWriteStoryFSTransport = ({ directoryPath }) => async (story) => {
8169
+ await saveToFile(resolve$1(directoryPath, getStoryFilename(story)), JSON.stringify(story, null, 2));
8170
+ return story;
8171
+ };
8172
+ const makeWriteStoryAPITransport = ({ spaceId, publish }) => (mappedLocalStory) => updateStory(spaceId, mappedLocalStory.id, {
8173
+ story: mappedLocalStory,
8174
+ publish: publish ?? (mappedLocalStory.published ? 1 : 0)
8175
+ });
8176
+ const makeCleanupStoryFSTransport = ({ directoryPath, maps }) => async (mappedStory) => {
8177
+ const mapEntry = maps.stories?.entries().find(([_, v]) => v === mappedStory.uuid);
8178
+ const originalUuid = mapEntry?.[0] && typeof mapEntry?.[0] === "string" ? mapEntry?.[0] : mappedStory.uuid;
8179
+ const storyFilename = getStoryFilename({
8180
+ slug: mappedStory.slug,
8181
+ uuid: originalUuid
8182
+ });
8183
+ const storyFilePath = resolve$1(directoryPath, storyFilename);
8184
+ await unlink(storyFilePath);
8185
+ };
8186
+ const writeStoryStream = ({
8187
+ transports,
8188
+ onIncrement,
8189
+ onStorySuccess,
8190
+ onStoryError
8191
+ }) => {
8192
+ const processing = /* @__PURE__ */ new Set();
8193
+ return new Writable({
8194
+ objectMode: true,
8195
+ async write(mappedLocalStory, _encoding, callback) {
8196
+ await apiConcurrencyLock.acquire();
8197
+ const task = (async () => {
8198
+ try {
8199
+ const remoteStory = await transports.writeStory(mappedLocalStory);
8200
+ await transports.cleanupStory?.(remoteStory);
8201
+ onStorySuccess?.(mappedLocalStory, remoteStory);
8202
+ } catch (maybeError) {
8203
+ onStoryError?.(toError(maybeError), mappedLocalStory);
8204
+ }
8205
+ })();
8206
+ processing.add(task);
8207
+ task.finally(() => {
8208
+ onIncrement?.();
8209
+ apiConcurrencyLock.release();
8210
+ processing.delete(task);
8211
+ });
8212
+ callback();
8213
+ },
8214
+ final(callback) {
8215
+ Promise.all(processing).finally(() => callback());
8216
+ }
8217
+ });
8218
+ };
8219
+
8220
+ const PROGRESS_BAR_PADDING = 23;
8221
+ const upsertAssetFoldersPipeline = async ({
8222
+ directoryPath,
8223
+ logger,
8224
+ maps,
8225
+ transports,
8226
+ ui
8227
+ }) => {
8228
+ const folderProgress = ui.createProgressBar({ title: "Folders...".padEnd(PROGRESS_BAR_PADDING) });
8229
+ const summary = { total: 0, succeeded: 0, failed: 0 };
8230
+ await pipeline$1(
8231
+ readLocalAssetFoldersStream({
8232
+ directoryPath,
8233
+ setTotalFolders: (total) => {
8234
+ summary.total = total;
8235
+ folderProgress.setTotal(total);
8236
+ },
8237
+ onFolderError: (error) => {
8238
+ summary.failed += 1;
8239
+ logOnlyError(error);
8240
+ }
8241
+ }),
8242
+ upsertAssetFolderStream({
8243
+ transports,
8244
+ maps,
8245
+ onIncrement: () => folderProgress.increment(),
8246
+ onFolderSuccess: (localFolder, remoteFolder) => {
8247
+ summary.succeeded += 1;
8248
+ maps.assetFolders.set(localFolder.id, remoteFolder.id);
8249
+ logger.info("Created asset folder", { folderId: remoteFolder.id });
8250
+ },
8251
+ onFolderError: (error, folder) => {
8252
+ summary.failed += 1;
8253
+ logOnlyError(error, { folderId: folder.id });
8254
+ }
8255
+ })
8256
+ );
8257
+ return [["assetFolderResults", summary]];
8258
+ };
8259
+ const upsertAssetsPipeline = async ({
8260
+ assetBinaryPath,
8261
+ assetData,
8262
+ directoryPath,
8263
+ logger,
8264
+ maps,
8265
+ transports,
8266
+ ui
8267
+ }) => {
8268
+ const assetProgress = ui.createProgressBar({ title: "Assets...".padEnd(PROGRESS_BAR_PADDING) });
8269
+ const summary = { total: 0, succeeded: 0, failed: 0, skipped: 0 };
8270
+ const steps = [];
8271
+ if (assetBinaryPath && assetData) {
8272
+ summary.total = 1;
8273
+ assetProgress.setTotal(1);
8274
+ steps.push(readSingleAssetStream({
8275
+ asset: assetData,
8276
+ assetBinaryPath,
8277
+ onAssetError: (error) => {
8278
+ summary.failed += 1;
8279
+ assetProgress.increment();
8280
+ logOnlyError(error);
8281
+ }
8282
+ }));
8283
+ } else {
8284
+ steps.push(readLocalAssetsStream({
8285
+ directoryPath,
8286
+ setTotalAssets: (total) => {
8287
+ summary.total = total;
8288
+ assetProgress.setTotal(total);
8289
+ },
8290
+ onAssetError: (error) => {
8291
+ summary.failed += 1;
8292
+ assetProgress.increment();
8293
+ logOnlyError(error);
8294
+ }
8295
+ }));
8296
+ }
8297
+ steps.push(upsertAssetStream({
8298
+ transports,
8299
+ maps,
8300
+ onIncrement: () => assetProgress.increment(),
8301
+ onAssetSuccess: (localAssetResult, remoteAsset) => {
8302
+ if ("id" in localAssetResult && localAssetResult.id) {
8303
+ maps.assets.set(localAssetResult.id, {
8304
+ old: localAssetResult,
8305
+ new: {
8306
+ id: remoteAsset.id,
8307
+ filename: remoteAsset.filename,
8308
+ meta_data: remoteAsset.meta_data
8309
+ }
8310
+ });
8311
+ }
8312
+ summary.succeeded += 1;
8313
+ logger.info("Uploaded asset", { assetId: remoteAsset.id });
8314
+ },
8315
+ onAssetSkipped: (localAssetResult, remoteAsset) => {
8316
+ if ("id" in localAssetResult && localAssetResult.id) {
8317
+ maps.assets.set(localAssetResult.id, {
8318
+ old: localAssetResult,
8319
+ new: {
8320
+ id: remoteAsset.id,
8321
+ filename: remoteAsset.filename,
8322
+ meta_data: remoteAsset.meta_data
8323
+ }
8324
+ });
8325
+ }
8326
+ summary.skipped += 1;
8327
+ logger.debug("Skipped asset (unchanged)", { assetId: remoteAsset.id });
8328
+ },
8329
+ onAssetError: (error, asset) => {
8330
+ summary.failed += 1;
8331
+ logOnlyError(error, { assetId: asset.id });
8332
+ }
8333
+ }));
8334
+ await pipeline$1(steps);
8335
+ return [["assetResults", summary]];
8336
+ };
8337
+ const mapAssetReferencesInStoriesPipeline = async ({
8338
+ logger,
8339
+ maps,
8340
+ schemas,
8341
+ space,
8342
+ transports,
8343
+ ui
8344
+ }) => {
8345
+ if (Object.keys(schemas).length === 0) {
8346
+ const message = "No components found. Please run `storyblok components pull` to fetch the latest components.";
8347
+ ui.error(message);
8348
+ logger.error(message);
8349
+ return [];
8350
+ }
8351
+ const fetchStoryPagesProgress = ui.createProgressBar({ title: "Fetching Story Pages...".padEnd(PROGRESS_BAR_PADDING) });
8352
+ const fetchStoriesProgress = ui.createProgressBar({ title: "Fetching Stories...".padEnd(PROGRESS_BAR_PADDING) });
8353
+ const processProgress = ui.createProgressBar({ title: "Processing Stories...".padEnd(PROGRESS_BAR_PADDING) });
8354
+ const updateProgress = ui.createProgressBar({ title: "Updating Stories...".padEnd(PROGRESS_BAR_PADDING) });
8355
+ const summaries = {
8356
+ fetchStoryPages: { total: 0, succeeded: 0, failed: 0 },
8357
+ fetchStories: { total: 0, succeeded: 0, failed: 0 },
8358
+ storyProcessResults: { total: 0, succeeded: 0, failed: 0 },
8359
+ storyUpdateResults: { total: 0, succeeded: 0, failed: 0 }
8360
+ };
8361
+ const warnAboutMissingSchemas = (missingSchemas, story) => {
8362
+ const missingSchemaWarnings = /* @__PURE__ */ new Set();
8363
+ for (const schemaName of missingSchemas) {
8364
+ if (missingSchemaWarnings.has(schemaName)) {
8365
+ continue;
8366
+ }
8367
+ const message = `The component "${schemaName}" was not found. Please run \`storyblok components pull\` to fetch the latest components.`;
8368
+ logger.warn(message, { storyId: story.uuid });
8369
+ missingSchemaWarnings.add(schemaName);
8370
+ }
8371
+ };
8372
+ const assetMapValues = [...maps.assets.values()];
8373
+ const reference_search = assetMapValues.length === 1 ? assetMapValues[0].new.filename : void 0;
8374
+ await pipeline$1(
8375
+ fetchStoriesStream({
8376
+ spaceId: space,
8377
+ params: {
8378
+ reference_search
8379
+ },
8380
+ setTotalPages: (totalPages) => {
8381
+ summaries.fetchStoryPages.total = totalPages;
8382
+ fetchStoryPagesProgress.setTotal(totalPages);
8383
+ },
8384
+ setTotalStories: (total) => {
8385
+ summaries.fetchStories.total = total;
8386
+ summaries.storyProcessResults.total = total;
8387
+ summaries.storyUpdateResults.total = total;
8388
+ fetchStoriesProgress.setTotal(total);
8389
+ processProgress.setTotal(total);
8390
+ updateProgress.setTotal(total);
8391
+ },
8392
+ onIncrement: () => fetchStoryPagesProgress.increment(),
8393
+ onPageSuccess: (page, total) => {
8394
+ logger.info(`Fetched stories page ${page} of ${total}`);
8395
+ summaries.fetchStoryPages.succeeded += 1;
8396
+ },
8397
+ onPageError: (error, page, total) => {
8398
+ summaries.fetchStoryPages.failed += 1;
8399
+ logOnlyError(error, { page, total });
8400
+ }
8401
+ }),
8402
+ fetchStoryStream({
8403
+ spaceId: space,
8404
+ onIncrement: () => {
8405
+ fetchStoriesProgress.increment();
8406
+ },
8407
+ onStorySuccess: (story) => {
8408
+ logger.info("Fetched story", { storyId: story.id });
8409
+ summaries.fetchStories.succeeded += 1;
8410
+ },
8411
+ onStoryError: (error, story) => {
8412
+ summaries.fetchStories.failed += 1;
8413
+ summaries.storyProcessResults.total -= 1;
8414
+ summaries.storyUpdateResults.total -= 1;
8415
+ processProgress.setTotal(summaries.storyProcessResults.total);
8416
+ updateProgress.setTotal(summaries.storyProcessResults.total);
8417
+ logOnlyError(error, { storyId: story.id });
8418
+ }
8419
+ }),
8420
+ // Map all references to numeric ids and uuids.
8421
+ mapReferencesStream({
8422
+ schemas,
8423
+ maps: { stories: /* @__PURE__ */ new Map(), ...maps },
8424
+ onIncrement() {
8425
+ processProgress.increment();
8426
+ },
8427
+ onStorySuccess(localStory, _, missingSchemas) {
8428
+ warnAboutMissingSchemas(missingSchemas, localStory);
8429
+ logger.info("Processed story", { storyId: localStory.uuid });
8430
+ summaries.storyProcessResults.succeeded += 1;
8431
+ },
8432
+ onStoryError(error, localStory) {
8433
+ summaries.storyProcessResults.failed += 1;
8434
+ summaries.storyUpdateResults.total -= 1;
8435
+ updateProgress.setTotal(summaries.storyUpdateResults.total);
8436
+ logOnlyError(error, { storyId: localStory.id });
8437
+ }
8438
+ }),
8439
+ // Update remote stories with correct references.
8440
+ writeStoryStream({
8441
+ transports: {
8442
+ writeStory: transports.writeStory
8443
+ },
8444
+ onIncrement() {
8445
+ updateProgress.increment();
8446
+ },
8447
+ onStorySuccess(localStory) {
8448
+ logger.info("Updated story", { storyId: localStory.uuid });
8449
+ summaries.storyUpdateResults.succeeded += 1;
8450
+ },
8451
+ onStoryError(error, localStory) {
8452
+ summaries.storyUpdateResults.failed += 1;
8453
+ logOnlyError(error, { storyId: localStory.id });
8454
+ }
8455
+ })
8456
+ );
8457
+ return Object.entries(summaries);
8458
+ };
8459
+
8460
+ 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) => {
8461
+ const ui = getUI();
8462
+ const logger = getLogger();
8463
+ const reporter = getReporter();
8464
+ ui.title(`${commands.ASSETS}`, colorPalette.ASSETS, "Pushing assets...");
8465
+ logger.info("Pushing assets started");
8466
+ if (options.dryRun) {
8467
+ ui.warn(`DRY RUN MODE ENABLED: No changes will be made.
8468
+ `);
8469
+ logger.warn("Dry run mode enabled");
8470
+ }
8471
+ const { space: targetSpace, path: basePath, verbose } = command.optsWithGlobals();
8472
+ const fromSpace = options.from || targetSpace;
8473
+ const assetToken = options.assetToken;
8474
+ const { state, initializeSession } = session();
8475
+ await initializeSession();
8476
+ if (!requireAuthentication(state, verbose)) {
8477
+ process.exitCode = 2;
8478
+ return;
8479
+ }
8480
+ if (!targetSpace) {
8481
+ handleError(new CommandError(`Please provide the space as argument --space YOUR_SPACE_ID.`), verbose);
8482
+ process.exitCode = 2;
8483
+ return;
8484
+ }
8485
+ const { password, region } = state;
8486
+ mapiClient({
8487
+ token: {
8488
+ accessToken: password
8489
+ },
8490
+ region
8491
+ });
8492
+ const summaries = [];
8493
+ let fatalError = false;
8494
+ const manifestFile = join(resolveCommandPath(directories.assets, fromSpace, basePath), "manifest.jsonl");
8495
+ const folderManifestFile = join(resolveCommandPath(directories.assets, fromSpace, basePath), "folders", "manifest.jsonl");
8496
+ try {
8497
+ const [assetMap, assetFolderMap] = await Promise.all([
8498
+ loadAssetMap(manifestFile),
8499
+ loadAssetFolderMap(folderManifestFile)
8500
+ ]);
8501
+ const maps = { assets: assetMap, assetFolders: assetFolderMap };
8502
+ const assetsDirectoryPath = resolveCommandPath(directories.assets, fromSpace, basePath);
8503
+ const assetFolderGetTransport = makeGetAssetFolderAPITransport({ spaceId: targetSpace });
8504
+ const assetFolderCreateTransport = options.dryRun ? async (folder) => folder : makeCreateAssetFolderAPITransport({ spaceId: targetSpace });
8505
+ const assetFolderUpdateTransport = options.dryRun ? async (folder) => folder : makeUpdateAssetFolderAPITransport({ spaceId: targetSpace });
8506
+ const assetFolderManifestTransport = options.dryRun ? () => Promise.resolve() : makeAppendAssetFolderManifestFSTransport({ manifestFile: folderManifestFile });
8507
+ const cleanupAssetFolderTransport = options.cleanup && !options.dryRun ? makeCleanupAssetFolderFSTransport() : () => Promise.resolve();
8508
+ summaries.push(...await upsertAssetFoldersPipeline({
8509
+ directoryPath: join(assetsDirectoryPath, "folders"),
8510
+ logger,
8511
+ maps,
8512
+ transports: {
8513
+ getAssetFolder: assetFolderGetTransport,
8514
+ createAssetFolder: assetFolderCreateTransport,
8515
+ updateAssetFolder: assetFolderUpdateTransport,
8516
+ appendAssetFolderManifest: assetFolderManifestTransport,
8517
+ cleanupAssetFolder: cleanupAssetFolderTransport
8518
+ },
8519
+ ui
8520
+ }));
8521
+ const assetBinaryPath = typeof assetInput === "string" && assetInput.trim().length > 0 ? assetInput : void 0;
8522
+ let assetData;
8523
+ if (assetBinaryPath) {
8524
+ const assetDataPartial = options.data ? parseAssetData(options.data) : !isRemoteSource(assetBinaryPath) ? await loadSidecarAssetData(assetBinaryPath) : {};
8525
+ const sourceBasename = isRemoteSource(assetBinaryPath) ? basename(new URL(assetBinaryPath).pathname) : basename(assetBinaryPath);
8526
+ const shortFilename = options.shortFilename || assetDataPartial.short_filename || sourceBasename;
8527
+ const folderId = options.folder ? Number(options.folder) : void 0;
8528
+ assetData = {
8529
+ ...assetDataPartial,
8530
+ short_filename: shortFilename,
8531
+ asset_folder_id: folderId
8532
+ };
8533
+ }
8534
+ const getAssetTransport = makeGetAssetAPITransport({ spaceId: targetSpace });
8535
+ const createAssetTransport = options.dryRun ? async (asset) => asset : makeCreateAssetAPITransport({ spaceId: targetSpace });
8536
+ const updateAssetTransport = options.dryRun ? async (asset) => asset : makeUpdateAssetAPITransport({ spaceId: targetSpace });
8537
+ const downloadAssetFileTransport = makeDownloadAssetFileTransport({
8538
+ assetToken,
8539
+ region
8540
+ });
8541
+ const assetManifestTransport = options.dryRun ? () => Promise.resolve() : makeAppendAssetManifestFSTransport({ manifestFile });
8542
+ const cleanupAssetTransport = options.cleanup && !options.dryRun ? makeCleanupAssetFSTransport() : () => Promise.resolve();
8543
+ summaries.push(...await upsertAssetsPipeline({
8544
+ assetBinaryPath,
8545
+ assetData,
8546
+ directoryPath: assetsDirectoryPath,
8547
+ logger,
8548
+ maps,
8549
+ transports: {
8550
+ getAsset: getAssetTransport,
8551
+ createAsset: createAssetTransport,
8552
+ updateAsset: updateAssetTransport,
8553
+ downloadAssetFile: downloadAssetFileTransport,
8554
+ appendAssetManifest: assetManifestTransport,
8555
+ cleanupAsset: cleanupAssetTransport
8556
+ },
8557
+ ui
8558
+ }));
8559
+ const hasUpdatedFilename = (entry) => "filename" in entry.old && entry.old.filename !== entry.new.filename;
8560
+ const hasMetadata = (entry) => "meta_data" in entry.new && entry.new.meta_data;
8561
+ const hasUpdatedAssets = maps.assets.values().some((v) => hasUpdatedFilename(v) || hasMetadata(v));
8562
+ if (hasUpdatedAssets && options.updateStories) {
8563
+ const schemas = await findComponentSchemas(resolveCommandPath(directories.components, fromSpace, basePath));
8564
+ const writeStoryTransport = options.dryRun ? async (story) => story : makeWriteStoryAPITransport({ spaceId: targetSpace });
8565
+ summaries.push(...await mapAssetReferencesInStoriesPipeline({
8566
+ logger,
8567
+ maps,
8568
+ schemas,
8569
+ space: targetSpace,
8570
+ transports: {
8571
+ writeStory: writeStoryTransport
8572
+ },
8573
+ ui
8574
+ }));
8575
+ }
8576
+ if (!options.dryRun) {
8577
+ await deduplicateManifest(manifestFile);
8578
+ }
8579
+ } catch (maybeError) {
8580
+ fatalError = true;
8581
+ handleError(toError(maybeError), verbose);
8582
+ } finally {
8583
+ ui.stopAllProgressBars();
8584
+ const summary = Object.fromEntries(summaries);
8585
+ logger.info("Pushing assets finished", { summary });
8586
+ const assetsTotal = summary.assetResults?.total ?? 0;
8587
+ const assetsSucceeded = summary.assetResults?.succeeded ?? 0;
8588
+ const assetsSkipped = summary.assetResults?.skipped ?? 0;
8589
+ const assetsFailed = summary.assetResults?.failed ?? 0;
8590
+ ui.info(`Push results: ${assetsTotal} processed, ${assetsFailed} assets failed`);
8591
+ ui.list([
8592
+ `Folders: ${summary.assetFolderResults?.succeeded ?? 0}/${summary.assetFolderResults?.total ?? 0} succeeded, ${summary.assetFolderResults?.failed ?? 0} failed.`,
8593
+ `Assets: ${assetsSucceeded}/${assetsTotal} succeeded, ${assetsSkipped} skipped, ${assetsFailed} failed.`
8594
+ ]);
8595
+ for (const [name, reportSummary] of summaries) {
8596
+ reporter.addSummary(name, reportSummary);
8597
+ }
8598
+ reporter.finalize();
8599
+ const failedTotal = Object.values(summary).reduce((total, entry) => {
8600
+ if (!entry || typeof entry.failed !== "number") {
8601
+ return total;
8602
+ }
8603
+ return total + entry.failed;
8604
+ }, 0);
8605
+ process.exitCode = fatalError ? 2 : failedTotal > 0 ? 1 : 0;
8606
+ }
8607
+ });
8608
+
8609
+ const program$1 = getProgram();
8610
+ const storiesCommand = program$1.command(commands.STORIES).description(`Manage your space's stories`).option("-s, --space <space>", "space ID");
8611
+
8612
+ 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) => {
8613
+ const ui = getUI();
8614
+ const logger = getLogger();
8615
+ const reporter = getReporter();
8616
+ ui.title(`${commands.STORIES}`, colorPalette.STORIES, "Pulling stories...");
8617
+ logger.info("Pulling stories started");
8618
+ if (options.dryRun) {
8619
+ ui.warn(`DRY RUN MODE ENABLED: No changes will be made.
8620
+ `);
8621
+ logger.warn("Dry run mode enabled");
8622
+ }
8623
+ const { space, path: basePath, verbose } = command.optsWithGlobals();
8624
+ const { state, initializeSession } = session();
8625
+ await initializeSession();
8626
+ if (!requireAuthentication(state, verbose)) {
8627
+ return;
8628
+ }
8629
+ if (!space) {
8630
+ handleError(new CommandError(`Please provide the space as argument --space YOUR_SPACE_ID.`), verbose);
8631
+ return;
8632
+ }
8633
+ const { password, region } = state;
8634
+ mapiClient({
8635
+ token: {
8636
+ accessToken: password
8637
+ },
8638
+ region
8639
+ });
8640
+ const summary = {
8641
+ fetchStoryPages: { total: 0, succeeded: 0, failed: 0 },
8642
+ fetchStories: { total: 0, succeeded: 0, failed: 0 },
8643
+ save: { total: 0, succeeded: 0, failed: 0 }
8644
+ };
8645
+ try {
8646
+ const fetchStoryPagesProgress = ui.createProgressBar({ title: "Fetching Story Pages...".padEnd(23) });
8647
+ const fetchStoriesProgress = ui.createProgressBar({ title: "Fetching Stories...".padEnd(23) });
8648
+ const saveProgress = ui.createProgressBar({ title: "Saving Stories...".padEnd(23) });
8649
+ await pipeline$1(
8650
+ fetchStoriesStream({
8651
+ spaceId: space,
8652
+ params: {
8653
+ filter_query: options.query,
8654
+ starts_with: options.startsWith
8655
+ },
8656
+ setTotalPages: (totalPages) => {
8657
+ summary.fetchStoryPages.total = totalPages;
8658
+ fetchStoryPagesProgress.setTotal(totalPages);
8659
+ },
8660
+ setTotalStories: (total) => {
8661
+ summary.fetchStories.total = total;
8662
+ summary.save.total = total;
8663
+ fetchStoriesProgress.setTotal(total);
8664
+ saveProgress.setTotal(total);
8665
+ },
8666
+ onIncrement: () => {
8667
+ fetchStoryPagesProgress.increment();
8668
+ },
8669
+ onPageSuccess: (page, total) => {
8670
+ logger.info(`Fetched stories page ${page} of ${total}`);
8671
+ summary.fetchStoryPages.succeeded += 1;
8672
+ },
8673
+ onPageError: (error, page, total) => {
8674
+ summary.fetchStoryPages.failed += 1;
8675
+ handleError(error, verbose, { page, total });
8676
+ }
8677
+ }),
8678
+ fetchStoryStream({
8679
+ spaceId: space,
8680
+ onIncrement: () => {
8681
+ fetchStoriesProgress.increment();
8682
+ },
8683
+ onStorySuccess: (story) => {
8684
+ logger.info("Fetched story", { storyId: story.id });
8685
+ summary.fetchStories.succeeded += 1;
8686
+ },
8687
+ onStoryError: (error, story) => {
8688
+ summary.fetchStories.failed += 1;
8689
+ summary.save.total -= 1;
8690
+ saveProgress.setTotal(summary.save.total);
8691
+ handleError(error, verbose, { storyId: story.id });
8692
+ }
8693
+ }),
8694
+ writeStoryStream({
8695
+ transports: {
8696
+ writeStory: options.dryRun ? async (story) => story : makeWriteStoryFSTransport({ directoryPath: resolveCommandPath(directories.stories, space, basePath) })
8697
+ },
8698
+ onIncrement: () => {
8699
+ saveProgress.increment();
8700
+ },
8701
+ onStorySuccess: (story) => {
8702
+ logger.info("Saved story", { storyId: story.id });
8703
+ summary.save.succeeded += 1;
8704
+ },
8705
+ onStoryError: (error, story) => {
8706
+ summary.save.failed += 1;
8707
+ handleError(error, verbose, { storyId: story.id });
8708
+ }
8709
+ })
8710
+ );
8711
+ } catch (maybeError) {
8712
+ handleError(toError(maybeError));
8713
+ } finally {
8714
+ logger.info("Pulling stories finished", summary);
8715
+ ui.stopAllProgressBars();
8716
+ ui.info(`Pull results: ${summary.save.total} stories pulled, ${Math.max(summary.fetchStories.failed, summary.save.failed)} stories failed`);
8717
+ ui.list([
8718
+ `Fetching pages: ${summary.fetchStoryPages.succeeded}/${summary.fetchStoryPages.total} succeeded, ${summary.fetchStoryPages.failed} failed.`,
8719
+ `Fetching stories: ${summary.fetchStories.succeeded}/${summary.fetchStories.total} succeeded, ${summary.fetchStories.failed} failed.`,
8720
+ `Saving stories: ${summary.save.succeeded}/${summary.save.total} succeeded, ${summary.save.failed} failed.`
8721
+ ]);
8722
+ reporter.addSummary("fetchStoryPagesResults", summary.fetchStoryPages);
8723
+ reporter.addSummary("fetchStoriesResults", summary.fetchStories);
8724
+ reporter.addSummary("saveResults", summary.save);
8725
+ reporter.finalize();
8726
+ }
8727
+ });
8728
+
8729
+ 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) => {
8730
+ const ui = getUI();
8731
+ const logger = getLogger();
8732
+ const reporter = getReporter();
8733
+ ui.title(`${commands.STORIES}`, colorPalette.STORIES, "Pushing stories...");
8734
+ logger.info("Pushing stories started");
8735
+ if (options.dryRun) {
8736
+ ui.warn(`DRY RUN MODE ENABLED: No changes will be made.
8737
+ `);
8738
+ logger.warn("Dry run mode enabled");
8739
+ }
8740
+ const { space, path: basePath, verbose } = command.optsWithGlobals();
8741
+ const fromSpace = options.from || space;
8742
+ const { state, initializeSession } = session();
8743
+ await initializeSession();
8744
+ if (!requireAuthentication(state, verbose)) {
8745
+ return;
8746
+ }
8747
+ if (!space) {
8748
+ handleError(new CommandError(`Please provide the space as argument --space YOUR_SPACE_ID.`), verbose);
8749
+ return;
8750
+ }
8751
+ const { password, region } = state;
8752
+ mapiClient({
8753
+ token: {
8754
+ accessToken: password
8755
+ },
8756
+ region
8757
+ });
8758
+ const warnAboutCustomPlugins = (fields, story) => {
8759
+ const warnedPlugins = /* @__PURE__ */ new Set();
8760
+ for (const field of fields) {
8761
+ if (field.type === "custom" && typeof field.field_type === "string") {
8762
+ if (warnedPlugins.has(field.field_type)) {
8763
+ continue;
8764
+ }
8765
+ warnedPlugins.add(field.field_type);
8766
+ const message = `The custom plugin "${field.field_type}" may contain references that require manual updates.`;
8767
+ ui.warn(message);
8768
+ logger.warn(message, { storyId: story.uuid });
8769
+ }
8770
+ }
8771
+ };
8772
+ const warnAboutMissingSchemas = (missingSchemas, story) => {
8773
+ const missingSchemaWarnings = /* @__PURE__ */ new Set();
8774
+ for (const schemaName of missingSchemas) {
8775
+ if (missingSchemaWarnings.has(schemaName)) {
8776
+ continue;
8777
+ }
8778
+ const message = `The component "${schemaName}" was not found. Please run \`storyblok components pull\` to fetch the latest components.`;
8779
+ ui.warn(message);
8780
+ logger.warn(message, { storyId: story.uuid });
8781
+ missingSchemaWarnings.add(schemaName);
8782
+ }
8783
+ };
8784
+ const summary = {
8785
+ creationResults: { total: 0, succeeded: 0, skipped: 0, failed: 0 },
8786
+ processResults: { total: 0, succeeded: 0, failed: 0 },
8787
+ updateResults: { total: 0, succeeded: 0, failed: 0 }
8788
+ };
8789
+ try {
8790
+ const manifestFile = join(resolveCommandPath(directories.stories, fromSpace, basePath), "manifest.jsonl");
8791
+ const manifest = await loadManifest(manifestFile);
8792
+ const assetManifestFile = join(resolveCommandPath(directories.assets, fromSpace, basePath), "manifest.jsonl");
8793
+ const maps = {
8794
+ assets: await loadAssetMap(assetManifestFile),
8795
+ stories: new Map(manifest.map((e) => [e.old_id, e.new_id]))
8796
+ };
8797
+ const schemas = await findComponentSchemas(resolveCommandPath(directories.components, fromSpace, basePath));
8798
+ if (Object.keys(schemas).length === 0) {
8799
+ const message = "No components found. Please run `storyblok components pull` to fetch the latest components.";
8800
+ ui.error(message);
8801
+ logger.error(message);
8802
+ return;
8803
+ }
8804
+ const storiesDirectoryPath = resolveCommandPath(directories.stories, fromSpace, basePath);
8805
+ const creationProgress = ui.createProgressBar({ title: "Creating Stories...".padEnd(21) });
8806
+ const processProgress = ui.createProgressBar({ title: "Processing Stories...".padEnd(21) });
8807
+ const updateProgress = ui.createProgressBar({ title: "Updating Stories...".padEnd(21) });
8808
+ await pipeline$1(
8809
+ // Read local stories from `.json` files.
8810
+ readLocalStoriesStream({
8811
+ directoryPath: storiesDirectoryPath,
8812
+ setTotalStories(total) {
8813
+ summary.creationResults.total = total;
8814
+ summary.processResults.total = total;
8815
+ summary.updateResults.total = total;
8816
+ creationProgress.setTotal(total);
8817
+ processProgress.setTotal(total);
8818
+ updateProgress.setTotal(total);
8819
+ },
8820
+ onStoryError(error) {
8821
+ summary.creationResults.failed += 1;
8822
+ summary.processResults.total -= 1;
8823
+ summary.updateResults.total -= 1;
8824
+ processProgress.setTotal(summary.processResults.total);
8825
+ updateProgress.setTotal(summary.updateResults.total);
8826
+ creationProgress.increment();
8827
+ handleError(error, verbose);
8828
+ }
8829
+ }),
8830
+ // Create remote stories.
8831
+ createStoryPlaceholderStream({
8832
+ maps,
8833
+ spaceId: space,
8834
+ transports: {
8835
+ createStory: options.dryRun ? async (story) => story : makeCreateStoryAPITransport({
8836
+ maps,
8837
+ spaceId: space
8838
+ }),
8839
+ appendStoryManifest: options.dryRun ? () => Promise.resolve() : makeAppendToManifestFSTransport({
8840
+ manifestFile
8841
+ })
8842
+ },
8843
+ onStorySuccess(localStory, remoteStory) {
8844
+ if (!localStory.uuid || !remoteStory.uuid) {
8845
+ throw new Error("Invalid story provided!");
8846
+ }
8847
+ maps.stories.set(localStory.id, remoteStory.id);
8848
+ maps.stories.set(localStory.uuid, remoteStory.uuid);
8849
+ logger.info("Created story", { storyId: remoteStory.uuid });
8850
+ summary.creationResults.succeeded += 1;
8851
+ },
8852
+ onStorySkipped(localStory, remoteStory) {
8853
+ if (!localStory.uuid || !remoteStory.uuid) {
8854
+ throw new Error("Invalid story provided!");
8855
+ }
8856
+ maps.stories.set(localStory.id, remoteStory.id);
8857
+ maps.stories.set(localStory.uuid, remoteStory.uuid);
8858
+ logger.info("Skipped creating story", { storyId: localStory.uuid });
8859
+ summary.creationResults.skipped += 1;
8860
+ },
8861
+ onStoryError(error) {
8862
+ summary.creationResults.failed += 1;
8863
+ summary.processResults.total -= 1;
8864
+ summary.updateResults.total -= 1;
8865
+ processProgress.setTotal(summary.processResults.total);
8866
+ updateProgress.setTotal(summary.updateResults.total);
8867
+ handleError(error, verbose);
8868
+ },
8869
+ onIncrement() {
8870
+ creationProgress.increment();
8871
+ }
8872
+ })
8873
+ );
8874
+ await pipeline$1(
8875
+ // Read local stories from `.json` files.
8876
+ readLocalStoriesStream({
8877
+ directoryPath: storiesDirectoryPath,
8878
+ fileFilter({ uuid }) {
8879
+ return Boolean(maps.stories.get(uuid));
8880
+ },
8881
+ setTotalStories(total) {
8882
+ summary.processResults.total = total;
8883
+ summary.updateResults.total = total;
8884
+ processProgress.setTotal(total);
8885
+ updateProgress.setTotal(total);
8886
+ },
8887
+ onStoryError(error) {
8888
+ summary.creationResults.failed += 1;
8889
+ summary.processResults.total -= 1;
8890
+ summary.updateResults.total -= 1;
8891
+ processProgress.setTotal(summary.processResults.total);
8892
+ updateProgress.setTotal(summary.updateResults.total);
8893
+ handleError(error, verbose);
8894
+ }
8895
+ }),
8896
+ // Map all references to numeric ids and uuids.
8897
+ mapReferencesStream({
8898
+ schemas,
8899
+ maps,
8900
+ onIncrement() {
8901
+ processProgress.increment();
8902
+ },
8903
+ onStorySuccess(localStory, processedFields, missingSchemas) {
8904
+ warnAboutCustomPlugins(processedFields, localStory);
8905
+ warnAboutMissingSchemas(missingSchemas, localStory);
8906
+ logger.info("Processed story", { storyId: localStory.uuid });
8907
+ summary.processResults.succeeded += 1;
8908
+ },
8909
+ onStoryError(error, localStory) {
8910
+ summary.processResults.failed += 1;
8911
+ summary.updateResults.total -= 1;
8912
+ updateProgress.setTotal(summary.updateResults.total);
8913
+ handleError(error, verbose, { storyId: localStory.uuid });
8914
+ }
8915
+ }),
8916
+ // Update remote stories with correct references.
8917
+ writeStoryStream({
8918
+ transports: {
8919
+ writeStory: options.dryRun ? async (story) => story : makeWriteStoryAPITransport({
8920
+ spaceId: space,
8921
+ publish: options.publish ? 1 : void 0
8922
+ }),
8923
+ cleanupStory: options.cleanup && !options.dryRun ? makeCleanupStoryFSTransport({ directoryPath: storiesDirectoryPath, maps }) : void 0
8924
+ },
8925
+ onIncrement() {
8926
+ updateProgress.increment();
8927
+ },
8928
+ onStorySuccess(localStory) {
8929
+ logger.info("Updated story", { storyId: localStory.uuid });
8930
+ summary.updateResults.succeeded += 1;
8931
+ },
8932
+ onStoryError(error, localStory) {
8933
+ summary.updateResults.failed += 1;
8934
+ handleError(error, verbose, { storyId: localStory.uuid });
8935
+ }
8936
+ })
8937
+ );
8938
+ } catch (maybeError) {
8939
+ handleError(toError(maybeError));
8940
+ } finally {
8941
+ logger.info("Pushing stories finished", summary);
8942
+ ui.stopAllProgressBars();
8943
+ const failedStories = Math.max(summary.creationResults.failed, summary.processResults.failed, summary.updateResults.failed);
8944
+ ui.info(`Push results: ${summary.creationResults.total} ${summary.creationResults.total === 1 ? "story" : "stories"} pushed, ${failedStories} ${failedStories === 1 ? "story" : "stories"} failed`);
8945
+ ui.list([
8946
+ `Creating stories: ${summary.creationResults.succeeded + summary.creationResults.skipped}/${summary.creationResults.total} succeeded, ${summary.creationResults.failed} failed.`,
8947
+ `Processing stories: ${summary.processResults.succeeded}/${summary.processResults.total} succeeded, ${summary.processResults.failed} failed.`,
8948
+ `Updating stories: ${summary.updateResults.succeeded}/${summary.updateResults.total} succeeded, ${summary.updateResults.failed} failed.`
8949
+ ]);
8950
+ reporter.addSummary("creationResults", summary.creationResults);
8951
+ reporter.addSummary("processResults", summary.processResults);
8952
+ reporter.addSummary("updateResults", summary.updateResults);
8953
+ reporter.finalize();
8954
+ }
8955
+ });
8956
+
6506
8957
  const program = getProgram();
6507
8958
  konsola.br();
6508
8959
  konsola.br();