storyblok 4.7.0 → 4.9.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();
@@ -1625,7 +1952,7 @@ function buildDependencyGraph(context) {
1625
1952
  graph.nodes.set(nodeId, node);
1626
1953
  });
1627
1954
  spaceState.local.groups.forEach((group) => {
1628
- if (group.parent_uuid) {
1955
+ if (group.parent_uuid && group.parent_id && group.parent_uuid !== group.uuid) {
1629
1956
  const childId = `group:${group.uuid}`;
1630
1957
  const parentId = `group:${group.parent_uuid}`;
1631
1958
  addDependency(childId, parentId);
@@ -2520,12 +2847,13 @@ 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
- const { from, filter } = options;
2855
+ const { filter } = options;
2856
+ const fromSpace = options.from || space;
2529
2857
  const { state, initializeSession } = session();
2530
2858
  await initializeSession();
2531
2859
  if (!requireAuthentication(state, verbose)) {
@@ -2535,10 +2863,7 @@ componentsCommand.command("push [componentName]").description(`Push your space's
2535
2863
  handleError(new CommandError(`Please provide the target space as argument --space TARGET_SPACE_ID.`), verbose);
2536
2864
  return;
2537
2865
  }
2538
- if (!from) {
2539
- options.from = space;
2540
- }
2541
- konsola.info(`Attempting to push components ${chalk.bold("from")} space ${chalk.hex(colorPalette.COMPONENTS)(options.from)} ${chalk.bold("to")} ${chalk.hex(colorPalette.COMPONENTS)(space)}`);
2866
+ konsola.info(`Attempting to push components ${chalk.bold("from")} space ${chalk.hex(colorPalette.COMPONENTS)(fromSpace)} ${chalk.bold("to")} ${chalk.hex(colorPalette.COMPONENTS)(space)}`);
2542
2867
  konsola.br();
2543
2868
  const { password, region } = state;
2544
2869
  let requestCount = 0;
@@ -2556,7 +2881,7 @@ componentsCommand.command("push [componentName]").description(`Push your space's
2556
2881
  const componentsData = await readComponentsFiles({
2557
2882
  ...options,
2558
2883
  path,
2559
- space
2884
+ from: fromSpace
2560
2885
  });
2561
2886
  const localData = {
2562
2887
  ...componentsData,
@@ -2719,11 +3044,11 @@ const saveLanguagesToFile = async (space, internationalizationOptions, options)
2719
3044
  }
2720
3045
  };
2721
3046
 
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");
3047
+ const program$9 = getProgram();
3048
+ 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
3049
  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
3050
  konsola.title(`${commands.LANGUAGES}`, colorPalette.LANGUAGES);
2726
- const verbose = program$b.opts().verbose;
3051
+ const verbose = program$9.opts().verbose;
2727
3052
  const { space, path } = languagesCommand.opts();
2728
3053
  const { filename = "languages", suffix = options.space } = options;
2729
3054
  const { state, initializeSession } = session();
@@ -2770,8 +3095,8 @@ languagesCommand.command("pull").description(`Download your space's languages sc
2770
3095
  konsola.br();
2771
3096
  });
2772
3097
 
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");
3098
+ const program$8 = getProgram();
3099
+ 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
3100
 
2776
3101
  const getMigrationTemplate = () => {
2777
3102
  return `export default function (block) {
@@ -2799,12 +3124,19 @@ const generateMigration = async (space, path, component, suffix) => {
2799
3124
  }
2800
3125
  };
2801
3126
 
2802
- const program$9 = getProgram();
2803
3127
  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;
3128
+ const program = getProgram();
3129
+ const ui = getUI();
3130
+ const logger = getLogger();
3131
+ ui.title(`${commands.MIGRATIONS}`, colorPalette.MIGRATIONS, componentName ? `Generating migration for component ${componentName}...` : "Generating migrations...");
3132
+ const verbose = program.opts().verbose;
2806
3133
  const { space, path } = migrationsCommand.opts();
2807
3134
  const { suffix } = options;
3135
+ logger.info("Migration generation started", {
3136
+ componentName,
3137
+ space,
3138
+ suffix
3139
+ });
2808
3140
  if (!componentName) {
2809
3141
  handleError(new CommandError(`Please provide the component name as argument ${chalk.hex(colorPalette.MIGRATIONS)("storyblok migrations generate YOUR_COMPONENT_NAME.")}`), verbose);
2810
3142
  return;
@@ -2825,9 +3157,7 @@ migrationsCommand.command("generate [componentName]").description("Generate a mi
2825
3157
  },
2826
3158
  region
2827
3159
  });
2828
- const spinner = new Spinner({
2829
- verbose: !isVitest
2830
- }).start(`Generating migration for component ${componentName}...`);
3160
+ const spinner = ui.createSpinner(`Generating migration for component ${componentName}...`);
2831
3161
  try {
2832
3162
  const component = await fetchComponent(space, componentName);
2833
3163
  if (!component) {
@@ -2839,7 +3169,13 @@ migrationsCommand.command("generate [componentName]").description("Generate a mi
2839
3169
  spinner.succeed(`Migration generated for component ${chalk.hex(colorPalette.MIGRATIONS)(componentName)} - Completed in ${spinner.elapsedTime.toFixed(2)}ms`);
2840
3170
  const fileName = suffix ? `${component.name}.${suffix}.js` : `${component.name}.js`;
2841
3171
  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)}`);
3172
+ ui.ok(`You can find the migration file in ${chalk.hex(colorPalette.MIGRATIONS)(migrationPath)}`);
3173
+ logger.info("Migration generation finished", {
3174
+ componentName: component.name,
3175
+ migrationPath,
3176
+ space,
3177
+ suffix
3178
+ });
2843
3179
  } catch (error) {
2844
3180
  spinner.failed(`Failed to generate migration for component ${componentName}`);
2845
3181
  handleError(error, verbose);
@@ -2904,6 +3240,18 @@ const updateStory = async (spaceId, storyId, payload) => {
2904
3240
  }
2905
3241
  };
2906
3242
 
3243
+ const ERROR_CODES = {
3244
+ MIGRATION_APPLY_TO_STORY_ERROR: "MIGRATION_APPLY_TO_STORY_ERROR",
3245
+ MIGRATION_CREATE_STORIES_PIPELINE_ERROR: "MIGRATION_CREATE_STORIES_PIPELINE_ERROR",
3246
+ MIGRATION_FILE_NO_DEFAULT_EXPORT: "MIGRATION_FILE_NO_DEFAULT_EXPORT",
3247
+ MIGRATION_FILE_NOT_FOUND: "MIGRATION_FILE_NOT_FOUND",
3248
+ MIGRATION_LOAD_ERROR: "MIGRATION_LOAD_ERROR",
3249
+ MIGRATION_STORY_CONTENT_MISSING: "MIGRATION_STORY_CONTENT_MISSING",
3250
+ MIGRATION_STORY_FETCH_ERROR: "MIGRATION_STORY_FETCH_ERROR",
3251
+ MIGRATION_STORY_UPDATE_ERROR: "MIGRATION_STORY_UPDATE_ERROR",
3252
+ MIGRATION_STORY_UPDATE_NULL: "MIGRATION_STORY_UPDATE_NULL"
3253
+ };
3254
+
2907
3255
  async function* storiesIterator(spaceId, params, onTotal) {
2908
3256
  try {
2909
3257
  let perPage = 500;
@@ -2924,6 +3272,7 @@ async function* storiesIterator(spaceId, params, onTotal) {
2924
3272
  page: 1,
2925
3273
  story_only: true
2926
3274
  });
3275
+ getLogger().info(`Fetched stories page 1 of ${perPage}`);
2927
3276
  if (!result) {
2928
3277
  return;
2929
3278
  }
@@ -2941,6 +3290,7 @@ async function* storiesIterator(spaceId, params, onTotal) {
2941
3290
  page,
2942
3291
  story_only: true
2943
3292
  });
3293
+ getLogger().info(`Fetched stories page ${page} of ${perPage}`);
2944
3294
  if (!result2) {
2945
3295
  return;
2946
3296
  }
@@ -2965,14 +3315,20 @@ class StoriesStream extends Transform {
2965
3315
  }
2966
3316
  semaphore;
2967
3317
  async _transform(chunk, _encoding, callback) {
2968
- await this.semaphore.acquire();
2969
- fetchStory(this.spaceId, chunk.id.toString()).then((story) => {
3318
+ try {
3319
+ await this.semaphore.acquire();
3320
+ const story = await fetchStory(this.spaceId, chunk.id.toString());
2970
3321
  this.push(story);
2971
3322
  this.onProgress?.();
2972
- }).finally(() => {
3323
+ getLogger().info("Fetched story", { storyId: chunk.id });
3324
+ callback();
3325
+ } catch (maybeError) {
3326
+ const error = toError(maybeError);
3327
+ getLogger().error(error.message, { storyId: chunk.id, error, errorCode: ERROR_CODES.MIGRATION_STORY_FETCH_ERROR });
3328
+ callback(error);
3329
+ } finally {
2973
3330
  this.semaphore.release();
2974
- });
2975
- callback();
3331
+ }
2976
3332
  }
2977
3333
  _flush(callback) {
2978
3334
  this.semaphore.drain().then(() => {
@@ -2992,37 +3348,14 @@ const createStoriesStream = async ({
2992
3348
  return pipeline(listStoriesStream, new StoriesStream(spaceId, batchSize, onProgress), (err) => {
2993
3349
  if (err) {
2994
3350
  console.error(err);
3351
+ getLogger().error(err.message, { errorCode: ERROR_CODES.MIGRATION_CREATE_STORIES_PIPELINE_ERROR });
2995
3352
  }
2996
3353
  });
2997
3354
  };
2998
3355
 
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
3356
  async function readMigrationFiles(options) {
3011
3357
  const { space, path, filter } = options;
3012
3358
  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
3359
  try {
3027
3360
  const dirFiles = await readdir(resolvedPath);
3028
3361
  const migrationFiles = [];
@@ -3035,20 +3368,21 @@ async function readMigrationFiles(options) {
3035
3368
  if (filterRegex && !filterRegex.test(file)) {
3036
3369
  continue;
3037
3370
  }
3038
- const filePath = join(resolvedPath, file);
3039
- const content = await readJavascriptFile(filePath);
3040
3371
  migrationFiles.push({
3041
- name: file,
3042
- content
3372
+ name: file
3043
3373
  });
3044
3374
  }
3045
3375
  }
3046
3376
  return migrationFiles;
3047
3377
  } catch (error) {
3378
+ const message = `No directory found for space "${space}". Please make sure you have generated migrations first by running:
3379
+
3380
+ storyblok migrations generate YOUR_COMPONENT_NAME --space ${space}`;
3048
3381
  throw new FileSystemError(
3049
3382
  "file_not_found",
3050
3383
  "read",
3051
- error
3384
+ error,
3385
+ message
3052
3386
  );
3053
3387
  }
3054
3388
  }
@@ -3056,14 +3390,24 @@ async function getMigrationFunction(fileName, space, basePath) {
3056
3390
  try {
3057
3391
  const resolvedPath = resolvePath(basePath, `migrations/${space}`);
3058
3392
  const filePath = join(resolvedPath, fileName);
3059
- const migrationModule = await import(`file://${filePath}`);
3393
+ const migrationModule = await importModule(filePath);
3060
3394
  if (typeof migrationModule.default === "function") {
3061
3395
  return migrationModule.default;
3062
3396
  }
3063
- konsola.error(`Migration file "${fileName}" does not export a default function.`);
3397
+ getUI().error(`Migration file "${fileName}" does not export a default function.`);
3398
+ getLogger().error("Migration file does not export a default function", {
3399
+ fileName,
3400
+ errorCode: ERROR_CODES.MIGRATION_FILE_NO_DEFAULT_EXPORT
3401
+ });
3064
3402
  return null;
3065
- } catch (error) {
3066
- konsola.error(`Error loading migration function from "${fileName}": ${error.message}`);
3403
+ } catch (maybeError) {
3404
+ const error = toError(maybeError);
3405
+ getUI().error(`Error loading migration function from "${fileName}": ${error.message}`);
3406
+ getLogger().error("Couldn't load migration function", {
3407
+ fileName,
3408
+ error,
3409
+ errorCode: ERROR_CODES.MIGRATION_LOAD_ERROR
3410
+ });
3067
3411
  return null;
3068
3412
  }
3069
3413
  }
@@ -3200,6 +3544,10 @@ class MigrationStream extends Transform {
3200
3544
  migrationNames: relevantMigrations.map((m) => m.name),
3201
3545
  error: new Error("Story content is missing")
3202
3546
  });
3547
+ getLogger().error("Failed to process story: Content is missing", {
3548
+ storyId: story.id,
3549
+ errorCode: ERROR_CODES.MIGRATION_STORY_CONTENT_MISSING
3550
+ });
3203
3551
  return [];
3204
3552
  }
3205
3553
  const successfulResults = [];
@@ -3218,10 +3566,14 @@ class MigrationStream extends Transform {
3218
3566
  for (const migrationFile of migrationFiles) {
3219
3567
  const migrationFunction = await this.getOrLoadMigrationFunction(migrationFile);
3220
3568
  if (!migrationFunction) {
3569
+ const error = new Error(`Failed to load migration function from file "${migrationFile.name}"`);
3221
3570
  this.results.failed.push({
3222
3571
  storyId: story.id,
3223
3572
  migrationNames,
3224
- error: new Error(`Failed to load migration function from file "${migrationFile.name}"`)
3573
+ error
3574
+ });
3575
+ getLogger().error(error.message, {
3576
+ errorCode: ERROR_CODES.MIGRATION_FILE_NOT_FOUND
3225
3577
  });
3226
3578
  return null;
3227
3579
  }
@@ -3251,6 +3603,7 @@ class MigrationStream extends Transform {
3251
3603
  migrationNames,
3252
3604
  content: storyContent
3253
3605
  });
3606
+ getLogger().info("Applied migration", { storyId: story.id, migrationNames });
3254
3607
  return {
3255
3608
  storyId: story.id,
3256
3609
  name: story.name,
@@ -3265,6 +3618,7 @@ class MigrationStream extends Transform {
3265
3618
  migrationNames,
3266
3619
  reason: "No changes detected after migration"
3267
3620
  });
3621
+ getLogger().info("Skipped migration: No changes detected", { storyId: story.id, migrationNames });
3268
3622
  return null;
3269
3623
  } else {
3270
3624
  const reason = migrationFiles.map((migrationFile) => {
@@ -3278,14 +3632,22 @@ class MigrationStream extends Transform {
3278
3632
  migrationNames,
3279
3633
  reason
3280
3634
  });
3635
+ getLogger().info(`Skipped migration: ${reason}`, { storyId: story.id, migrationNames });
3281
3636
  return null;
3282
3637
  }
3283
- } catch (error) {
3638
+ } catch (maybeError) {
3639
+ const error = toError(maybeError);
3284
3640
  this.results.failed.push({
3285
3641
  storyId: story.id,
3286
3642
  migrationNames,
3287
3643
  error
3288
3644
  });
3645
+ getLogger().error(error.message, {
3646
+ storyId: story.id,
3647
+ migrationNames,
3648
+ error,
3649
+ errorCode: ERROR_CODES.MIGRATION_APPLY_TO_STORY_ERROR
3650
+ });
3289
3651
  return null;
3290
3652
  }
3291
3653
  }
@@ -3385,16 +3747,23 @@ class UpdateStream extends Writable {
3385
3747
  this.results.successful.push({ storyId, name: storyName });
3386
3748
  this.results.totalProcessed++;
3387
3749
  this.options.onProgress?.(this.results.totalProcessed);
3750
+ getLogger().info("Updated story", { storyId });
3388
3751
  } else {
3752
+ const error = new Error("Update returned null");
3389
3753
  this.results.failed.push({
3390
3754
  storyId,
3391
3755
  name: storyName,
3392
- error: new Error("Update returned null")
3756
+ error
3393
3757
  });
3394
3758
  this.results.totalProcessed++;
3395
3759
  this.options.onProgress?.(this.results.totalProcessed);
3760
+ getLogger().error(`Failed to update story: ${error.message}`, {
3761
+ storyId,
3762
+ errorCode: ERROR_CODES.MIGRATION_STORY_UPDATE_NULL
3763
+ });
3396
3764
  }
3397
- } catch (error) {
3765
+ } catch (maybeError) {
3766
+ const error = toError(maybeError);
3398
3767
  this.results.failed.push({
3399
3768
  storyId,
3400
3769
  name: storyName,
@@ -3402,6 +3771,11 @@ class UpdateStream extends Writable {
3402
3771
  });
3403
3772
  this.results.totalProcessed++;
3404
3773
  this.options.onProgress?.(this.results.totalProcessed);
3774
+ getLogger().error(error.message, {
3775
+ storyId,
3776
+ error,
3777
+ errorCode: ERROR_CODES.MIGRATION_STORY_UPDATE_ERROR
3778
+ });
3405
3779
  }
3406
3780
  }
3407
3781
  async _destroy(error, callback) {
@@ -3437,15 +3811,18 @@ class UpdateStream extends Writable {
3437
3811
  }
3438
3812
  }
3439
3813
 
3440
- const program$8 = getProgram();
3441
3814
  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...");
3815
+ const program = getProgram();
3816
+ const ui = getUI();
3817
+ const logger = getLogger();
3818
+ ui.title(`${commands.MIGRATIONS}`, colorPalette.MIGRATIONS, componentName ? `Running migrations for component ${componentName}...` : "Running migrations...");
3819
+ logger.info("Migration started");
3443
3820
  if (options.dryRun) {
3444
- konsola.warn(`DRY RUN MODE ENABLED: No changes will be made.
3821
+ ui.warn(`DRY RUN MODE ENABLED: No changes will be made.
3445
3822
  `);
3823
+ logger.warn("Dry run mode enabled");
3446
3824
  }
3447
- const verbose = program$8.opts().verbose;
3448
- const { filter, dryRun = false, query, startsWith, publish } = options;
3825
+ const verbose = program.opts().verbose;
3449
3826
  const { space, path } = migrationsCommand.opts();
3450
3827
  const { state, initializeSession } = session();
3451
3828
  await initializeSession();
@@ -3456,6 +3833,7 @@ migrationsCommand.command("run [componentName]").description("Run migrations").o
3456
3833
  handleError(new CommandError(`Please provide the space as argument --space YOUR_SPACE_ID.`), verbose);
3457
3834
  return;
3458
3835
  }
3836
+ const { filter, dryRun = false, query, startsWith, publish } = options;
3459
3837
  const { password, region } = state;
3460
3838
  mapiClient({
3461
3839
  token: {
@@ -3464,40 +3842,25 @@ migrationsCommand.command("run [componentName]").description("Run migrations").o
3464
3842
  region
3465
3843
  });
3466
3844
  try {
3467
- const spinner = new Spinner({
3468
- verbose: !isVitest
3469
- }).start(`Fetching migration files and stories...`);
3845
+ const spinner = ui.createSpinner(`Fetching migration files and stories...`);
3470
3846
  const migrationFiles = await readMigrationFiles({
3471
3847
  space,
3472
3848
  path,
3473
3849
  filter
3474
3850
  });
3475
- if (migrationFiles.length === 0) {
3476
- spinner.failed(`No migration files found for space "${space}"${filter ? ` matching filter "${filter}"` : ""}.`);
3477
- return;
3478
- }
3479
3851
  const filteredMigrations = componentName ? migrationFiles.filter((file) => {
3480
3852
  return file.name.match(new RegExp(`^${componentName}(\\..*)?.js$`));
3481
3853
  }) : migrationFiles;
3482
3854
  if (filteredMigrations.length === 0) {
3483
3855
  spinner.failed(`No migration files found${componentName ? ` for component "${componentName}"` : ""}${filter ? ` matching filter "${filter}"` : ""} in space "${space}".`);
3856
+ logger.warn("No migration files found");
3857
+ logger.info("Migration finished");
3484
3858
  return;
3485
3859
  }
3486
3860
  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
- });
3861
+ const storiesProgress = ui.createProgressBar({ title: "Fetching Stories...".padEnd(19) });
3862
+ const migrationsProgress = ui.createProgressBar({ title: "Applying Migrations".padEnd(19) });
3863
+ const updateProgress = ui.createProgressBar({ title: "Updating Stories...".padEnd(19) });
3501
3864
  const storiesStream = await createStoriesStream({
3502
3865
  spaceId: space,
3503
3866
  params: {
@@ -3535,7 +3898,7 @@ migrationsCommand.command("run [componentName]").description("Run migrations").o
3535
3898
  updateProgress.increment();
3536
3899
  }
3537
3900
  });
3538
- return new Promise((resolve, reject) => {
3901
+ await new Promise((resolve, reject) => {
3539
3902
  pipeline(
3540
3903
  storiesStream,
3541
3904
  migrationStream,
@@ -3545,25 +3908,51 @@ migrationsCommand.command("run [componentName]").description("Run migrations").o
3545
3908
  reject(err);
3546
3909
  return;
3547
3910
  }
3548
- multiBar.stop();
3549
- const migrationSummary = migrationStream.getSummary();
3550
- konsola.info(migrationSummary);
3551
- const updateSummary = updateStream.getSummary();
3552
- konsola.info(updateSummary);
3553
3911
  resolve();
3554
3912
  }
3555
3913
  );
3556
3914
  });
3915
+ ui.stopAllProgressBars();
3916
+ const migrationSummary = migrationStream.getSummary();
3917
+ ui.info(migrationSummary);
3918
+ const updateSummary = updateStream.getSummary();
3919
+ ui.info(updateSummary);
3920
+ const migrationResults = migrationStream.getResults();
3921
+ const updateResults = updateStream.getResults();
3922
+ logger.info("Migration finished", {
3923
+ migrationResults: {
3924
+ total: migrationResults.totalProcessed,
3925
+ succeeded: migrationResults.successful.length,
3926
+ skipped: migrationResults.skipped.length,
3927
+ failed: migrationResults.failed.length
3928
+ },
3929
+ updateResults: {
3930
+ total: updateResults.totalProcessed,
3931
+ succeeded: updateResults.successful.length,
3932
+ failed: updateResults.failed.length
3933
+ }
3934
+ });
3557
3935
  } catch (error) {
3558
3936
  handleError(error, verbose);
3559
3937
  }
3560
3938
  });
3561
3939
 
3562
- const program$7 = getProgram();
3563
3940
  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;
3941
+ const program = getProgram();
3942
+ const ui = getUI();
3943
+ const logger = getLogger();
3944
+ ui.title(
3945
+ `${commands.MIGRATIONS}`,
3946
+ colorPalette.MIGRATIONS,
3947
+ migrationFile ? `Rolling back migration ${chalk.hex(colorPalette.MIGRATIONS)(migrationFile)}...` : "Rolling back migration..."
3948
+ );
3949
+ const verbose = program.opts().verbose;
3566
3950
  const { space, path } = migrationsCommand.opts();
3951
+ logger.info("Migration rollback started", {
3952
+ migrationFile,
3953
+ space,
3954
+ path
3955
+ });
3567
3956
  const { state, initializeSession } = session();
3568
3957
  await initializeSession();
3569
3958
  if (!requireAuthentication(state, verbose)) {
@@ -3586,8 +3975,13 @@ migrationsCommand.command("rollback [migrationFile]").description("Rollback a mi
3586
3975
  path,
3587
3976
  migrationFile
3588
3977
  });
3978
+ const rollbackSummary = {
3979
+ total: rollbackData.stories.length,
3980
+ succeeded: 0,
3981
+ failed: 0
3982
+ };
3589
3983
  for (const story of rollbackData.stories) {
3590
- const spinner = new Spinner({ verbose }).start(`Restoring story ${chalk.hex(colorPalette.PRIMARY)(story.name || story.storyId)}...`);
3984
+ const spinner = ui.createSpinner(`Restoring story ${chalk.hex(colorPalette.PRIMARY)(story.name || story.storyId)}...`);
3591
3985
  try {
3592
3986
  const payload = {
3593
3987
  story: {
@@ -3603,18 +3997,37 @@ migrationsCommand.command("rollback [migrationFile]").description("Rollback a mi
3603
3997
  }
3604
3998
  }
3605
3999
  await updateStory(space, story.storyId, payload);
3606
- spinner.succeed(`Restored story ${chalk.hex(colorPalette.PRIMARY)(story.name || story.storyId)}`);
3607
- } catch (error) {
4000
+ rollbackSummary.succeeded += 1;
4001
+ spinner.succeed(`Restored story ${chalk.hex(colorPalette.PRIMARY)(story.name || story.storyId)} - Completed in ${spinner.elapsedTime.toFixed(2)}ms`);
4002
+ logger.info("Story restored", {
4003
+ storyId: story.storyId,
4004
+ migrationFile,
4005
+ space
4006
+ });
4007
+ } catch (maybeError) {
4008
+ const error = toError(maybeError);
4009
+ rollbackSummary.failed += 1;
3608
4010
  spinner.failed(`Failed to restore story ${chalk.hex(colorPalette.PRIMARY)(story.name || story.storyId)}: ${error.message}`);
4011
+ logger.error("Failed to restore story", {
4012
+ storyId: story.storyId,
4013
+ migrationFile,
4014
+ space,
4015
+ error
4016
+ });
3609
4017
  }
3610
4018
  }
4019
+ logger.info("Migration rollback finished", {
4020
+ migrationFile,
4021
+ space,
4022
+ results: rollbackSummary
4023
+ });
3611
4024
  } catch (error) {
3612
4025
  handleError(new CommandError(`Failed to rollback migration: ${error.message}`), verbose);
3613
4026
  }
3614
4027
  });
3615
4028
 
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");
4029
+ const program$7 = getProgram();
4030
+ 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
4031
 
3619
4032
  const getAssetJSONSchema = (title) => ({
3620
4033
  $id: "#/asset",
@@ -4454,7 +4867,7 @@ const upsertDatasourceEntry = async (space, datasourceId, entry, existingId) =>
4454
4867
  }
4455
4868
  };
4456
4869
  const readDatasourcesFiles = async (options) => {
4457
- const { from, path, separateFiles = false, suffix, space } = options;
4870
+ const { from, path, separateFiles = false, suffix } = options;
4458
4871
  const resolvedPath = resolvePath(path, `datasources/${from}`);
4459
4872
  try {
4460
4873
  await readdir(resolvedPath);
@@ -4465,7 +4878,7 @@ const readDatasourcesFiles = async (options) => {
4465
4878
  ${chalk.cyan(`storyblok datasources pull --space ${from}`)}
4466
4879
 
4467
4880
  2. Then try pushing again:
4468
- ${chalk.cyan(`storyblok datasources push --space ${space} --from ${from}`)}`;
4881
+ ${chalk.cyan(`storyblok datasources push --space <target_space> --from ${from}`)}`;
4469
4882
  throw new FileSystemError(
4470
4883
  "file_not_found",
4471
4884
  "read",
@@ -4522,13 +4935,13 @@ async function readConsolidatedFiles(resolvedPath, suffix) {
4522
4935
  };
4523
4936
  }
4524
4937
 
4525
- const program$5 = getProgram();
4938
+ const program$6 = getProgram();
4526
4939
  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(
4527
4940
  "--filename <name>",
4528
4941
  "Base file name for all component types when generating a single declarations file (e.g. components.d.ts). Ignored when using --separate-files."
4529
4942
  ).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) => {
4530
4943
  konsola.title(`${commands.TYPES}`, colorPalette.TYPES, "Generating types...");
4531
- const verbose = program$5.opts().verbose;
4944
+ const verbose = program$6.opts().verbose;
4532
4945
  const { space, path } = typesCommand.opts();
4533
4946
  const spinner = new Spinner({
4534
4947
  verbose: !isVitest
@@ -4581,8 +4994,8 @@ typesCommand.command("generate").description("Generate types d.ts for your compo
4581
4994
  }
4582
4995
  });
4583
4996
 
4584
- const program$4 = getProgram();
4585
- 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");
4997
+ const program$5 = getProgram();
4998
+ 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");
4586
4999
 
4587
5000
  async function fetchAllPages(fetchFunction, extractDataFunction, page = 1, collectedItems = []) {
4588
5001
  const { data, response } = await fetchFunction(page);
@@ -4688,10 +5101,10 @@ const saveDatasourcesToFiles = async (space, datasources, options) => {
4688
5101
  }
4689
5102
  };
4690
5103
 
4691
- const program$3 = getProgram();
5104
+ const program$4 = getProgram();
4692
5105
  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) => {
4693
5106
  konsola.title(`${commands.DATASOURCES}`, colorPalette.DATASOURCES, datasourceName ? `Pulling datasource ${datasourceName}...` : "Pulling datasources...");
4694
- const verbose = program$3.opts().verbose;
5107
+ const verbose = program$4.opts().verbose;
4695
5108
  const { space, path } = datasourcesCommand.opts();
4696
5109
  const { separateFiles, suffix, filename = "datasources" } = options;
4697
5110
  const { state, initializeSession } = session();
@@ -4760,12 +5173,13 @@ datasourcesCommand.command("pull [datasourceName]").option("-f, --filename <file
4760
5173
  }
4761
5174
  });
4762
5175
 
4763
- const program$2 = getProgram();
5176
+ const program$3 = getProgram();
4764
5177
  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) => {
4765
5178
  konsola.title(`${commands.DATASOURCES}`, colorPalette.DATASOURCES, datasourceName ? `Pushing datasource ${datasourceName}...` : "Pushing datasources...");
4766
- const verbose = program$2.opts().verbose;
5179
+ const verbose = program$3.opts().verbose;
4767
5180
  const { space, path } = datasourcesCommand.opts();
4768
- const { from, filter } = options;
5181
+ const { filter } = options;
5182
+ const fromSpace = options.from || space;
4769
5183
  const { state, initializeSession } = session();
4770
5184
  await initializeSession();
4771
5185
  if (!requireAuthentication(state, verbose)) {
@@ -4775,10 +5189,7 @@ datasourcesCommand.command("push [datasourceName]").description(`Push your space
4775
5189
  handleError(new CommandError(`Please provide the target space as argument --space TARGET_SPACE_ID.`), verbose);
4776
5190
  return;
4777
5191
  }
4778
- if (!from) {
4779
- options.from = space;
4780
- }
4781
- konsola.info(`Attempting to push datasources ${chalk.bold("from")} space ${chalk.hex(colorPalette.DATASOURCES)(options.from || space)} ${chalk.bold("to")} ${chalk.hex(colorPalette.DATASOURCES)(space)}`);
5192
+ konsola.info(`Attempting to push datasources ${chalk.bold("from")} space ${chalk.hex(colorPalette.DATASOURCES)(fromSpace)} ${chalk.bold("to")} ${chalk.hex(colorPalette.DATASOURCES)(space)}`);
4782
5193
  konsola.br();
4783
5194
  const { password, region } = state;
4784
5195
  mapiClient({
@@ -4792,7 +5203,7 @@ datasourcesCommand.command("push [datasourceName]").description(`Push your space
4792
5203
  local: await readDatasourcesFiles({
4793
5204
  ...options,
4794
5205
  path,
4795
- space
5206
+ from: fromSpace
4796
5207
  }),
4797
5208
  target: {
4798
5209
  datasources: /* @__PURE__ */ new Map()
@@ -5080,11 +5491,33 @@ const fetchBlueprintRepositories = async () => {
5080
5491
  }
5081
5492
  };
5082
5493
 
5083
- const program$1 = getProgram();
5084
- 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) => {
5494
+ function showNextSteps(technologyTemplate, finalProjectPath) {
5495
+ konsola.br();
5496
+ konsola.ok(`Your ${chalk.hex(colorPalette.PRIMARY)(technologyTemplate)} project is ready \u{1F389} !`);
5497
+ konsola.br();
5498
+ konsola.info(`Next steps:
5499
+ cd ${finalProjectPath}
5500
+ npm install
5501
+ npm run dev
5502
+ `);
5503
+ konsola.info(`Or check the dedicated guide at: ${chalk.hex(colorPalette.PRIMARY)(`https://www.storyblok.com/docs/guides/${technologyTemplate}`)}`);
5504
+ }
5505
+ async function handleEnvFileCreation(resolvedPath, token) {
5506
+ try {
5507
+ await createEnvFile(resolvedPath, token);
5508
+ konsola.ok(`Created .env file with Storyblok access token`, true);
5509
+ return true;
5510
+ } catch (error) {
5511
+ konsola.warn(`Failed to create .env file: ${error.message}`);
5512
+ konsola.info(`You can manually add this token to your .env file: ${token}`);
5513
+ return false;
5514
+ }
5515
+ }
5516
+ const program$2 = getProgram();
5517
+ 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").option("--token <token>", "Storyblok access token (skip space creation and use this token)").action(async (projectPath, options) => {
5085
5518
  konsola.title(`${commands.CREATE}`, colorPalette.CREATE);
5086
- const verbose = program$1.opts().verbose;
5087
- const { template, blueprint } = options;
5519
+ const verbose = program$2.opts().verbose;
5520
+ const { template, blueprint, token } = options;
5088
5521
  let selectedTemplate = template;
5089
5522
  if (blueprint && !template) {
5090
5523
  konsola.warn(`The --blueprint flag is deprecated. Please use --template instead.`);
@@ -5110,18 +5543,6 @@ program$1.command(`${commands.CREATE} [project-path]`).alias("c").description(`S
5110
5543
  const spinnerSpace = new Spinner({
5111
5544
  verbose: !isVitest
5112
5545
  });
5113
- let userData;
5114
- try {
5115
- const user = await getUser(password, region);
5116
- if (!user) {
5117
- throw new Error("User data is undefined");
5118
- }
5119
- userData = user;
5120
- } catch (error) {
5121
- konsola.error("Failed to fetch user info. Please login again.", error);
5122
- konsola.br();
5123
- return;
5124
- }
5125
5546
  try {
5126
5547
  spinnerBlueprints.start("Fetching starter templates...");
5127
5548
  const templates = await fetchBlueprintRepositories();
@@ -5177,91 +5598,96 @@ program$1.command(`${commands.CREATE} [project-path]`).alias("c").description(`S
5177
5598
  await generateProject(technologyTemplate, projectName, targetDirectory);
5178
5599
  konsola.ok(`Project ${chalk.hex(colorPalette.PRIMARY)(projectName)} created successfully in ${chalk.hex(colorPalette.PRIMARY)(finalProjectPath)}`, true);
5179
5600
  let createdSpace;
5180
- const choices = [
5181
- { name: "My personal account", value: "personal" }
5182
- ];
5183
- if (userData.has_org) {
5184
- choices.push({ name: `Organization (${userData?.org?.name})`, value: "org" });
5185
- }
5186
- if (userData.has_partner) {
5187
- choices.push({ name: "Partner Portal", value: "partner" });
5188
- }
5601
+ let userData;
5189
5602
  let whereToCreateSpace = "personal";
5190
- if (region === "eu" && (userData.has_partner || userData.has_org)) {
5191
- whereToCreateSpace = await select({
5192
- message: `Where would you like to create this space?`,
5193
- choices
5194
- });
5195
- }
5196
- if (region !== "eu" && userData.has_org) {
5197
- whereToCreateSpace = "org";
5603
+ if (token) {
5604
+ await handleEnvFileCreation(resolvedPath, token);
5605
+ showNextSteps(technologyTemplate, finalProjectPath);
5606
+ return;
5198
5607
  }
5199
- if (region !== "eu" && !userData.has_org) {
5200
- konsola.warn(`Space creation in this region is limited to Enterprise accounts. If you're part of an organization, please ensure you have the required permissions. For more information about Enterprise access, contact our Sales Team.`);
5201
- konsola.br();
5608
+ if (options.skipSpace) {
5609
+ showNextSteps(technologyTemplate, finalProjectPath);
5202
5610
  return;
5203
5611
  }
5204
- if (!options.skipSpace) {
5612
+ try {
5205
5613
  try {
5206
- spinnerSpace.start(`Creating space "${toHumanReadable(projectName)}"`);
5207
- const selectedBlueprint = templates.find((bp) => bp.value === technologyTemplate);
5208
- const blueprintDomain = selectedBlueprint?.location || "https://localhost:3000/";
5209
- const spaceToCreate = {
5210
- name: toHumanReadable(projectName),
5211
- domain: blueprintDomain
5212
- };
5213
- if (whereToCreateSpace === "org") {
5214
- spaceToCreate.org = userData.org;
5215
- spaceToCreate.in_org = true;
5216
- } else if (whereToCreateSpace === "partner") {
5217
- spaceToCreate.assign_partner = true;
5614
+ const user = await getUser(password, region);
5615
+ if (!user) {
5616
+ throw new Error("User data is undefined");
5218
5617
  }
5219
- createdSpace = await createSpace(spaceToCreate);
5220
- spinnerSpace.succeed(`Space "${chalk.hex(colorPalette.PRIMARY)(toHumanReadable(projectName))}" created successfully`);
5618
+ userData = user;
5221
5619
  } catch (error) {
5222
- spinnerSpace.failed();
5620
+ konsola.error("Failed to fetch user info. Please login again.", error);
5223
5621
  konsola.br();
5224
- handleError(error, verbose);
5225
5622
  return;
5226
5623
  }
5227
- }
5228
- if (createdSpace?.first_token) {
5229
- try {
5230
- await createEnvFile(resolvedPath, createdSpace.first_token);
5231
- konsola.ok(`Created .env file with Storyblok access token`, true);
5232
- } catch (error) {
5233
- konsola.warn(`Failed to create .env file: ${error.message}`);
5234
- konsola.info(`You can manually add this token to your .env file: ${createdSpace.first_token}`);
5624
+ const choices = [
5625
+ { name: "My personal account", value: "personal" }
5626
+ ];
5627
+ if (userData.has_org) {
5628
+ choices.push({ name: `Organization (${userData?.org?.name})`, value: "org" });
5235
5629
  }
5236
- }
5237
- if (createdSpace?.id) {
5238
- try {
5239
- await openSpaceInBrowser(createdSpace.id, region);
5240
- konsola.info(`Opened space in your browser`);
5241
- } catch (error) {
5242
- konsola.warn(`Failed to open browser: ${error.message}`);
5243
- const spaceUrl = generateSpaceUrl(createdSpace.id, region);
5244
- konsola.info(`You can manually open your space at: ${chalk.hex(colorPalette.PRIMARY)(spaceUrl)}`);
5630
+ if (userData.has_partner) {
5631
+ choices.push({ name: "Partner Portal", value: "partner" });
5245
5632
  }
5246
- }
5247
- konsola.br();
5248
- konsola.ok(`Your ${chalk.hex(colorPalette.PRIMARY)(technologyTemplate)} project is ready \u{1F389} !`);
5249
- if (createdSpace?.first_token) {
5633
+ if (region === regions.EU && (userData.has_partner || userData.has_org)) {
5634
+ whereToCreateSpace = await select({
5635
+ message: `Where would you like to create this space?`,
5636
+ choices
5637
+ });
5638
+ }
5639
+ if (region !== regions.EU && userData.has_org) {
5640
+ whereToCreateSpace = "org";
5641
+ }
5642
+ if (region !== regions.EU && !userData.has_org) {
5643
+ konsola.warn(`Space creation in this region is limited to Enterprise accounts. If you're part of an organization, please ensure you have the required permissions. For more information about Enterprise access, contact our Sales Team.`);
5644
+ konsola.br();
5645
+ return;
5646
+ }
5647
+ spinnerSpace.start(`Creating space "${toHumanReadable(projectName)}"`);
5648
+ const selectedBlueprint = templates.find((bp) => bp.value === technologyTemplate);
5649
+ const blueprintDomain = selectedBlueprint?.location || "https://localhost:3000/";
5650
+ const spaceToCreate = {
5651
+ name: toHumanReadable(projectName),
5652
+ domain: blueprintDomain
5653
+ };
5250
5654
  if (whereToCreateSpace === "org") {
5251
- konsola.ok(`Storyblok space created in organization ${chalk.hex(colorPalette.PRIMARY)(userData?.org?.name)}, preview url and .env configured automatically. You can now open your space in the browser at ${chalk.hex(colorPalette.PRIMARY)(generateSpaceUrl(createdSpace.id, region))}`);
5655
+ spaceToCreate.org = userData.org;
5656
+ spaceToCreate.in_org = true;
5252
5657
  } else if (whereToCreateSpace === "partner") {
5253
- konsola.ok(`Storyblok space created in partner portal, preview url and .env configured automatically. You can now open your space in the browser at ${chalk.hex(colorPalette.PRIMARY)(generateSpaceUrl(createdSpace.id, region))}`);
5254
- } else {
5255
- konsola.ok(`Storyblok space created, preview url and .env configured automatically. You can now open your space in the browser at ${chalk.hex(colorPalette.PRIMARY)(generateSpaceUrl(createdSpace.id, region))}`);
5658
+ spaceToCreate.assign_partner = true;
5659
+ }
5660
+ createdSpace = await createSpace(spaceToCreate);
5661
+ spinnerSpace.succeed(`Space "${chalk.hex(colorPalette.PRIMARY)(toHumanReadable(projectName))}" created successfully`);
5662
+ if (createdSpace?.first_token) {
5663
+ await handleEnvFileCreation(resolvedPath, createdSpace.first_token);
5664
+ }
5665
+ if (createdSpace?.id) {
5666
+ try {
5667
+ await openSpaceInBrowser(createdSpace.id, region);
5668
+ konsola.info(`Opened space in your browser`);
5669
+ } catch (error) {
5670
+ konsola.warn(`Failed to open browser: ${error.message}`);
5671
+ const spaceUrl = generateSpaceUrl(createdSpace.id, region);
5672
+ konsola.info(`You can manually open your space at: ${chalk.hex(colorPalette.PRIMARY)(spaceUrl)}`);
5673
+ }
5674
+ }
5675
+ showNextSteps(technologyTemplate, finalProjectPath);
5676
+ if (createdSpace?.first_token) {
5677
+ if (whereToCreateSpace === "org") {
5678
+ konsola.ok(`Storyblok space created in organization ${chalk.hex(colorPalette.PRIMARY)(userData?.org?.name)}, preview url and .env configured automatically. You can now open your space in the browser at ${chalk.hex(colorPalette.PRIMARY)(generateSpaceUrl(createdSpace.id, region))}`);
5679
+ } else if (whereToCreateSpace === "partner") {
5680
+ konsola.ok(`Storyblok space created in partner portal, preview url and .env configured automatically. You can now open your space in the browser at ${chalk.hex(colorPalette.PRIMARY)(generateSpaceUrl(createdSpace.id, region))}`);
5681
+ } else {
5682
+ konsola.ok(`Storyblok space created, preview url and .env configured automatically. You can now open your space in the browser at ${chalk.hex(colorPalette.PRIMARY)(generateSpaceUrl(createdSpace.id, region))}`);
5683
+ }
5256
5684
  }
5685
+ } catch (error) {
5686
+ spinnerSpace.failed();
5687
+ konsola.br();
5688
+ handleError(error, verbose);
5689
+ return;
5257
5690
  }
5258
- konsola.br();
5259
- konsola.info(`Next steps:
5260
- cd ${finalProjectPath}
5261
- npm install
5262
- npm run dev
5263
- `);
5264
- konsola.info(`Or check the dedicated guide at: ${chalk.hex(colorPalette.PRIMARY)(`https://www.storyblok.com/docs/guides/${technologyTemplate}`)}`);
5265
5691
  } catch (error) {
5266
5692
  spinnerSpace.failed();
5267
5693
  spinnerBlueprints.failed();
@@ -5271,7 +5697,31 @@ program$1.command(`${commands.CREATE} [project-path]`).alias("c").description(`S
5271
5697
  konsola.br();
5272
5698
  });
5273
5699
 
5274
- const version = "4.7.0";
5700
+ const program$1 = getProgram();
5701
+ 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'.");
5702
+
5703
+ logsCommand.command("list").description("List logs").action(async () => {
5704
+ const { space, path } = logsCommand.opts();
5705
+ const ui = getUI();
5706
+ const logsPath = getLogsPath(directories.log, space, path);
5707
+ const logFiles = FileTransport.listLogFiles(logsPath);
5708
+ if (logFiles.length === 0) {
5709
+ ui.info(`No logs found for space "${space}".`);
5710
+ return;
5711
+ }
5712
+ ui.info(`Found ${logFiles.length} log file${logFiles.length === 1 ? "" : "s"} for space "${space}":`);
5713
+ ui.list(logFiles);
5714
+ });
5715
+
5716
+ 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 }) => {
5717
+ const { space, path } = logsCommand.opts();
5718
+ const ui = getUI();
5719
+ const logsPath = getLogsPath(directories.log, space, path);
5720
+ const deletedFilesCount = FileTransport.pruneLogFiles(logsPath, keep);
5721
+ ui.info(`Deleted ${deletedFilesCount} log file${deletedFilesCount === 1 ? "" : "s"}`);
5722
+ });
5723
+
5724
+ const version = "4.9.0";
5275
5725
  const pkg = {
5276
5726
  version: version};
5277
5727