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 +2458 -84
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
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
|
-
|
|
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
|
|
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 (
|
|
1163
|
-
|
|
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(
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
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
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
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$
|
|
2045
|
+
const program$j = getProgram();
|
|
1975
2046
|
const allRegionsText = Object.values(regions).join(",");
|
|
1976
|
-
program$
|
|
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$
|
|
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$
|
|
2041
|
-
program$
|
|
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$
|
|
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$
|
|
2092
|
-
program$
|
|
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$
|
|
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$
|
|
2117
|
-
program$
|
|
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$
|
|
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$
|
|
2149
|
-
const componentsCommand = program$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
3779
|
-
const languagesCommand = program$
|
|
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$
|
|
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$
|
|
3830
|
-
const migrationsCommand = program$
|
|
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
|
|
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
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
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$
|
|
4767
|
-
const typesCommand = program$
|
|
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$
|
|
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$
|
|
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$
|
|
5737
|
-
const datasourcesCommand = program$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
6304
|
-
program$
|
|
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$
|
|
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$
|
|
6536
|
-
const logsCommand = program$
|
|
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.
|
|
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.
|
|
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$
|
|
6560
|
-
const reportsCommand = program$
|
|
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.
|
|
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.
|
|
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();
|