storyblok 4.13.0 → 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;
@@ -1971,14 +2042,14 @@ async function performInteractiveLogin(options) {
1971
2042
  }
1972
2043
  }
1973
2044
 
1974
- const program$h = getProgram();
2045
+ const program$j = getProgram();
1975
2046
  const allRegionsText = Object.values(regions).join(",");
1976
- program$h.command(commands.LOGIN).description("Login to the Storyblok CLI").option("-t, --token <token>", "Token to login directly without questions, like for CI environments").option(
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(
1977
2048
  "-r, --region <region>",
1978
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}.`
1979
2050
  ).action(async (options) => {
1980
2051
  konsola.title(`${commands.LOGIN}`, colorPalette.LOGIN);
1981
- const verbose = program$h.opts().verbose;
2052
+ const verbose = program$j.opts().verbose;
1982
2053
  const { token, region } = options;
1983
2054
  const { state, updateSession, persistCredentials, initializeSession } = session();
1984
2055
  await initializeSession();
@@ -2037,10 +2108,10 @@ program$h.command(commands.LOGIN).description("Login to the Storyblok CLI").opti
2037
2108
  konsola.br();
2038
2109
  });
2039
2110
 
2040
- const program$g = getProgram();
2041
- 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 () => {
2042
2113
  konsola.title(`${commands.LOGOUT}`, colorPalette.LOGOUT);
2043
- const verbose = program$g.opts().verbose;
2114
+ const verbose = program$i.opts().verbose;
2044
2115
  try {
2045
2116
  const { state, initializeSession } = session();
2046
2117
  await initializeSession();
@@ -2088,10 +2159,10 @@ async function openSignupInBrowser(url) {
2088
2159
  }
2089
2160
  }
2090
2161
 
2091
- const program$f = getProgram();
2092
- 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 () => {
2093
2164
  konsola.title(`${commands.SIGNUP}`, colorPalette.SIGNUP);
2094
- const verbose = program$f.opts().verbose;
2165
+ const verbose = program$h.opts().verbose;
2095
2166
  const { state, initializeSession } = session();
2096
2167
  await initializeSession();
2097
2168
  if (state.isLoggedIn && !state.envLogin) {
@@ -2113,10 +2184,10 @@ program$f.command(commands.SIGNUP).description("Sign up for Storyblok").action(a
2113
2184
  konsola.br();
2114
2185
  });
2115
2186
 
2116
- const program$e = getProgram();
2117
- 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 () => {
2118
2189
  konsola.title(`${commands.USER}`, colorPalette.USER);
2119
- const verbose = program$e.opts().verbose;
2190
+ const verbose = program$g.opts().verbose;
2120
2191
  const { state, initializeSession } = session();
2121
2192
  await initializeSession();
2122
2193
  if (!requireAuthentication(state)) {
@@ -2145,8 +2216,8 @@ program$e.command(commands.USER).description("Get the current user").action(asyn
2145
2216
  konsola.br();
2146
2217
  });
2147
2218
 
2148
- const program$d = getProgram();
2149
- const componentsCommand = program$d.command(commands.COMPONENTS).alias("comp").description(`Manage your space's block schema`).option("-s, --space <space>", "space ID").option("-p, --path <path>", "path to save the file. Default is .storyblok/components");
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");
2150
2221
 
2151
2222
  const DEFAULT_COMPONENTS_FILENAME = "components";
2152
2223
  const DEFAULT_GROUPS_FILENAME = "groups";
@@ -2531,10 +2602,10 @@ async function readConsolidatedFiles$1(resolvedPath, suffix) {
2531
2602
  };
2532
2603
  }
2533
2604
 
2534
- const program$c = getProgram();
2605
+ const program$e = getProgram();
2535
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) => {
2536
2607
  konsola.title(`${commands.COMPONENTS}`, colorPalette.COMPONENTS, componentName ? `Pulling component ${componentName}...` : "Pulling components...");
2537
- const verbose = program$c.opts().verbose;
2608
+ const verbose = program$e.opts().verbose;
2538
2609
  const { space, path } = componentsCommand.opts();
2539
2610
  const {
2540
2611
  separateFiles = false,
@@ -3576,10 +3647,10 @@ async function pushWithDependencyGraph(space, spaceState, maxConcurrency = getAc
3576
3647
  return results;
3577
3648
  }
3578
3649
 
3579
- const program$b = getProgram();
3650
+ const program$d = getProgram();
3580
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) => {
3581
3652
  konsola.title(`${commands.COMPONENTS}`, colorPalette.COMPONENTS, componentName ? `Pushing component ${componentName}...` : "Pushing components...");
3582
- const verbose = program$b.opts().verbose;
3653
+ const verbose = program$d.opts().verbose;
3583
3654
  const { space, path } = componentsCommand.opts();
3584
3655
  const { filter } = options;
3585
3656
  const fromSpace = options.from || space;
@@ -3775,11 +3846,11 @@ const saveLanguagesToFile = async (space, internationalizationOptions, options)
3775
3846
  }
3776
3847
  };
3777
3848
 
3778
- const program$a = getProgram();
3779
- const languagesCommand = program$a.command(commands.LANGUAGES).alias("lang").description(`Manage your space's languages`).option("-s, --space <space>", "space ID").option("-p, --path <path>", "path to save the file. Default is .storyblok/languages");
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");
3780
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) => {
3781
3852
  konsola.title(`${commands.LANGUAGES}`, colorPalette.LANGUAGES);
3782
- const verbose = program$a.opts().verbose;
3853
+ const verbose = program$c.opts().verbose;
3783
3854
  const { space, path } = languagesCommand.opts();
3784
3855
  const { filename = "languages", suffix = options.space } = options;
3785
3856
  const { state, initializeSession } = session();
@@ -3826,8 +3897,8 @@ languagesCommand.command("pull").description(`Download your space's languages sc
3826
3897
  konsola.br();
3827
3898
  });
3828
3899
 
3829
- const program$9 = getProgram();
3830
- const migrationsCommand = program$9.command(commands.MIGRATIONS).alias("mig").description(`Manage your space's migrations`).option("-s, --space <space>", "space ID").option("-p, --path <path>", "path to save the file. Default is .storyblok/migrations");
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");
3831
3902
 
3832
3903
  const getMigrationTemplate = () => {
3833
3904
  return `export default function (block) {
@@ -3945,11 +4016,29 @@ const fetchStory = async (spaceId, storyId) => {
3945
4016
  },
3946
4017
  throwOnError: true
3947
4018
  });
3948
- return data?.story;
4019
+ return data.story;
3949
4020
  } catch (error) {
3950
4021
  handleAPIError("pull_story", error);
3951
4022
  }
3952
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
+ };
3953
4042
  const updateStory = async (spaceId, storyId, payload) => {
3954
4043
  try {
3955
4044
  const client = mapiClient();
@@ -3965,9 +4054,14 @@ const updateStory = async (spaceId, storyId, payload) => {
3965
4054
  },
3966
4055
  throwOnError: true
3967
4056
  });
3968
- return data?.story;
3969
- } catch (error) {
3970
- 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;
3971
4065
  }
3972
4066
  };
3973
4067
 
@@ -4424,6 +4518,49 @@ const isStoryPublishedWithoutChanges = (story) => {
4424
4518
  const isStoryWithUnpublishedChanges = (story) => {
4425
4519
  return story.published && story.unpublished_changes;
4426
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
+ };
4427
4564
 
4428
4565
  class UpdateStream extends Writable {
4429
4566
  constructor(options) {
@@ -4763,8 +4900,8 @@ migrationsCommand.command("rollback [migrationFile]").description("Rollback a mi
4763
4900
  }
4764
4901
  });
4765
4902
 
4766
- const program$8 = getProgram();
4767
- const typesCommand = program$8.command(commands.TYPES).alias("ts").description(`Generate types d.ts for your component schemas`).option("-s, --space <space>", "space ID").option("-p, --path <path>", "path to save the file. Default is .storyblok/types");
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");
4768
4905
 
4769
4906
  const getAssetJSONSchema = (title) => ({
4770
4907
  $id: "#/asset",
@@ -5674,13 +5811,13 @@ async function readConsolidatedFiles(resolvedPath, suffix) {
5674
5811
  };
5675
5812
  }
5676
5813
 
5677
- const program$7 = getProgram();
5814
+ const program$9 = getProgram();
5678
5815
  typesCommand.command("generate").description("Generate types d.ts for your component schemas").option(
5679
5816
  "--filename <name>",
5680
5817
  "Base file name for all component types when generating a single declarations file (e.g. components.d.ts). Ignored when using --separate-files."
5681
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) => {
5682
5819
  konsola.title(`${commands.TYPES}`, colorPalette.TYPES, "Generating types...");
5683
- const verbose = program$7.opts().verbose;
5820
+ const verbose = program$9.opts().verbose;
5684
5821
  const { space, path } = typesCommand.opts();
5685
5822
  const spinner = new Spinner({
5686
5823
  verbose: !isVitest
@@ -5733,8 +5870,8 @@ typesCommand.command("generate").description("Generate types d.ts for your compo
5733
5870
  }
5734
5871
  });
5735
5872
 
5736
- const program$6 = getProgram();
5737
- const datasourcesCommand = program$6.command(commands.DATASOURCES).alias("ds").description(`Manage your space's datasources`).option("-s, --space <space>", "space ID").option("-p, --path <path>", "path to save the file. Default is .storyblok/datasources");
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");
5738
5875
 
5739
5876
  async function fetchAllPages(fetchFunction, extractDataFunction, page = 1, collectedItems = []) {
5740
5877
  const { data, response } = await fetchFunction(page);
@@ -5840,10 +5977,10 @@ const saveDatasourcesToFiles = async (space, datasources, options) => {
5840
5977
  }
5841
5978
  };
5842
5979
 
5843
- const program$5 = getProgram();
5980
+ const program$7 = getProgram();
5844
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) => {
5845
5982
  konsola.title(`${commands.DATASOURCES}`, colorPalette.DATASOURCES, datasourceName ? `Pulling datasource ${datasourceName}...` : "Pulling datasources...");
5846
- const verbose = program$5.opts().verbose;
5983
+ const verbose = program$7.opts().verbose;
5847
5984
  const { space, path } = datasourcesCommand.opts();
5848
5985
  const {
5849
5986
  separateFiles = false,
@@ -5921,10 +6058,10 @@ datasourcesCommand.command("pull [datasourceName]").option("-f, --filename <file
5921
6058
  }
5922
6059
  });
5923
6060
 
5924
- const program$4 = getProgram();
6061
+ const program$6 = getProgram();
5925
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) => {
5926
6063
  konsola.title(`${commands.DATASOURCES}`, colorPalette.DATASOURCES, datasourceName ? `Pushing datasource ${datasourceName}...` : "Pushing datasources...");
5927
- const verbose = program$4.opts().verbose;
6064
+ const verbose = program$6.opts().verbose;
5928
6065
  const { space, path } = datasourcesCommand.opts();
5929
6066
  const { filter } = options;
5930
6067
  const fromSpace = options.from || space;
@@ -6300,13 +6437,13 @@ async function promptForLogin(verbose) {
6300
6437
  return null;
6301
6438
  }
6302
6439
  }
6303
- const program$3 = getProgram();
6304
- program$3.command(`${commands.CREATE} [project-path]`).alias("c").description(`Scaffold a new project using Storyblok`).option("-t, --template <template>", "technology starter template").option("-b, --blueprint <blueprint>", "[DEPRECATED] use --template instead").option("--skip-space", "skip space creation").option("--token <token>", "Storyblok access token (skip space creation and use this token)").option(
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(
6305
6442
  "-r, --region <region>",
6306
6443
  `The region to apply to the generated project template (does not affect space creation).`
6307
6444
  ).action(async (projectPath, options) => {
6308
6445
  konsola.title(`${commands.CREATE}`, colorPalette.CREATE);
6309
- const verbose = program$3.opts().verbose;
6446
+ const verbose = program$5.opts().verbose;
6310
6447
  const { template, blueprint, token } = options;
6311
6448
  if (options.region && !isRegion(options.region)) {
6312
6449
  handleError(new CommandError(`The provided region: ${options.region} is not valid. Please use one of the following values: ${Object.values(regions).join(" | ")}`));
@@ -6532,13 +6669,13 @@ program$3.command(`${commands.CREATE} [project-path]`).alias("c").description(`S
6532
6669
  konsola.br();
6533
6670
  });
6534
6671
 
6535
- const program$2 = getProgram();
6536
- const logsCommand = program$2.command(commands.LOGS).alias("lg").description(`Inspect and manage logs.`).option("-s, --space <space>", "The space ID.").option("-p, --path <path>", "Path to the directory containing the logs directory. Defaults to '.storyblok'.");
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'.");
6537
6674
 
6538
6675
  logsCommand.command("list").description("List logs").action(async () => {
6539
6676
  const { space, path } = logsCommand.opts();
6540
6677
  const ui = getUI();
6541
- const logsPath = resolveCommandPath(directories.log, space, path);
6678
+ const logsPath = resolveCommandPath(directories.logs, space, path);
6542
6679
  const logFiles = FileTransport.listLogFiles(logsPath);
6543
6680
  if (logFiles.length === 0) {
6544
6681
  ui.info(`No logs found for space "${space}".`);
@@ -6551,18 +6688,18 @@ logsCommand.command("list").description("List logs").action(async () => {
6551
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 }) => {
6552
6689
  const { space, path } = logsCommand.opts();
6553
6690
  const ui = getUI();
6554
- const logsPath = resolveCommandPath(directories.log, space, path);
6691
+ const logsPath = resolveCommandPath(directories.logs, space, path);
6555
6692
  const deletedFilesCount = FileTransport.pruneLogFiles(logsPath, keep);
6556
6693
  ui.info(`Deleted ${deletedFilesCount} log file${deletedFilesCount === 1 ? "" : "s"}`);
6557
6694
  });
6558
6695
 
6559
- const program$1 = getProgram();
6560
- const reportsCommand = program$1.command(commands.REPORTS).alias("rp").description("Inspect and manage reports.").option("-s, --space <space>", "The space ID.").option("-p, --path <path>", "Path to the directory containing the reports directory. Defaults to '.storyblok'.");
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'.");
6561
6698
 
6562
6699
  reportsCommand.command("list").description("List reports").action(async () => {
6563
6700
  const { space, path } = reportsCommand.opts();
6564
6701
  const ui = getUI();
6565
- const reportsPath = resolveCommandPath(directories.report, space, path);
6702
+ const reportsPath = resolveCommandPath(directories.reports, space, path);
6566
6703
  const reportFiles = Reporter.listReportFiles(reportsPath, ".jsonl");
6567
6704
  if (reportFiles.length === 0) {
6568
6705
  ui.info(`No reports found for space "${space}".`);
@@ -6575,11 +6712,2248 @@ reportsCommand.command("list").description("List reports").action(async () => {
6575
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 }) => {
6576
6713
  const { space, path } = reportsCommand.opts();
6577
6714
  const ui = getUI();
6578
- const reportsPath = resolveCommandPath(directories.report, space, path);
6715
+ const reportsPath = resolveCommandPath(directories.reports, space, path);
6579
6716
  const deletedFilesCount = Reporter.pruneReportFiles(reportsPath, keep, ".jsonl");
6580
6717
  ui.info(`Deleted ${deletedFilesCount} report file${deletedFilesCount === 1 ? "" : "s"}`);
6581
6718
  });
6582
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
+
6583
8957
  const program = getProgram();
6584
8958
  konsola.br();
6585
8959
  konsola.br();