storyblok 4.6.14 → 4.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -5,21 +5,21 @@ import { dirname } from 'pathe';
5
5
  import chalk from 'chalk';
6
6
  import { Command } from 'commander';
7
7
  import { readPackageUp } from 'read-package-up';
8
+ import path, { join, resolve, parse, dirname as dirname$1, extname } from 'node:path';
9
+ import { MultiBar, Presets } from 'cli-progress';
8
10
  import { Spinner } from '@topcli/spinner';
11
+ import fs, { mkdir, writeFile, readFile as readFile$1, appendFile, access, readdir } from 'node:fs/promises';
12
+ import filenamify from 'filenamify';
13
+ import { mkdirSync, appendFileSync, existsSync, readdirSync, unlinkSync, readFileSync } from 'node:fs';
9
14
  import { select, password, input, confirm } from '@inquirer/prompts';
10
15
  import { ManagementApiClient } from '@storyblok/management-api-client';
11
16
  import { RateLimit, Sema } from 'async-sema';
12
- import fs, { mkdir, writeFile, readFile as readFile$1, appendFile, access, readdir } from 'node:fs/promises';
13
- import path, { join, parse, resolve } from 'node:path';
14
- import filenamify from 'filenamify';
15
17
  import { exec, spawn } from 'node:child_process';
16
18
  import { promisify } from 'node:util';
17
19
  import { minimatch } from 'minimatch';
18
20
  import { Readable, pipeline, Transform, Writable } from 'node:stream';
19
21
  import { hash } from 'ohash';
20
- import { MultiBar, Presets } from 'cli-progress';
21
22
  import { compile } from 'json-schema-to-typescript';
22
- import { readFileSync } from 'node:fs';
23
23
  import open from 'open';
24
24
  import { Octokit } from 'octokit';
25
25
 
@@ -33,7 +33,8 @@ const commands = {
33
33
  MIGRATIONS: "migrations",
34
34
  TYPES: "types",
35
35
  DATASOURCES: "datasources",
36
- CREATE: "create"
36
+ CREATE: "create",
37
+ LOGS: "logs"
37
38
  };
38
39
  const colorPalette = {
39
40
  PRIMARY: "#8d60ff",
@@ -49,7 +50,8 @@ const colorPalette = {
49
50
  GROUPS: "#4ade80",
50
51
  TAGS: "#fbbf24",
51
52
  PRESETS: "#a855f7",
52
- DATASOURCES: "#4ade80"
53
+ DATASOURCES: "#4ade80",
54
+ LOGS: "#4ade80"
53
55
  };
54
56
  const regions = {
55
57
  EU: "eu",
@@ -89,6 +91,9 @@ const regionNames = {
89
91
  ({
90
92
  SB_Agent_Version: process.env.npm_package_version || "4.x"
91
93
  });
94
+ const directories = {
95
+ log: "logs"
96
+ };
92
97
 
93
98
  class FetchError extends Error {
94
99
  response;
@@ -279,6 +284,51 @@ class CommandError extends Error {
279
284
  }
280
285
  }
281
286
 
287
+ class Logger {
288
+ transports = [];
289
+ context = {};
290
+ constructor(options) {
291
+ if (options?.transports) {
292
+ this.transports = options.transports;
293
+ }
294
+ if (options?.context) {
295
+ this.context = options.context;
296
+ }
297
+ }
298
+ log(level, message, context) {
299
+ const timestamp = /* @__PURE__ */ new Date();
300
+ const mergedContext = context ? { ...this.context, ...context } : this.context;
301
+ const record = {
302
+ timestamp,
303
+ level,
304
+ message,
305
+ context: Object.keys(mergedContext).length ? mergedContext : void 0
306
+ };
307
+ for (const transport of this.transports) {
308
+ transport.log(record);
309
+ }
310
+ }
311
+ error(message, context) {
312
+ this.log("error", message, context);
313
+ }
314
+ warn(message, context) {
315
+ this.log("warn", message, context);
316
+ }
317
+ info(message, context) {
318
+ this.log("info", message, context);
319
+ }
320
+ debug(message, context) {
321
+ this.log("debug", message, context);
322
+ }
323
+ }
324
+ let loggerInstance = null;
325
+ function getLogger(options) {
326
+ if (!loggerInstance) {
327
+ loggerInstance = new Logger(options);
328
+ }
329
+ return loggerInstance;
330
+ }
331
+
282
332
  const FS_ERRORS = {
283
333
  file_not_found: "The file requested was not found",
284
334
  permission_denied: "Permission denied while accessing the file",
@@ -359,6 +409,25 @@ class FileSystemError extends Error {
359
409
  }
360
410
  }
361
411
 
412
+ function hasMessage(error) {
413
+ return typeof error === "object" && error !== null && "message" in error && typeof error.message === "string";
414
+ }
415
+ function toError(maybeError) {
416
+ if (maybeError instanceof Error) {
417
+ return maybeError;
418
+ }
419
+ if (typeof maybeError === "string") {
420
+ return new Error(maybeError);
421
+ }
422
+ if (hasMessage(maybeError)) {
423
+ return new Error(maybeError.message);
424
+ }
425
+ try {
426
+ return new Error(JSON.stringify(maybeError));
427
+ } catch {
428
+ return new Error(String(maybeError));
429
+ }
430
+ }
362
431
  function handleVerboseError(error) {
363
432
  if (error instanceof CommandError || error instanceof APIError || error instanceof FileSystemError) {
364
433
  const errorDetails = "getInfo" in error ? error.getInfo() : {};
@@ -398,6 +467,7 @@ function handleError(error, verbose = false) {
398
467
  if (!process.env.VITEST) {
399
468
  console.log("");
400
469
  }
470
+ getLogger().error(error.message, { error, errorCode: "code" in error ? String(error.code) : "UNKNOWN_ERROR" });
401
471
  }
402
472
 
403
473
  function requireAuthentication(state, verbose = false) {
@@ -503,6 +573,311 @@ function isRegion(value) {
503
573
  }
504
574
  const isVitest = process.env.VITEST === "true";
505
575
 
576
+ const noopProgressBar = {
577
+ increment: () => {
578
+ },
579
+ setTotal: (_n) => {
580
+ },
581
+ stop: () => {
582
+ }
583
+ };
584
+ const noopSpinner = {
585
+ failed: (_title) => {
586
+ },
587
+ succeed: (_title) => {
588
+ },
589
+ elapsedTime: 0
590
+ };
591
+ class UI {
592
+ console;
593
+ enabled;
594
+ multiBar;
595
+ constructor({ enabled }) {
596
+ this.console = enabled ? console : null;
597
+ this.enabled = enabled;
598
+ this.multiBar = enabled ? new MultiBar({
599
+ clearOnComplete: false,
600
+ format: `${chalk.bold(" {title} ")} ${chalk.hex(colorPalette.PRIMARY)("[{bar}]")} {percentage}% | {eta_formatted} | {value}/{total} processed`,
601
+ etaBuffer: 60
602
+ }, Presets.rect) : null;
603
+ }
604
+ title(message, color, subtitle) {
605
+ if (subtitle) {
606
+ this.console?.log(`${chalk.bgHex(color).bold(` ${capitalize(message)} `)} ${subtitle}`);
607
+ } else {
608
+ this.console?.log(chalk.bgHex(color).bold(` ${capitalize(message)} `));
609
+ }
610
+ this.br();
611
+ this.br();
612
+ }
613
+ br() {
614
+ this.console?.log("");
615
+ }
616
+ ok(message, header = false) {
617
+ if (header) {
618
+ this.br();
619
+ const successHeader = chalk.bgGreen.bold.white(` Success `);
620
+ this.console?.log(successHeader);
621
+ this.br();
622
+ }
623
+ this.console?.log(message ? `${chalk.green("\u2714")} ${message}` : "");
624
+ }
625
+ info(message, options = {
626
+ header: false,
627
+ margin: true
628
+ }) {
629
+ if (options.header) {
630
+ this.br();
631
+ const infoHeader = chalk.bgBlue.bold.white(` Info `);
632
+ this.console?.info(infoHeader);
633
+ }
634
+ this.console?.info(message ? `${chalk.blue("\u2139")} ${message}` : "");
635
+ if (options.margin) {
636
+ this.br();
637
+ }
638
+ }
639
+ warn(message, header = false) {
640
+ if (header) {
641
+ this.br();
642
+ const warnHeader = chalk.bgYellow.bold.black(` Warning `);
643
+ this.console?.warn(warnHeader);
644
+ }
645
+ this.console?.warn(message ? `${chalk.yellow("\u26A0\uFE0F ")} ${message}` : "");
646
+ }
647
+ error(message, info, options = {
648
+ header: false,
649
+ margin: false
650
+ }) {
651
+ if (options.header) {
652
+ const errorHeader = chalk.bgRed.bold.white(` Error `);
653
+ this.console?.error(errorHeader);
654
+ this.br();
655
+ }
656
+ this.console?.error(`${chalk.red.bold("\u25B2 error")} ${message}`, info || "");
657
+ if (options.margin) {
658
+ this.br();
659
+ }
660
+ }
661
+ list(items) {
662
+ for (const item of items) {
663
+ this.console?.log(` ${item}`);
664
+ }
665
+ }
666
+ createProgressBar(options) {
667
+ return this.multiBar?.create(0, 0, options) || noopProgressBar;
668
+ }
669
+ stopAllProgressBars() {
670
+ this.multiBar?.stop();
671
+ }
672
+ createSpinner(title) {
673
+ return this.enabled ? new Spinner({
674
+ verbose: !isVitest
675
+ }).start(title) : noopSpinner;
676
+ }
677
+ }
678
+ let uiInstance = null;
679
+ function getUI(options = { enabled: false }) {
680
+ if (!uiInstance) {
681
+ uiInstance = new UI(options);
682
+ }
683
+ return uiInstance;
684
+ }
685
+
686
+ const getStoryblokGlobalPath = () => {
687
+ const homeDirectory = process.env[process.platform.startsWith("win") ? "USERPROFILE" : "HOME"] || process.cwd();
688
+ return join(homeDirectory, ".storyblok");
689
+ };
690
+ const saveToFile = async (filePath, data, options) => {
691
+ const resolvedPath = parse(filePath).dir;
692
+ try {
693
+ await mkdir(resolvedPath, { recursive: true });
694
+ } catch (mkdirError) {
695
+ handleFileSystemError("mkdir", mkdirError);
696
+ return;
697
+ }
698
+ try {
699
+ await writeFile(filePath, data, options);
700
+ } catch (writeError) {
701
+ handleFileSystemError("write", writeError);
702
+ }
703
+ };
704
+ const appendToFile = async (filePath, data, options) => {
705
+ const resolvedPath = parse(filePath).dir;
706
+ try {
707
+ await mkdir(resolvedPath, { recursive: true });
708
+ } catch (mkdirError) {
709
+ handleFileSystemError("mkdir", mkdirError);
710
+ return;
711
+ }
712
+ try {
713
+ const dataWithNewline = data.endsWith("\n") ? data : `${data}
714
+ `;
715
+ await appendFile(filePath, dataWithNewline, options);
716
+ } catch (writeError) {
717
+ handleFileSystemError("write", writeError);
718
+ }
719
+ };
720
+ const appendToFileSync = (filePath, data, options) => {
721
+ const resolvedPath = parse(filePath).dir;
722
+ try {
723
+ mkdirSync(resolvedPath, { recursive: true });
724
+ } catch (mkdirError) {
725
+ handleFileSystemError("mkdir", mkdirError);
726
+ return;
727
+ }
728
+ try {
729
+ const dataWithNewline = data.endsWith("\n") ? data : `${data}
730
+ `;
731
+ appendFileSync(filePath, dataWithNewline, options);
732
+ } catch (writeError) {
733
+ handleFileSystemError("write", writeError);
734
+ }
735
+ };
736
+ const readFile = async (filePath) => {
737
+ try {
738
+ return await readFile$1(filePath, "utf8");
739
+ } catch (error) {
740
+ handleFileSystemError("read", error);
741
+ return "";
742
+ }
743
+ };
744
+ const resolvePath = (path, folder) => {
745
+ if (path) {
746
+ return resolve(process.cwd(), path, folder);
747
+ }
748
+ return resolve(resolve(process.cwd(), ".storyblok"), folder);
749
+ };
750
+ const getComponentNameFromFilename = (filename) => {
751
+ return filename.replace(/\.js$/, "");
752
+ };
753
+ const sanitizeFilename = (filename) => {
754
+ return filenamify(filename, {
755
+ replacement: "_"
756
+ });
757
+ };
758
+ async function readJsonFile(filePath) {
759
+ try {
760
+ const content = (await readFile(filePath)).toString();
761
+ if (!content) {
762
+ return { data: [] };
763
+ }
764
+ const parsed = JSON.parse(content);
765
+ return { data: Array.isArray(parsed) ? parsed : [parsed] };
766
+ } catch (error) {
767
+ return { data: [], error };
768
+ }
769
+ }
770
+ function importModule(filePath) {
771
+ return import(`file://${filePath}`);
772
+ }
773
+ function getLogsPath(logFileDir, space, baseDir) {
774
+ if (space) {
775
+ return resolvePath(baseDir, join(logFileDir, space));
776
+ }
777
+ return resolvePath(baseDir, logFileDir);
778
+ }
779
+
780
+ class FileTransport {
781
+ filePath;
782
+ level;
783
+ maxFiles;
784
+ hasPruned = false;
785
+ constructor(options) {
786
+ this.filePath = options?.filePath ?? `./${Date.now()}.jsonl`;
787
+ this.level = options?.level ?? "info";
788
+ this.maxFiles = options?.maxFiles;
789
+ }
790
+ log(record) {
791
+ if (!this.shouldLog(record.level)) {
792
+ return;
793
+ }
794
+ const line = this.format(record);
795
+ appendToFileSync(this.filePath, line);
796
+ if (!this.hasPruned && this.maxFiles !== void 0) {
797
+ this.hasPruned = true;
798
+ this.pruneOldFiles();
799
+ }
800
+ }
801
+ pruneOldFiles() {
802
+ if (this.maxFiles === void 0) {
803
+ return;
804
+ }
805
+ const dir = dirname$1(this.filePath);
806
+ const ext = extname(this.filePath);
807
+ FileTransport.pruneLogFiles(dir, this.maxFiles, ext);
808
+ }
809
+ static pruneLogFiles(directory, keep, extension = ".jsonl") {
810
+ if (!existsSync(directory)) {
811
+ return 0;
812
+ }
813
+ const files = readdirSync(directory).filter((file) => extname(file) === extension).sort();
814
+ const filesToDelete = files.length - keep;
815
+ if (filesToDelete <= 0) {
816
+ return 0;
817
+ }
818
+ for (const file of files.slice(0, filesToDelete)) {
819
+ unlinkSync(join(directory, file));
820
+ }
821
+ return filesToDelete;
822
+ }
823
+ static listLogFiles(directory, extension = ".jsonl") {
824
+ if (!existsSync(directory)) {
825
+ return [];
826
+ }
827
+ const files = readdirSync(directory).filter((file) => extname(file) === extension).sort();
828
+ return files.map((f) => join(directory, f).replace(process.cwd(), "."));
829
+ }
830
+ levelRank(level) {
831
+ switch (level) {
832
+ case "error":
833
+ return 0;
834
+ case "warn":
835
+ return 1;
836
+ case "info":
837
+ return 2;
838
+ case "debug":
839
+ return 3;
840
+ default:
841
+ return 3;
842
+ }
843
+ }
844
+ shouldLog(level) {
845
+ return this.levelRank(level) <= this.levelRank(this.level);
846
+ }
847
+ format(record) {
848
+ const timestamp = (record.timestamp ?? /* @__PURE__ */ new Date()).toISOString();
849
+ const level = record.level.toUpperCase();
850
+ const message = record.message.replaceAll("\n", "\\n");
851
+ const contextNormalized = record.context && this.formatContext(record.context);
852
+ return JSON.stringify({ timestamp, level, message, context: contextNormalized });
853
+ }
854
+ formatContext(context) {
855
+ const contextNormalized = {};
856
+ for (const [key, value] of Object.entries(context)) {
857
+ if (value instanceof APIError) {
858
+ contextNormalized[key] = {
859
+ name: value.name,
860
+ message: value.message,
861
+ httpCode: value.code,
862
+ httpStatusText: value.error?.response.statusText,
863
+ stack: value.stack
864
+ };
865
+ continue;
866
+ }
867
+ if (value instanceof Error) {
868
+ contextNormalized[key] = {
869
+ name: value.name,
870
+ message: value.message,
871
+ stack: value.stack
872
+ };
873
+ continue;
874
+ }
875
+ contextNormalized[key] = value;
876
+ }
877
+ return contextNormalized;
878
+ }
879
+ }
880
+
506
881
  let packageJson;
507
882
  const result = await readPackageUp({
508
883
  cwd: __dirname
@@ -521,7 +896,28 @@ let programInstance = null;
521
896
  function getProgram() {
522
897
  if (!programInstance) {
523
898
  programInstance = new Command();
524
- programInstance.name(packageJson.name).description(packageJson.description || "").version(packageJson.version);
899
+ programInstance.name(packageJson.name).description(packageJson.description || "").version(packageJson.version).hook("preAction", (_, actionCmd) => {
900
+ const options = actionCmd.optsWithGlobals();
901
+ const commandPieces = [];
902
+ for (let c = actionCmd; c; c = c.parent) {
903
+ commandPieces.unshift(c.name());
904
+ }
905
+ const command = commandPieces.join(" ");
906
+ const runId = Date.now();
907
+ const transports = [];
908
+ const logsPath = getLogsPath(directories.log, options.space, options.path);
909
+ const logFilename = `${commandPieces.join("-")}-${runId}.jsonl`;
910
+ const filePath = path.join(logsPath, logFilename);
911
+ transports.push(new FileTransport({
912
+ filePath,
913
+ maxFiles: 10
914
+ }));
915
+ getLogger({
916
+ context: { runId, command, options, cliVersion: packageJson.version },
917
+ transports
918
+ });
919
+ getUI({ enabled: true });
920
+ });
525
921
  programInstance.configureOutput({
526
922
  writeErr: (str) => handleError(new Error(str))
527
923
  });
@@ -650,75 +1046,6 @@ const loginWithOtp = async (email, password, otp, region) => {
650
1046
  }
651
1047
  };
652
1048
 
653
- const getStoryblokGlobalPath = () => {
654
- const homeDirectory = process.env[process.platform.startsWith("win") ? "USERPROFILE" : "HOME"] || process.cwd();
655
- return join(homeDirectory, ".storyblok");
656
- };
657
- const saveToFile = async (filePath, data, options) => {
658
- const resolvedPath = parse(filePath).dir;
659
- try {
660
- await mkdir(resolvedPath, { recursive: true });
661
- } catch (mkdirError) {
662
- handleFileSystemError("mkdir", mkdirError);
663
- return;
664
- }
665
- try {
666
- await writeFile(filePath, data, options);
667
- } catch (writeError) {
668
- handleFileSystemError("write", writeError);
669
- }
670
- };
671
- const appendToFile = async (filePath, data, options) => {
672
- const resolvedPath = parse(filePath).dir;
673
- try {
674
- await mkdir(resolvedPath, { recursive: true });
675
- } catch (mkdirError) {
676
- handleFileSystemError("mkdir", mkdirError);
677
- return;
678
- }
679
- try {
680
- const dataWithNewline = data.endsWith("\n") ? data : `${data}
681
- `;
682
- await appendFile(filePath, dataWithNewline, options);
683
- } catch (writeError) {
684
- handleFileSystemError("write", writeError);
685
- }
686
- };
687
- const readFile = async (filePath) => {
688
- try {
689
- return await readFile$1(filePath, "utf8");
690
- } catch (error) {
691
- handleFileSystemError("read", error);
692
- return "";
693
- }
694
- };
695
- const resolvePath = (path, folder) => {
696
- if (path) {
697
- return resolve(process.cwd(), path, folder);
698
- }
699
- return resolve(resolve(process.cwd(), ".storyblok"), folder);
700
- };
701
- const getComponentNameFromFilename = (filename) => {
702
- return filename.replace(/\.js$/, "");
703
- };
704
- const sanitizeFilename = (filename) => {
705
- return filenamify(filename, {
706
- replacement: "_"
707
- });
708
- };
709
- async function readJsonFile(filePath) {
710
- try {
711
- const content = (await readFile(filePath)).toString();
712
- if (!content) {
713
- return { data: [] };
714
- }
715
- const parsed = JSON.parse(content);
716
- return { data: Array.isArray(parsed) ? parsed : [parsed] };
717
- } catch (error) {
718
- return { data: [], error };
719
- }
720
- }
721
-
722
1049
  const getCredentials = async (filePath = join(getStoryblokGlobalPath(), "credentials.json")) => {
723
1050
  try {
724
1051
  await access(filePath);
@@ -845,7 +1172,7 @@ function session() {
845
1172
  return sessionInstance;
846
1173
  }
847
1174
 
848
- const program$i = getProgram();
1175
+ const program$g = getProgram();
849
1176
  const allRegionsText = Object.values(regions).join(",");
850
1177
  const loginStrategy = {
851
1178
  message: "How would you like to login?",
@@ -862,12 +1189,12 @@ const loginStrategy = {
862
1189
  }
863
1190
  ]
864
1191
  };
865
- program$i.command(commands.LOGIN).description("Login to the Storyblok CLI").option("-t, --token <token>", "Token to login directly without questions, like for CI environments").option(
1192
+ program$g.command(commands.LOGIN).description("Login to the Storyblok CLI").option("-t, --token <token>", "Token to login directly without questions, like for CI environments").option(
866
1193
  "-r, --region <region>",
867
1194
  `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}.`
868
1195
  ).action(async (options) => {
869
1196
  konsola.title(`${commands.LOGIN}`, colorPalette.LOGIN);
870
- const verbose = program$i.opts().verbose;
1197
+ const verbose = program$g.opts().verbose;
871
1198
  const { token, region } = options;
872
1199
  const { state, updateSession, persistCredentials, initializeSession } = session();
873
1200
  await initializeSession();
@@ -995,10 +1322,10 @@ program$i.command(commands.LOGIN).description("Login to the Storyblok CLI").opti
995
1322
  konsola.br();
996
1323
  });
997
1324
 
998
- const program$h = getProgram();
999
- program$h.command(commands.LOGOUT).description("Logout from the Storyblok CLI").action(async () => {
1325
+ const program$f = getProgram();
1326
+ program$f.command(commands.LOGOUT).description("Logout from the Storyblok CLI").action(async () => {
1000
1327
  konsola.title(`${commands.LOGOUT}`, colorPalette.LOGOUT);
1001
- const verbose = program$h.opts().verbose;
1328
+ const verbose = program$f.opts().verbose;
1002
1329
  try {
1003
1330
  const { state, initializeSession } = session();
1004
1331
  await initializeSession();
@@ -1046,10 +1373,10 @@ async function openSignupInBrowser(url) {
1046
1373
  }
1047
1374
  }
1048
1375
 
1049
- const program$g = getProgram();
1050
- program$g.command(commands.SIGNUP).description("Sign up for Storyblok").action(async () => {
1376
+ const program$e = getProgram();
1377
+ program$e.command(commands.SIGNUP).description("Sign up for Storyblok").action(async () => {
1051
1378
  konsola.title(`${commands.SIGNUP}`, colorPalette.SIGNUP);
1052
- const verbose = program$g.opts().verbose;
1379
+ const verbose = program$e.opts().verbose;
1053
1380
  const { state, initializeSession } = session();
1054
1381
  await initializeSession();
1055
1382
  if (state.isLoggedIn && !state.envLogin) {
@@ -1071,10 +1398,10 @@ program$g.command(commands.SIGNUP).description("Sign up for Storyblok").action(a
1071
1398
  konsola.br();
1072
1399
  });
1073
1400
 
1074
- const program$f = getProgram();
1075
- program$f.command(commands.USER).description("Get the current user").action(async () => {
1401
+ const program$d = getProgram();
1402
+ program$d.command(commands.USER).description("Get the current user").action(async () => {
1076
1403
  konsola.title(`${commands.USER}`, colorPalette.USER);
1077
- const verbose = program$f.opts().verbose;
1404
+ const verbose = program$d.opts().verbose;
1078
1405
  const { state, initializeSession } = session();
1079
1406
  await initializeSession();
1080
1407
  if (!requireAuthentication(state)) {
@@ -1103,8 +1430,8 @@ program$f.command(commands.USER).description("Get the current user").action(asyn
1103
1430
  konsola.br();
1104
1431
  });
1105
1432
 
1106
- const program$e = getProgram();
1107
- const componentsCommand = program$e.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");
1433
+ const program$c = getProgram();
1434
+ const componentsCommand = program$c.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");
1108
1435
 
1109
1436
  const fetchComponents = async (spaceId) => {
1110
1437
  try {
@@ -1484,10 +1811,10 @@ async function readConsolidatedFiles$1(resolvedPath, suffix) {
1484
1811
  };
1485
1812
  }
1486
1813
 
1487
- const program$d = getProgram();
1814
+ const program$b = getProgram();
1488
1815
  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) => {
1489
1816
  konsola.title(`${commands.COMPONENTS}`, colorPalette.COMPONENTS, componentName ? `Pulling component ${componentName}...` : "Pulling components...");
1490
- const verbose = program$d.opts().verbose;
1817
+ const verbose = program$b.opts().verbose;
1491
1818
  const { space, path } = componentsCommand.opts();
1492
1819
  const { separateFiles, suffix, filename = "components" } = options;
1493
1820
  const { state, initializeSession } = session();
@@ -2520,10 +2847,10 @@ async function pushWithDependencyGraph(space, spaceState, maxConcurrency = 5) {
2520
2847
  return results;
2521
2848
  }
2522
2849
 
2523
- const program$c = getProgram();
2850
+ const program$a = getProgram();
2524
2851
  componentsCommand.command("push [componentName]").description(`Push your space's components schema as json`).option("-f, --from <from>", "source space id").option("--fi, --filter <filter>", "glob filter to apply to the components before pushing").option("--sf, --separate-files", "Read from separate files instead of consolidated files").option("--su, --suffix <suffix>", "Suffix to add to the component name").action(async (componentName, options) => {
2525
2852
  konsola.title(`${commands.COMPONENTS}`, colorPalette.COMPONENTS, componentName ? `Pushing component ${componentName}...` : "Pushing components...");
2526
- const verbose = program$c.opts().verbose;
2853
+ const verbose = program$a.opts().verbose;
2527
2854
  const { space, path } = componentsCommand.opts();
2528
2855
  const { from, filter } = options;
2529
2856
  const { state, initializeSession } = session();
@@ -2719,11 +3046,11 @@ const saveLanguagesToFile = async (space, internationalizationOptions, options)
2719
3046
  }
2720
3047
  };
2721
3048
 
2722
- const program$b = getProgram();
2723
- const languagesCommand = program$b.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");
3049
+ const program$9 = getProgram();
3050
+ const languagesCommand = program$9.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");
2724
3051
  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) => {
2725
3052
  konsola.title(`${commands.LANGUAGES}`, colorPalette.LANGUAGES);
2726
- const verbose = program$b.opts().verbose;
3053
+ const verbose = program$9.opts().verbose;
2727
3054
  const { space, path } = languagesCommand.opts();
2728
3055
  const { filename = "languages", suffix = options.space } = options;
2729
3056
  const { state, initializeSession } = session();
@@ -2770,8 +3097,8 @@ languagesCommand.command("pull").description(`Download your space's languages sc
2770
3097
  konsola.br();
2771
3098
  });
2772
3099
 
2773
- const program$a = getProgram();
2774
- const migrationsCommand = program$a.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");
3100
+ const program$8 = getProgram();
3101
+ const migrationsCommand = program$8.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");
2775
3102
 
2776
3103
  const getMigrationTemplate = () => {
2777
3104
  return `export default function (block) {
@@ -2799,12 +3126,19 @@ const generateMigration = async (space, path, component, suffix) => {
2799
3126
  }
2800
3127
  };
2801
3128
 
2802
- const program$9 = getProgram();
2803
3129
  migrationsCommand.command("generate [componentName]").description("Generate a migration file").option("--su, --suffix <suffix>", "suffix to add to the file name (e.g. {component-name}.<suffix>.js)").action(async (componentName, options) => {
2804
- konsola.title(`${commands.MIGRATIONS}`, colorPalette.MIGRATIONS, componentName ? `Generating migration for component ${componentName}...` : "Generating migrations...");
2805
- const verbose = program$9.opts().verbose;
3130
+ const program = getProgram();
3131
+ const ui = getUI();
3132
+ const logger = getLogger();
3133
+ ui.title(`${commands.MIGRATIONS}`, colorPalette.MIGRATIONS, componentName ? `Generating migration for component ${componentName}...` : "Generating migrations...");
3134
+ const verbose = program.opts().verbose;
2806
3135
  const { space, path } = migrationsCommand.opts();
2807
3136
  const { suffix } = options;
3137
+ logger.info("Migration generation started", {
3138
+ componentName,
3139
+ space,
3140
+ suffix
3141
+ });
2808
3142
  if (!componentName) {
2809
3143
  handleError(new CommandError(`Please provide the component name as argument ${chalk.hex(colorPalette.MIGRATIONS)("storyblok migrations generate YOUR_COMPONENT_NAME.")}`), verbose);
2810
3144
  return;
@@ -2825,9 +3159,7 @@ migrationsCommand.command("generate [componentName]").description("Generate a mi
2825
3159
  },
2826
3160
  region
2827
3161
  });
2828
- const spinner = new Spinner({
2829
- verbose: !isVitest
2830
- }).start(`Generating migration for component ${componentName}...`);
3162
+ const spinner = ui.createSpinner(`Generating migration for component ${componentName}...`);
2831
3163
  try {
2832
3164
  const component = await fetchComponent(space, componentName);
2833
3165
  if (!component) {
@@ -2839,7 +3171,13 @@ migrationsCommand.command("generate [componentName]").description("Generate a mi
2839
3171
  spinner.succeed(`Migration generated for component ${chalk.hex(colorPalette.MIGRATIONS)(componentName)} - Completed in ${spinner.elapsedTime.toFixed(2)}ms`);
2840
3172
  const fileName = suffix ? `${component.name}.${suffix}.js` : `${component.name}.js`;
2841
3173
  const migrationPath = path ? `${path}/migrations/${space}/${fileName}` : `.storyblok/migrations/${space}/${fileName}`;
2842
- konsola.ok(`You can find the migration file in ${chalk.hex(colorPalette.MIGRATIONS)(migrationPath)}`);
3174
+ ui.ok(`You can find the migration file in ${chalk.hex(colorPalette.MIGRATIONS)(migrationPath)}`);
3175
+ logger.info("Migration generation finished", {
3176
+ componentName: component.name,
3177
+ migrationPath,
3178
+ space,
3179
+ suffix
3180
+ });
2843
3181
  } catch (error) {
2844
3182
  spinner.failed(`Failed to generate migration for component ${componentName}`);
2845
3183
  handleError(error, verbose);
@@ -2904,6 +3242,18 @@ const updateStory = async (spaceId, storyId, payload) => {
2904
3242
  }
2905
3243
  };
2906
3244
 
3245
+ const ERROR_CODES = {
3246
+ MIGRATION_APPLY_TO_STORY_ERROR: "MIGRATION_APPLY_TO_STORY_ERROR",
3247
+ MIGRATION_CREATE_STORIES_PIPELINE_ERROR: "MIGRATION_CREATE_STORIES_PIPELINE_ERROR",
3248
+ MIGRATION_FILE_NO_DEFAULT_EXPORT: "MIGRATION_FILE_NO_DEFAULT_EXPORT",
3249
+ MIGRATION_FILE_NOT_FOUND: "MIGRATION_FILE_NOT_FOUND",
3250
+ MIGRATION_LOAD_ERROR: "MIGRATION_LOAD_ERROR",
3251
+ MIGRATION_STORY_CONTENT_MISSING: "MIGRATION_STORY_CONTENT_MISSING",
3252
+ MIGRATION_STORY_FETCH_ERROR: "MIGRATION_STORY_FETCH_ERROR",
3253
+ MIGRATION_STORY_UPDATE_ERROR: "MIGRATION_STORY_UPDATE_ERROR",
3254
+ MIGRATION_STORY_UPDATE_NULL: "MIGRATION_STORY_UPDATE_NULL"
3255
+ };
3256
+
2907
3257
  async function* storiesIterator(spaceId, params, onTotal) {
2908
3258
  try {
2909
3259
  let perPage = 500;
@@ -2924,6 +3274,7 @@ async function* storiesIterator(spaceId, params, onTotal) {
2924
3274
  page: 1,
2925
3275
  story_only: true
2926
3276
  });
3277
+ getLogger().info(`Fetched stories page 1 of ${perPage}`);
2927
3278
  if (!result) {
2928
3279
  return;
2929
3280
  }
@@ -2941,6 +3292,7 @@ async function* storiesIterator(spaceId, params, onTotal) {
2941
3292
  page,
2942
3293
  story_only: true
2943
3294
  });
3295
+ getLogger().info(`Fetched stories page ${page} of ${perPage}`);
2944
3296
  if (!result2) {
2945
3297
  return;
2946
3298
  }
@@ -2965,14 +3317,20 @@ class StoriesStream extends Transform {
2965
3317
  }
2966
3318
  semaphore;
2967
3319
  async _transform(chunk, _encoding, callback) {
2968
- await this.semaphore.acquire();
2969
- fetchStory(this.spaceId, chunk.id.toString()).then((story) => {
3320
+ try {
3321
+ await this.semaphore.acquire();
3322
+ const story = await fetchStory(this.spaceId, chunk.id.toString());
2970
3323
  this.push(story);
2971
3324
  this.onProgress?.();
2972
- }).finally(() => {
3325
+ getLogger().info("Fetched story", { storyId: chunk.id });
3326
+ callback();
3327
+ } catch (maybeError) {
3328
+ const error = toError(maybeError);
3329
+ getLogger().error(error.message, { storyId: chunk.id, error, errorCode: ERROR_CODES.MIGRATION_STORY_FETCH_ERROR });
3330
+ callback(error);
3331
+ } finally {
2973
3332
  this.semaphore.release();
2974
- });
2975
- callback();
3333
+ }
2976
3334
  }
2977
3335
  _flush(callback) {
2978
3336
  this.semaphore.drain().then(() => {
@@ -2992,37 +3350,14 @@ const createStoriesStream = async ({
2992
3350
  return pipeline(listStoriesStream, new StoriesStream(spaceId, batchSize, onProgress), (err) => {
2993
3351
  if (err) {
2994
3352
  console.error(err);
3353
+ getLogger().error(err.message, { errorCode: ERROR_CODES.MIGRATION_CREATE_STORIES_PIPELINE_ERROR });
2995
3354
  }
2996
3355
  });
2997
3356
  };
2998
3357
 
2999
- async function readJavascriptFile(filePath) {
3000
- try {
3001
- const content = await readFile$1(filePath, "utf-8");
3002
- if (!content) {
3003
- throw new FileSystemError("invalid_argument", "read", new Error(`File ${filePath} is empty`));
3004
- }
3005
- return content;
3006
- } catch (error) {
3007
- throw new FileSystemError("file_not_found", "read", error);
3008
- }
3009
- }
3010
3358
  async function readMigrationFiles(options) {
3011
3359
  const { space, path, filter } = options;
3012
3360
  const resolvedPath = resolvePath(path, `migrations/${space}`);
3013
- try {
3014
- await readdir(resolvedPath);
3015
- } catch (error) {
3016
- const message = `No directory found for space "${space}". Please make sure you have pulled the migrations first by running:
3017
-
3018
- storyblok migrations pull --space ${space}`;
3019
- throw new FileSystemError(
3020
- "file_not_found",
3021
- "read",
3022
- error,
3023
- message
3024
- );
3025
- }
3026
3361
  try {
3027
3362
  const dirFiles = await readdir(resolvedPath);
3028
3363
  const migrationFiles = [];
@@ -3035,20 +3370,21 @@ async function readMigrationFiles(options) {
3035
3370
  if (filterRegex && !filterRegex.test(file)) {
3036
3371
  continue;
3037
3372
  }
3038
- const filePath = join(resolvedPath, file);
3039
- const content = await readJavascriptFile(filePath);
3040
3373
  migrationFiles.push({
3041
- name: file,
3042
- content
3374
+ name: file
3043
3375
  });
3044
3376
  }
3045
3377
  }
3046
3378
  return migrationFiles;
3047
3379
  } catch (error) {
3380
+ const message = `No directory found for space "${space}". Please make sure you have generated migrations first by running:
3381
+
3382
+ storyblok migrations generate YOUR_COMPONENT_NAME --space ${space}`;
3048
3383
  throw new FileSystemError(
3049
3384
  "file_not_found",
3050
3385
  "read",
3051
- error
3386
+ error,
3387
+ message
3052
3388
  );
3053
3389
  }
3054
3390
  }
@@ -3056,14 +3392,24 @@ async function getMigrationFunction(fileName, space, basePath) {
3056
3392
  try {
3057
3393
  const resolvedPath = resolvePath(basePath, `migrations/${space}`);
3058
3394
  const filePath = join(resolvedPath, fileName);
3059
- const migrationModule = await import(`file://${filePath}`);
3395
+ const migrationModule = await importModule(filePath);
3060
3396
  if (typeof migrationModule.default === "function") {
3061
3397
  return migrationModule.default;
3062
3398
  }
3063
- konsola.error(`Migration file "${fileName}" does not export a default function.`);
3399
+ getUI().error(`Migration file "${fileName}" does not export a default function.`);
3400
+ getLogger().error("Migration file does not export a default function", {
3401
+ fileName,
3402
+ errorCode: ERROR_CODES.MIGRATION_FILE_NO_DEFAULT_EXPORT
3403
+ });
3064
3404
  return null;
3065
- } catch (error) {
3066
- konsola.error(`Error loading migration function from "${fileName}": ${error.message}`);
3405
+ } catch (maybeError) {
3406
+ const error = toError(maybeError);
3407
+ getUI().error(`Error loading migration function from "${fileName}": ${error.message}`);
3408
+ getLogger().error("Couldn't load migration function", {
3409
+ fileName,
3410
+ error,
3411
+ errorCode: ERROR_CODES.MIGRATION_LOAD_ERROR
3412
+ });
3067
3413
  return null;
3068
3414
  }
3069
3415
  }
@@ -3200,6 +3546,10 @@ class MigrationStream extends Transform {
3200
3546
  migrationNames: relevantMigrations.map((m) => m.name),
3201
3547
  error: new Error("Story content is missing")
3202
3548
  });
3549
+ getLogger().error("Failed to process story: Content is missing", {
3550
+ storyId: story.id,
3551
+ errorCode: ERROR_CODES.MIGRATION_STORY_CONTENT_MISSING
3552
+ });
3203
3553
  return [];
3204
3554
  }
3205
3555
  const successfulResults = [];
@@ -3218,10 +3568,14 @@ class MigrationStream extends Transform {
3218
3568
  for (const migrationFile of migrationFiles) {
3219
3569
  const migrationFunction = await this.getOrLoadMigrationFunction(migrationFile);
3220
3570
  if (!migrationFunction) {
3571
+ const error = new Error(`Failed to load migration function from file "${migrationFile.name}"`);
3221
3572
  this.results.failed.push({
3222
3573
  storyId: story.id,
3223
3574
  migrationNames,
3224
- error: new Error(`Failed to load migration function from file "${migrationFile.name}"`)
3575
+ error
3576
+ });
3577
+ getLogger().error(error.message, {
3578
+ errorCode: ERROR_CODES.MIGRATION_FILE_NOT_FOUND
3225
3579
  });
3226
3580
  return null;
3227
3581
  }
@@ -3251,6 +3605,7 @@ class MigrationStream extends Transform {
3251
3605
  migrationNames,
3252
3606
  content: storyContent
3253
3607
  });
3608
+ getLogger().info("Applied migration", { storyId: story.id, migrationNames });
3254
3609
  return {
3255
3610
  storyId: story.id,
3256
3611
  name: story.name,
@@ -3265,6 +3620,7 @@ class MigrationStream extends Transform {
3265
3620
  migrationNames,
3266
3621
  reason: "No changes detected after migration"
3267
3622
  });
3623
+ getLogger().info("Skipped migration: No changes detected", { storyId: story.id, migrationNames });
3268
3624
  return null;
3269
3625
  } else {
3270
3626
  const reason = migrationFiles.map((migrationFile) => {
@@ -3278,14 +3634,22 @@ class MigrationStream extends Transform {
3278
3634
  migrationNames,
3279
3635
  reason
3280
3636
  });
3637
+ getLogger().info(`Skipped migration: ${reason}`, { storyId: story.id, migrationNames });
3281
3638
  return null;
3282
3639
  }
3283
- } catch (error) {
3640
+ } catch (maybeError) {
3641
+ const error = toError(maybeError);
3284
3642
  this.results.failed.push({
3285
3643
  storyId: story.id,
3286
3644
  migrationNames,
3287
3645
  error
3288
3646
  });
3647
+ getLogger().error(error.message, {
3648
+ storyId: story.id,
3649
+ migrationNames,
3650
+ error,
3651
+ errorCode: ERROR_CODES.MIGRATION_APPLY_TO_STORY_ERROR
3652
+ });
3289
3653
  return null;
3290
3654
  }
3291
3655
  }
@@ -3385,16 +3749,23 @@ class UpdateStream extends Writable {
3385
3749
  this.results.successful.push({ storyId, name: storyName });
3386
3750
  this.results.totalProcessed++;
3387
3751
  this.options.onProgress?.(this.results.totalProcessed);
3752
+ getLogger().info("Updated story", { storyId });
3388
3753
  } else {
3754
+ const error = new Error("Update returned null");
3389
3755
  this.results.failed.push({
3390
3756
  storyId,
3391
3757
  name: storyName,
3392
- error: new Error("Update returned null")
3758
+ error
3393
3759
  });
3394
3760
  this.results.totalProcessed++;
3395
3761
  this.options.onProgress?.(this.results.totalProcessed);
3762
+ getLogger().error(`Failed to update story: ${error.message}`, {
3763
+ storyId,
3764
+ errorCode: ERROR_CODES.MIGRATION_STORY_UPDATE_NULL
3765
+ });
3396
3766
  }
3397
- } catch (error) {
3767
+ } catch (maybeError) {
3768
+ const error = toError(maybeError);
3398
3769
  this.results.failed.push({
3399
3770
  storyId,
3400
3771
  name: storyName,
@@ -3402,6 +3773,11 @@ class UpdateStream extends Writable {
3402
3773
  });
3403
3774
  this.results.totalProcessed++;
3404
3775
  this.options.onProgress?.(this.results.totalProcessed);
3776
+ getLogger().error(error.message, {
3777
+ storyId,
3778
+ error,
3779
+ errorCode: ERROR_CODES.MIGRATION_STORY_UPDATE_ERROR
3780
+ });
3405
3781
  }
3406
3782
  }
3407
3783
  async _destroy(error, callback) {
@@ -3437,15 +3813,18 @@ class UpdateStream extends Writable {
3437
3813
  }
3438
3814
  }
3439
3815
 
3440
- const program$8 = getProgram();
3441
3816
  migrationsCommand.command("run [componentName]").description("Run migrations").option("--fi, --filter <filter>", "glob filter to apply to the components before pushing").option("-d, --dry-run", "Preview changes without applying them to Storyblok").option("-q, --query <query>", 'Filter stories by content attributes using Storyblok filter query syntax. Example: --query="[highlighted][in]=true"').option("--starts-with <path>", 'Filter stories by path. Example: --starts-with="/en/blog/"').option("--publish <publish>", "Options for publication mode: all | published | published-with-changes").action(async (componentName, options) => {
3442
- konsola.title(`${commands.MIGRATIONS}`, colorPalette.MIGRATIONS, componentName ? `Running migrations for component ${componentName}...` : "Running migrations...");
3817
+ const program = getProgram();
3818
+ const ui = getUI();
3819
+ const logger = getLogger();
3820
+ ui.title(`${commands.MIGRATIONS}`, colorPalette.MIGRATIONS, componentName ? `Running migrations for component ${componentName}...` : "Running migrations...");
3821
+ logger.info("Migration started");
3443
3822
  if (options.dryRun) {
3444
- konsola.warn(`DRY RUN MODE ENABLED: No changes will be made.
3823
+ ui.warn(`DRY RUN MODE ENABLED: No changes will be made.
3445
3824
  `);
3825
+ logger.warn("Dry run mode enabled");
3446
3826
  }
3447
- const verbose = program$8.opts().verbose;
3448
- const { filter, dryRun = false, query, startsWith, publish } = options;
3827
+ const verbose = program.opts().verbose;
3449
3828
  const { space, path } = migrationsCommand.opts();
3450
3829
  const { state, initializeSession } = session();
3451
3830
  await initializeSession();
@@ -3456,6 +3835,7 @@ migrationsCommand.command("run [componentName]").description("Run migrations").o
3456
3835
  handleError(new CommandError(`Please provide the space as argument --space YOUR_SPACE_ID.`), verbose);
3457
3836
  return;
3458
3837
  }
3838
+ const { filter, dryRun = false, query, startsWith, publish } = options;
3459
3839
  const { password, region } = state;
3460
3840
  mapiClient({
3461
3841
  token: {
@@ -3464,40 +3844,25 @@ migrationsCommand.command("run [componentName]").description("Run migrations").o
3464
3844
  region
3465
3845
  });
3466
3846
  try {
3467
- const spinner = new Spinner({
3468
- verbose: !isVitest
3469
- }).start(`Fetching migration files and stories...`);
3847
+ const spinner = ui.createSpinner(`Fetching migration files and stories...`);
3470
3848
  const migrationFiles = await readMigrationFiles({
3471
3849
  space,
3472
3850
  path,
3473
3851
  filter
3474
3852
  });
3475
- if (migrationFiles.length === 0) {
3476
- spinner.failed(`No migration files found for space "${space}"${filter ? ` matching filter "${filter}"` : ""}.`);
3477
- return;
3478
- }
3479
3853
  const filteredMigrations = componentName ? migrationFiles.filter((file) => {
3480
3854
  return file.name.match(new RegExp(`^${componentName}(\\..*)?.js$`));
3481
3855
  }) : migrationFiles;
3482
3856
  if (filteredMigrations.length === 0) {
3483
3857
  spinner.failed(`No migration files found${componentName ? ` for component "${componentName}"` : ""}${filter ? ` matching filter "${filter}"` : ""} in space "${space}".`);
3858
+ logger.warn("No migration files found");
3859
+ logger.info("Migration finished");
3484
3860
  return;
3485
3861
  }
3486
3862
  spinner.succeed(`Found ${filteredMigrations.length} migration files.`);
3487
- const multiBar = new MultiBar({
3488
- clearOnComplete: false,
3489
- format: `${chalk.bold(" {title} ")} ${chalk.hex(colorPalette.PRIMARY)("[{bar}]")} {percentage}% | {eta_formatted} | {value}/{total} processed`,
3490
- etaBuffer: 60
3491
- }, Presets.rect);
3492
- const storiesProgress = multiBar.create(0, 0, {
3493
- title: "Fetching Stories...".padEnd(19)
3494
- });
3495
- const migrationsProgress = multiBar.create(0, 0, {
3496
- title: "Applying Migrations".padEnd(19)
3497
- });
3498
- const updateProgress = multiBar.create(0, 0, {
3499
- title: "Updating Stories...".padEnd(19)
3500
- });
3863
+ const storiesProgress = ui.createProgressBar({ title: "Fetching Stories...".padEnd(19) });
3864
+ const migrationsProgress = ui.createProgressBar({ title: "Applying Migrations".padEnd(19) });
3865
+ const updateProgress = ui.createProgressBar({ title: "Updating Stories...".padEnd(19) });
3501
3866
  const storiesStream = await createStoriesStream({
3502
3867
  spaceId: space,
3503
3868
  params: {
@@ -3535,7 +3900,7 @@ migrationsCommand.command("run [componentName]").description("Run migrations").o
3535
3900
  updateProgress.increment();
3536
3901
  }
3537
3902
  });
3538
- return new Promise((resolve, reject) => {
3903
+ await new Promise((resolve, reject) => {
3539
3904
  pipeline(
3540
3905
  storiesStream,
3541
3906
  migrationStream,
@@ -3545,25 +3910,51 @@ migrationsCommand.command("run [componentName]").description("Run migrations").o
3545
3910
  reject(err);
3546
3911
  return;
3547
3912
  }
3548
- multiBar.stop();
3549
- const migrationSummary = migrationStream.getSummary();
3550
- konsola.info(migrationSummary);
3551
- const updateSummary = updateStream.getSummary();
3552
- konsola.info(updateSummary);
3553
3913
  resolve();
3554
3914
  }
3555
3915
  );
3556
3916
  });
3917
+ ui.stopAllProgressBars();
3918
+ const migrationSummary = migrationStream.getSummary();
3919
+ ui.info(migrationSummary);
3920
+ const updateSummary = updateStream.getSummary();
3921
+ ui.info(updateSummary);
3922
+ const migrationResults = migrationStream.getResults();
3923
+ const updateResults = updateStream.getResults();
3924
+ logger.info("Migration finished", {
3925
+ migrationResults: {
3926
+ total: migrationResults.totalProcessed,
3927
+ succeeded: migrationResults.successful.length,
3928
+ skipped: migrationResults.skipped.length,
3929
+ failed: migrationResults.failed.length
3930
+ },
3931
+ updateResults: {
3932
+ total: updateResults.totalProcessed,
3933
+ succeeded: updateResults.successful.length,
3934
+ failed: updateResults.failed.length
3935
+ }
3936
+ });
3557
3937
  } catch (error) {
3558
3938
  handleError(error, verbose);
3559
3939
  }
3560
3940
  });
3561
3941
 
3562
- const program$7 = getProgram();
3563
3942
  migrationsCommand.command("rollback [migrationFile]").description("Rollback a migration").action(async (migrationFile) => {
3564
- konsola.title(`${commands.MIGRATIONS}`, colorPalette.MIGRATIONS, `Rolling back migration ${chalk.hex(colorPalette.MIGRATIONS)(migrationFile)}...`);
3565
- const verbose = program$7.opts().verbose;
3943
+ const program = getProgram();
3944
+ const ui = getUI();
3945
+ const logger = getLogger();
3946
+ ui.title(
3947
+ `${commands.MIGRATIONS}`,
3948
+ colorPalette.MIGRATIONS,
3949
+ migrationFile ? `Rolling back migration ${chalk.hex(colorPalette.MIGRATIONS)(migrationFile)}...` : "Rolling back migration..."
3950
+ );
3951
+ const verbose = program.opts().verbose;
3566
3952
  const { space, path } = migrationsCommand.opts();
3953
+ logger.info("Migration rollback started", {
3954
+ migrationFile,
3955
+ space,
3956
+ path
3957
+ });
3567
3958
  const { state, initializeSession } = session();
3568
3959
  await initializeSession();
3569
3960
  if (!requireAuthentication(state, verbose)) {
@@ -3586,8 +3977,13 @@ migrationsCommand.command("rollback [migrationFile]").description("Rollback a mi
3586
3977
  path,
3587
3978
  migrationFile
3588
3979
  });
3980
+ const rollbackSummary = {
3981
+ total: rollbackData.stories.length,
3982
+ succeeded: 0,
3983
+ failed: 0
3984
+ };
3589
3985
  for (const story of rollbackData.stories) {
3590
- const spinner = new Spinner({ verbose }).start(`Restoring story ${chalk.hex(colorPalette.PRIMARY)(story.name || story.storyId)}...`);
3986
+ const spinner = ui.createSpinner(`Restoring story ${chalk.hex(colorPalette.PRIMARY)(story.name || story.storyId)}...`);
3591
3987
  try {
3592
3988
  const payload = {
3593
3989
  story: {
@@ -3603,18 +3999,37 @@ migrationsCommand.command("rollback [migrationFile]").description("Rollback a mi
3603
3999
  }
3604
4000
  }
3605
4001
  await updateStory(space, story.storyId, payload);
3606
- spinner.succeed(`Restored story ${chalk.hex(colorPalette.PRIMARY)(story.name || story.storyId)}`);
3607
- } catch (error) {
4002
+ rollbackSummary.succeeded += 1;
4003
+ spinner.succeed(`Restored story ${chalk.hex(colorPalette.PRIMARY)(story.name || story.storyId)} - Completed in ${spinner.elapsedTime.toFixed(2)}ms`);
4004
+ logger.info("Story restored", {
4005
+ storyId: story.storyId,
4006
+ migrationFile,
4007
+ space
4008
+ });
4009
+ } catch (maybeError) {
4010
+ const error = toError(maybeError);
4011
+ rollbackSummary.failed += 1;
3608
4012
  spinner.failed(`Failed to restore story ${chalk.hex(colorPalette.PRIMARY)(story.name || story.storyId)}: ${error.message}`);
4013
+ logger.error("Failed to restore story", {
4014
+ storyId: story.storyId,
4015
+ migrationFile,
4016
+ space,
4017
+ error
4018
+ });
3609
4019
  }
3610
4020
  }
4021
+ logger.info("Migration rollback finished", {
4022
+ migrationFile,
4023
+ space,
4024
+ results: rollbackSummary
4025
+ });
3611
4026
  } catch (error) {
3612
4027
  handleError(new CommandError(`Failed to rollback migration: ${error.message}`), verbose);
3613
4028
  }
3614
4029
  });
3615
4030
 
3616
- const program$6 = getProgram();
3617
- const typesCommand = program$6.command(commands.TYPES).alias("ts").description(`Generate types d.ts for your component schemas`).option("-s, --space <space>", "space ID").option("-p, --path <path>", "path to save the file. Default is .storyblok/types");
4031
+ const program$7 = getProgram();
4032
+ const typesCommand = program$7.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");
3618
4033
 
3619
4034
  const getAssetJSONSchema = (title) => ({
3620
4035
  $id: "#/asset",
@@ -4041,6 +4456,7 @@ const DEFAULT_TYPEDEFS_HEADER = [
4041
4456
  "// This file was generated by the storyblok CLI.",
4042
4457
  "// DO NOT MODIFY THIS FILE BY HAND."
4043
4458
  ];
4459
+ const getDatasourceTypeTitle = (slug) => `${toPascalCase(slug)}DataSource`;
4044
4460
  const getPropertyTypeAnnotation = (property, prefix, suffix) => {
4045
4461
  if (Array.from(storyblokSchemas.keys()).includes(property.type)) {
4046
4462
  return { type: property.type };
@@ -4149,6 +4565,13 @@ const getComponentPropertiesTypeAnnotations = async (component, options, spaceDa
4149
4565
  const componentType = toPascalCase(propertyType);
4150
4566
  propertyTypeAnnotation[key].tsType = `Storyblok${componentType}`;
4151
4567
  }
4568
+ if (spaceData.datasources.length > 0 && schema.source === "internal" && schema?.datasource_slug) {
4569
+ const datasourceExists = spaceData.datasources.some((ds) => ds.slug === schema.datasource_slug);
4570
+ if (datasourceExists) {
4571
+ const type = getDatasourceTypeTitle(schema.datasource_slug);
4572
+ propertyTypeAnnotation[key].tsType = propertyType === "options" ? `${type}[]` : type;
4573
+ }
4574
+ }
4152
4575
  if (propertyType === "multilink") {
4153
4576
  const excludedLinktypes = [
4154
4577
  ...!schema.email_link_type ? ['{ linktype?: "email" }'] : [],
@@ -4222,6 +4645,7 @@ const generateTypes = async (spaceData, options = {
4222
4645
  try {
4223
4646
  const typeDefs = [...DEFAULT_TYPEDEFS_HEADER];
4224
4647
  const storyblokPropertyTypes = /* @__PURE__ */ new Set();
4648
+ const contentTypeBloks = /* @__PURE__ */ new Set();
4225
4649
  let customFieldsParser;
4226
4650
  let compilerOptions;
4227
4651
  if (options.customFieldsParser) {
@@ -4230,8 +4654,11 @@ const generateTypes = async (spaceData, options = {
4230
4654
  if (options.compilerOptions) {
4231
4655
  compilerOptions = await loadCompilerOptions(options.compilerOptions);
4232
4656
  }
4233
- const schemas = await Promise.all(spaceData.components.map(async (component) => {
4657
+ const componentsSchema = spaceData.components.map(async (component) => {
4234
4658
  const type = getComponentType(component.name, options);
4659
+ if (component.is_root) {
4660
+ contentTypeBloks.add(type);
4661
+ }
4235
4662
  const componentPropertiesTypeAnnotations = await getComponentPropertiesTypeAnnotations(component, options, spaceData, customFieldsParser);
4236
4663
  const requiredFields = Object.entries(component?.schema || {}).reduce(
4237
4664
  (acc, [key, value]) => {
@@ -4270,7 +4697,35 @@ const generateTypes = async (spaceData, options = {
4270
4697
  }
4271
4698
  };
4272
4699
  return componentSchema;
4273
- }));
4700
+ });
4701
+ const resolvedComponentsSchema = await Promise.all(componentsSchema);
4702
+ const datasourcesSchema = spaceData.datasources.map(async (datasource) => {
4703
+ const allComponentTypes = resolvedComponentsSchema.map((schema) => schema.title);
4704
+ const enumValues = datasource.entries?.filter((d) => d.value).map((d) => d.value);
4705
+ const type = getDatasourceTypeTitle(datasource.slug);
4706
+ if (allComponentTypes.includes(type)) {
4707
+ console.warn(`Warning: Datasource type "${type}" conflicts with existing component type`);
4708
+ }
4709
+ const datasourceSchema = {
4710
+ $id: `#/${datasource.slug}`,
4711
+ title: type,
4712
+ type: "string",
4713
+ enum: enumValues
4714
+ };
4715
+ return datasourceSchema;
4716
+ });
4717
+ const resolvedDatasourcesSchema = await Promise.all(datasourcesSchema);
4718
+ const contentTypeSchema = {
4719
+ $id: `#/ContentType`,
4720
+ title: "ContentType",
4721
+ type: "string",
4722
+ tsType: contentTypeBloks.size > 0 ? `${Array.from(contentTypeBloks).join(" | ")}` : "never"
4723
+ };
4724
+ const schemas = [
4725
+ ...resolvedComponentsSchema,
4726
+ ...resolvedDatasourcesSchema,
4727
+ contentTypeSchema
4728
+ ];
4274
4729
  const result = await Promise.all(schemas.map(async (schema) => {
4275
4730
  return await compile(schema, schema.title || schema.$id.replace("#/", ""), {
4276
4731
  additionalProperties: !options.strict,
@@ -4299,61 +4754,229 @@ const generateTypes = async (spaceData, options = {
4299
4754
  handleError(error);
4300
4755
  }
4301
4756
  };
4302
- const saveTypesToComponentsFile = async (space, typedefString, options) => {
4303
- const { filename = DEFAULT_COMPONENT_FILENAME, path } = options;
4304
- const resolvedPath = path ? resolve(process.cwd(), path, "types", space) : resolvePath(path, `types/${space}`);
4305
- try {
4306
- await saveToFile(join(resolvedPath, `${filename}.d.ts`), typedefString);
4307
- } catch (error) {
4308
- handleFileSystemError("write", error);
4757
+ const saveTypesToComponentsFile = async (space, typedefString, options) => {
4758
+ const { filename = DEFAULT_COMPONENT_FILENAME, path } = options;
4759
+ const resolvedPath = path ? resolve(process.cwd(), path, "types", space) : resolvePath(path, `types/${space}`);
4760
+ try {
4761
+ await saveToFile(join(resolvedPath, `${filename}.d.ts`), typedefString);
4762
+ } catch (error) {
4763
+ handleFileSystemError("write", error);
4764
+ }
4765
+ };
4766
+ const generateStoryblokTypes = async (options = {}) => {
4767
+ const { path } = options;
4768
+ try {
4769
+ const storyblokTypesPath = resolve(__dirname, "./index.d.ts");
4770
+ const storyblokTypesContent = readFileSync(storyblokTypesPath, "utf-8");
4771
+ const typeDefs = [
4772
+ "// This file was generated by the Storyblok CLI.",
4773
+ "// DO NOT MODIFY THIS FILE BY HAND.",
4774
+ `import type { ${STORY_TYPE} } from '@storyblok/js';`,
4775
+ storyblokTypesContent
4776
+ ].join("\n");
4777
+ const resolvedPath = path ? resolve(process.cwd(), path, "types") : resolvePath(path, "types");
4778
+ await saveToFile(join(resolvedPath, `storyblok.d.ts`), typeDefs);
4779
+ return true;
4780
+ } catch (error) {
4781
+ handleFileSystemError("read", error);
4782
+ return false;
4783
+ }
4784
+ };
4785
+
4786
+ const pushDatasource = async (spaceId, datasource) => {
4787
+ try {
4788
+ const client = mapiClient();
4789
+ const { data } = await client.datasources.create({
4790
+ path: {
4791
+ space_id: spaceId
4792
+ },
4793
+ body: { datasource },
4794
+ throwOnError: true
4795
+ });
4796
+ return data.datasource;
4797
+ } catch (error) {
4798
+ handleAPIError("push_datasource", error, `Failed to push datasource ${datasource.name}`);
4799
+ }
4800
+ };
4801
+ const updateDatasource = async (spaceId, datasourceId, datasource) => {
4802
+ try {
4803
+ const client = mapiClient();
4804
+ const { data } = await client.datasources.update({
4805
+ path: {
4806
+ space_id: spaceId,
4807
+ datasource_id: datasourceId
4808
+ },
4809
+ body: {
4810
+ datasource
4811
+ },
4812
+ throwOnError: true
4813
+ });
4814
+ return data.datasource;
4815
+ } catch (error) {
4816
+ handleAPIError("update_datasource", error, `Failed to update datasource ${datasource.name}`);
4817
+ }
4818
+ };
4819
+ const upsertDatasource = async (space, datasource, existingId) => {
4820
+ if (existingId) {
4821
+ return await updateDatasource(space, existingId, datasource);
4822
+ } else {
4823
+ return await pushDatasource(space, datasource);
4824
+ }
4825
+ };
4826
+ const pushDatasourceEntry = async (spaceId, datasourceId, entry) => {
4827
+ try {
4828
+ const client = mapiClient();
4829
+ const { data } = await client.datasourceEntries.create({
4830
+ path: {
4831
+ space_id: spaceId
4832
+ },
4833
+ body: {
4834
+ datasource_entry: {
4835
+ ...entry,
4836
+ datasource_id: datasourceId
4837
+ }
4838
+ },
4839
+ throwOnError: true
4840
+ });
4841
+ return data.datasource_entry;
4842
+ } catch (error) {
4843
+ handleAPIError("push_datasource", error, `Failed to push datasource entry ${entry.name}`);
4844
+ }
4845
+ };
4846
+ const updateDatasourceEntry = async (spaceId, entryId, entry) => {
4847
+ try {
4848
+ const client = mapiClient();
4849
+ await client.datasourceEntries.updateDatasourceEntry({
4850
+ path: {
4851
+ space_id: spaceId,
4852
+ datasource_entry_id: entryId
4853
+ },
4854
+ body: {
4855
+ datasource_entry: entry
4856
+ },
4857
+ throwOnError: true
4858
+ });
4859
+ } catch (error) {
4860
+ handleAPIError("update_datasource", error, `Failed to update datasource entry ${entry.name}`);
4861
+ }
4862
+ };
4863
+ const upsertDatasourceEntry = async (space, datasourceId, entry, existingId) => {
4864
+ if (existingId) {
4865
+ await updateDatasourceEntry(space, existingId, entry);
4866
+ return void 0;
4867
+ } else {
4868
+ return await pushDatasourceEntry(space, datasourceId, entry);
4869
+ }
4870
+ };
4871
+ const readDatasourcesFiles = async (options) => {
4872
+ const { from, path, separateFiles = false, suffix, space } = options;
4873
+ const resolvedPath = resolvePath(path, `datasources/${from}`);
4874
+ try {
4875
+ await readdir(resolvedPath);
4876
+ } catch (error) {
4877
+ const message = `No local datasources found for space ${chalk.bold(from)}. To push datasources, you need to pull them first:
4878
+
4879
+ 1. Pull the datasources from your source space:
4880
+ ${chalk.cyan(`storyblok datasources pull --space ${from}`)}
4881
+
4882
+ 2. Then try pushing again:
4883
+ ${chalk.cyan(`storyblok datasources push --space ${space} --from ${from}`)}`;
4884
+ throw new FileSystemError(
4885
+ "file_not_found",
4886
+ "read",
4887
+ error,
4888
+ message
4889
+ );
4890
+ }
4891
+ if (separateFiles) {
4892
+ return await readSeparateFiles(resolvedPath, suffix);
4893
+ }
4894
+ return await readConsolidatedFiles(resolvedPath, suffix);
4895
+ };
4896
+ async function readSeparateFiles(resolvedPath, suffix) {
4897
+ const files = await readdir(resolvedPath);
4898
+ const datasources = [];
4899
+ const filteredFiles = files.filter((file) => {
4900
+ if (suffix) {
4901
+ return file.endsWith(`.${suffix}.json`);
4902
+ } else {
4903
+ return !/\.\w+\.json$/.test(file);
4904
+ }
4905
+ });
4906
+ for (const file of filteredFiles) {
4907
+ const filePath = join(resolvedPath, file);
4908
+ if (file.endsWith(".json") || file.endsWith(`${suffix}.json`)) {
4909
+ if (file === "datasources.json" || /^datasources\.\w+\.json$/.test(file)) {
4910
+ continue;
4911
+ }
4912
+ const result = await readJsonFile(filePath);
4913
+ if (result.error) {
4914
+ handleFileSystemError("read", result.error);
4915
+ continue;
4916
+ }
4917
+ datasources.push(...result.data);
4918
+ }
4309
4919
  }
4310
- };
4311
- const generateStoryblokTypes = async (options = {}) => {
4312
- const { path } = options;
4313
- try {
4314
- const storyblokTypesPath = resolve(__dirname, "./index.d.ts");
4315
- const storyblokTypesContent = readFileSync(storyblokTypesPath, "utf-8");
4316
- const typeDefs = [
4317
- "// This file was generated by the Storyblok CLI.",
4318
- "// DO NOT MODIFY THIS FILE BY HAND.",
4319
- `import type { ${STORY_TYPE} } from '@storyblok/js';`,
4320
- storyblokTypesContent
4321
- ].join("\n");
4322
- const resolvedPath = path ? resolve(process.cwd(), path, "types") : resolvePath(path, "types");
4323
- await saveToFile(join(resolvedPath, `storyblok.d.ts`), typeDefs);
4324
- return true;
4325
- } catch (error) {
4326
- handleFileSystemError("read", error);
4327
- return false;
4920
+ return {
4921
+ datasources
4922
+ };
4923
+ }
4924
+ async function readConsolidatedFiles(resolvedPath, suffix) {
4925
+ const datasourcesPath = join(resolvedPath, suffix ? `datasources.${suffix}.json` : "datasources.json");
4926
+ const datasourcesResult = await readJsonFile(datasourcesPath);
4927
+ if (datasourcesResult.error || !datasourcesResult.data.length) {
4928
+ throw new FileSystemError(
4929
+ "file_not_found",
4930
+ "read",
4931
+ datasourcesResult.error || new Error("Datasources file is empty"),
4932
+ `No datasources found in ${datasourcesPath}. Please make sure you have pulled the datasources first.`
4933
+ );
4328
4934
  }
4329
- };
4935
+ return {
4936
+ datasources: datasourcesResult.data
4937
+ };
4938
+ }
4330
4939
 
4331
- const program$5 = getProgram();
4940
+ const program$6 = getProgram();
4332
4941
  typesCommand.command("generate").description("Generate types d.ts for your component schemas").option("--sf, --separate-files", "Generate one .d.ts file per component instead of a single combined file").option(
4333
4942
  "--filename <name>",
4334
4943
  "Base file name for all component types when generating a single declarations file (e.g. components.d.ts). Ignored when using --separate-files."
4335
4944
  ).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) => {
4336
4945
  konsola.title(`${commands.TYPES}`, colorPalette.TYPES, "Generating types...");
4337
- const verbose = program$5.opts().verbose;
4946
+ const verbose = program$6.opts().verbose;
4338
4947
  const { space, path } = typesCommand.opts();
4339
4948
  const spinner = new Spinner({
4340
4949
  verbose: !isVitest
4341
4950
  });
4342
4951
  try {
4343
4952
  spinner.start(`Generating types...`);
4344
- const spaceData = await readComponentsFiles({
4953
+ const componentsData = await readComponentsFiles({
4345
4954
  ...options,
4346
4955
  from: space,
4347
4956
  path
4348
4957
  });
4958
+ let dataSourceData;
4959
+ try {
4960
+ dataSourceData = await readDatasourcesFiles({
4961
+ ...options,
4962
+ from: space,
4963
+ path
4964
+ });
4965
+ } catch (error) {
4966
+ if (error instanceof FileSystemError && error.errorId === "file_not_found") {
4967
+ dataSourceData = { datasources: [] };
4968
+ } else {
4969
+ throw error;
4970
+ }
4971
+ }
4349
4972
  await generateStoryblokTypes({
4350
4973
  path
4351
4974
  });
4352
- const spaceDataWithDatasources = {
4353
- ...spaceData,
4354
- datasources: []
4975
+ const spaceDataWithComponentsAndDatasources = {
4976
+ ...componentsData,
4977
+ ...dataSourceData
4355
4978
  };
4356
- const typedefString = await generateTypes(spaceDataWithDatasources, {
4979
+ const typedefString = await generateTypes(spaceDataWithComponentsAndDatasources, {
4357
4980
  ...options,
4358
4981
  path
4359
4982
  });
@@ -4373,8 +4996,8 @@ typesCommand.command("generate").description("Generate types d.ts for your compo
4373
4996
  }
4374
4997
  });
4375
4998
 
4376
- const program$4 = getProgram();
4377
- const datasourcesCommand = program$4.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");
4999
+ const program$5 = getProgram();
5000
+ const datasourcesCommand = program$5.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");
4378
5001
 
4379
5002
  async function fetchAllPages(fetchFunction, extractDataFunction, page = 1, collectedItems = []) {
4380
5003
  const { data, response } = await fetchFunction(page);
@@ -4480,10 +5103,10 @@ const saveDatasourcesToFiles = async (space, datasources, options) => {
4480
5103
  }
4481
5104
  };
4482
5105
 
4483
- const program$3 = getProgram();
5106
+ const program$4 = getProgram();
4484
5107
  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) => {
4485
5108
  konsola.title(`${commands.DATASOURCES}`, colorPalette.DATASOURCES, datasourceName ? `Pulling datasource ${datasourceName}...` : "Pulling datasources...");
4486
- const verbose = program$3.opts().verbose;
5109
+ const verbose = program$4.opts().verbose;
4487
5110
  const { space, path } = datasourcesCommand.opts();
4488
5111
  const { separateFiles, suffix, filename = "datasources" } = options;
4489
5112
  const { state, initializeSession } = session();
@@ -4552,164 +5175,10 @@ datasourcesCommand.command("pull [datasourceName]").option("-f, --filename <file
4552
5175
  }
4553
5176
  });
4554
5177
 
4555
- const pushDatasource = async (spaceId, datasource) => {
4556
- try {
4557
- const client = mapiClient();
4558
- const { data } = await client.datasources.create({
4559
- path: {
4560
- space_id: spaceId
4561
- },
4562
- body: { datasource },
4563
- throwOnError: true
4564
- });
4565
- return data.datasource;
4566
- } catch (error) {
4567
- handleAPIError("push_datasource", error, `Failed to push datasource ${datasource.name}`);
4568
- }
4569
- };
4570
- const updateDatasource = async (spaceId, datasourceId, datasource) => {
4571
- try {
4572
- const client = mapiClient();
4573
- const { data } = await client.datasources.update({
4574
- path: {
4575
- space_id: spaceId,
4576
- datasource_id: datasourceId
4577
- },
4578
- body: {
4579
- datasource
4580
- },
4581
- throwOnError: true
4582
- });
4583
- return data.datasource;
4584
- } catch (error) {
4585
- handleAPIError("update_datasource", error, `Failed to update datasource ${datasource.name}`);
4586
- }
4587
- };
4588
- const upsertDatasource = async (space, datasource, existingId) => {
4589
- if (existingId) {
4590
- return await updateDatasource(space, existingId, datasource);
4591
- } else {
4592
- return await pushDatasource(space, datasource);
4593
- }
4594
- };
4595
- const pushDatasourceEntry = async (spaceId, datasourceId, entry) => {
4596
- try {
4597
- const client = mapiClient();
4598
- const { data } = await client.datasourceEntries.create({
4599
- path: {
4600
- space_id: spaceId
4601
- },
4602
- body: {
4603
- datasource_entry: {
4604
- ...entry,
4605
- datasource_id: datasourceId
4606
- }
4607
- },
4608
- throwOnError: true
4609
- });
4610
- return data.datasource_entry;
4611
- } catch (error) {
4612
- handleAPIError("push_datasource", error, `Failed to push datasource entry ${entry.name}`);
4613
- }
4614
- };
4615
- const updateDatasourceEntry = async (spaceId, entryId, entry) => {
4616
- try {
4617
- const client = mapiClient();
4618
- await client.datasourceEntries.updateDatasourceEntry({
4619
- path: {
4620
- space_id: spaceId,
4621
- datasource_entry_id: entryId
4622
- },
4623
- body: {
4624
- datasource_entry: entry
4625
- },
4626
- throwOnError: true
4627
- });
4628
- } catch (error) {
4629
- handleAPIError("update_datasource", error, `Failed to update datasource entry ${entry.name}`);
4630
- }
4631
- };
4632
- const upsertDatasourceEntry = async (space, datasourceId, entry, existingId) => {
4633
- if (existingId) {
4634
- await updateDatasourceEntry(space, existingId, entry);
4635
- return void 0;
4636
- } else {
4637
- return await pushDatasourceEntry(space, datasourceId, entry);
4638
- }
4639
- };
4640
- const readDatasourcesFiles = async (options) => {
4641
- const { from, path, separateFiles = false, suffix, space } = options;
4642
- const resolvedPath = resolvePath(path, `datasources/${from}`);
4643
- try {
4644
- await readdir(resolvedPath);
4645
- } catch (error) {
4646
- const message = `No local datasources found for space ${chalk.bold(from)}. To push datasources, you need to pull them first:
4647
-
4648
- 1. Pull the datasources from your source space:
4649
- ${chalk.cyan(`storyblok datasources pull --space ${from}`)}
4650
-
4651
- 2. Then try pushing again:
4652
- ${chalk.cyan(`storyblok datasources push --space ${space} --from ${from}`)}`;
4653
- throw new FileSystemError(
4654
- "file_not_found",
4655
- "read",
4656
- error,
4657
- message
4658
- );
4659
- }
4660
- if (separateFiles) {
4661
- return await readSeparateFiles(resolvedPath, suffix);
4662
- }
4663
- return await readConsolidatedFiles(resolvedPath, suffix);
4664
- };
4665
- async function readSeparateFiles(resolvedPath, suffix) {
4666
- const files = await readdir(resolvedPath);
4667
- const datasources = [];
4668
- const filteredFiles = files.filter((file) => {
4669
- if (suffix) {
4670
- return file.endsWith(`.${suffix}.json`);
4671
- } else {
4672
- return !/\.\w+\.json$/.test(file);
4673
- }
4674
- });
4675
- for (const file of filteredFiles) {
4676
- const filePath = join(resolvedPath, file);
4677
- if (file.endsWith(".json") || file.endsWith(`${suffix}.json`)) {
4678
- if (file === "datasources.json" || /^datasources\.\w+\.json$/.test(file)) {
4679
- continue;
4680
- }
4681
- const result = await readJsonFile(filePath);
4682
- if (result.error) {
4683
- handleFileSystemError("read", result.error);
4684
- continue;
4685
- }
4686
- datasources.push(...result.data);
4687
- }
4688
- }
4689
- return {
4690
- datasources
4691
- };
4692
- }
4693
- async function readConsolidatedFiles(resolvedPath, suffix) {
4694
- const datasourcesPath = join(resolvedPath, suffix ? `datasources.${suffix}.json` : "datasources.json");
4695
- const datasourcesResult = await readJsonFile(datasourcesPath);
4696
- if (datasourcesResult.error || !datasourcesResult.data.length) {
4697
- throw new FileSystemError(
4698
- "file_not_found",
4699
- "read",
4700
- datasourcesResult.error || new Error("Datasources file is empty"),
4701
- `No datasources found in ${datasourcesPath}. Please make sure you have pulled the datasources first.`
4702
- );
4703
- }
4704
- return {
4705
- datasources: datasourcesResult.data
4706
- };
4707
- }
4708
-
4709
- const program$2 = getProgram();
5178
+ const program$3 = getProgram();
4710
5179
  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) => {
4711
5180
  konsola.title(`${commands.DATASOURCES}`, colorPalette.DATASOURCES, datasourceName ? `Pushing datasource ${datasourceName}...` : "Pushing datasources...");
4712
- const verbose = program$2.opts().verbose;
5181
+ const verbose = program$3.opts().verbose;
4713
5182
  const { space, path } = datasourcesCommand.opts();
4714
5183
  const { from, filter } = options;
4715
5184
  const { state, initializeSession } = session();
@@ -5026,10 +5495,10 @@ const fetchBlueprintRepositories = async () => {
5026
5495
  }
5027
5496
  };
5028
5497
 
5029
- const program$1 = getProgram();
5030
- program$1.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").action(async (projectPath, options) => {
5498
+ const program$2 = getProgram();
5499
+ program$2.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").action(async (projectPath, options) => {
5031
5500
  konsola.title(`${commands.CREATE}`, colorPalette.CREATE);
5032
- const verbose = program$1.opts().verbose;
5501
+ const verbose = program$2.opts().verbose;
5033
5502
  const { template, blueprint } = options;
5034
5503
  let selectedTemplate = template;
5035
5504
  if (blueprint && !template) {
@@ -5217,7 +5686,31 @@ program$1.command(`${commands.CREATE} [project-path]`).alias("c").description(`S
5217
5686
  konsola.br();
5218
5687
  });
5219
5688
 
5220
- const version = "4.6.14";
5689
+ const program$1 = getProgram();
5690
+ const logsCommand = program$1.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'.");
5691
+
5692
+ logsCommand.command("list").description("List logs").action(async () => {
5693
+ const { space, path } = logsCommand.opts();
5694
+ const ui = getUI();
5695
+ const logsPath = getLogsPath(directories.log, space, path);
5696
+ const logFiles = FileTransport.listLogFiles(logsPath);
5697
+ if (logFiles.length === 0) {
5698
+ ui.info(`No logs found for space "${space}".`);
5699
+ return;
5700
+ }
5701
+ ui.info(`Found ${logFiles.length} log file${logFiles.length === 1 ? "" : "s"} for space "${space}":`);
5702
+ ui.list(logFiles);
5703
+ });
5704
+
5705
+ 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 }) => {
5706
+ const { space, path } = logsCommand.opts();
5707
+ const ui = getUI();
5708
+ const logsPath = getLogsPath(directories.log, space, path);
5709
+ const deletedFilesCount = FileTransport.pruneLogFiles(logsPath, keep);
5710
+ ui.info(`Deleted ${deletedFilesCount} log file${deletedFilesCount === 1 ? "" : "s"}`);
5711
+ });
5712
+
5713
+ const version = "4.8.0";
5221
5714
  const pkg = {
5222
5715
  version: version};
5223
5716