sb-mig 5.6.2-beta.1 → 5.7.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/README.md CHANGED
@@ -189,6 +189,49 @@ This command will look for `row.sb.js` and `column.sb.js` files inside a directo
189
189
 
190
190
  ## Syncing datasources
191
191
 
192
+ ## Migrating content
193
+
194
+ Use `sb-mig migrate content` to run one or more migration configs against
195
+ stories from a space or from a file.
196
+
197
+ ```bash
198
+ sb-mig migrate content --all --from 12345 --to 12345 --migration itemsToContent
199
+ ```
200
+
201
+ ### Extending migration component scope
202
+
203
+ Migration files export a mapper keyed by component name. By default, `sb-mig`
204
+ runs a migration only for the component keys exported by that migration file.
205
+
206
+ Use these flags when a consuming app has wrapper components that should reuse an
207
+ existing migration function:
208
+
209
+ - `--migrationComponentAlias`
210
+ Format: `<migration>:<source>=<alias1>,<alias2>`
211
+ - `--migrationComponents`
212
+ Format: `<migration>:<component1>,<component2>`
213
+
214
+ `--migrationComponentAlias` extends the migration mapper at runtime by reusing
215
+ the source component's migration function for extra component names.
216
+
217
+ `--migrationComponents` overrides the exact component scope for that migration.
218
+
219
+ Example:
220
+
221
+ ```bash
222
+ sb-mig migrate content --all \
223
+ --from 12345 \
224
+ --to 12345 \
225
+ --migration colorPickerModeValues \
226
+ --migrationComponentAlias colorPickerModeValues:sb-button=sb-open-drift-button \
227
+ --migrationComponentAlias colorPickerModeValues:sb-section=sb-tour-page-section \
228
+ --migrationComponents colorPickerModeValues:sb-button,sb-open-drift-button,sb-section,sb-tour-page-section
229
+ ```
230
+
231
+ This runs `colorPickerModeValues` for the normal Backpack keys and also for the
232
+ two wrapper component names by aliasing them onto the existing `sb-button` and
233
+ `sb-section` migration handlers.
234
+
192
235
  You can also sync your `datasources`.
193
236
 
194
237
  Add `datasourceExt: "your-own-extension",` to your `storyblok.config.js`. If u will not add it, will be used default one (`sb.datasource.js`)
@@ -1,4 +1,5 @@
1
1
  import type { RequestBaseConfig } from "../utils/request.js";
2
+ import { type MigrationComponentAliasesByMigration, type MigrationComponentOverridesByMigration } from "./migration-component-scope.js";
2
3
  import { type PreparedMigrationValidator } from "./migration-validation.js";
3
4
  export type MigrateFrom = "file" | "space";
4
5
  export interface PreparedMigrationConfig {
@@ -35,6 +36,8 @@ interface MigrateItems {
35
36
  migrateFrom: MigrateFrom;
36
37
  migrationConfig: string | string[];
37
38
  componentsToMigrate?: string[];
39
+ migrationComponentAliases?: MigrationComponentAliasesByMigration;
40
+ migrationComponentOverrides?: MigrationComponentOverridesByMigration;
38
41
  filters?: {
39
42
  withSlug?: string[];
40
43
  startsWith?: string;
@@ -50,9 +53,11 @@ export declare const prepareStoriesFromLocalFile: ({ from, fromFilePath, }: {
50
53
  from?: string;
51
54
  fromFilePath?: string;
52
55
  }) => any;
53
- export declare const prepareMigrationConfigs: ({ migrationConfig, componentsToMigrate, }: {
56
+ export declare const prepareMigrationConfigs: ({ migrationConfig, componentsToMigrate, migrationComponentAliases, migrationComponentOverrides, }: {
54
57
  migrationConfig: string | string[];
55
58
  componentsToMigrate?: string[];
59
+ migrationComponentAliases?: MigrationComponentAliasesByMigration;
60
+ migrationComponentOverrides?: MigrationComponentOverridesByMigration;
56
61
  }) => PreparedMigrationConfig[];
57
62
  export declare const prepareMigrationConfig: ({ migrationConfig, }: {
58
63
  migrationConfig: string;
@@ -62,7 +67,7 @@ export declare const runMigrationPipelineInMemory: ({ itemType, itemsToMigrate,
62
67
  itemsToMigrate: any[];
63
68
  preparedMigrationConfigs: PreparedMigrationConfig[];
64
69
  }) => MigrationPipelineResult;
65
- export declare const migrateAllComponentsDataInStories: ({ itemType, migrationConfig, migrateFrom, from, to, filters, dryRun, fromFilePath, fileName, }: Omit<MigrateItems, "componentsToMigrate" | "preparedMigrationConfigs">, config: RequestBaseConfig) => Promise<void>;
70
+ export declare const migrateAllComponentsDataInStories: ({ itemType, migrationConfig, migrateFrom, from, to, filters, dryRun, fromFilePath, fileName, migrationComponentAliases, migrationComponentOverrides, }: Omit<MigrateItems, "componentsToMigrate" | "preparedMigrationConfigs">, config: RequestBaseConfig) => Promise<void>;
66
71
  export declare const doTheMigration: ({ itemType, from, itemsToMigrate, migrationConfig, migrationConfigs, to, dryRun, migrateFrom, fromFilePath, fileName, }: {
67
72
  itemType?: "story" | "preset";
68
73
  from: string;
@@ -75,5 +80,5 @@ export declare const doTheMigration: ({ itemType, from, itemsToMigrate, migratio
75
80
  fromFilePath?: string;
76
81
  fileName?: string;
77
82
  }, config: RequestBaseConfig) => Promise<void>;
78
- export declare const migrateProvidedComponentsDataInStories: ({ itemType, migrationConfig, migrateFrom, from, to, componentsToMigrate, filters, dryRun, fromFilePath, fileName, preparedMigrationConfigs, }: MigrateItems, config: RequestBaseConfig) => Promise<void>;
83
+ export declare const migrateProvidedComponentsDataInStories: ({ itemType, migrationConfig, migrateFrom, from, to, componentsToMigrate, filters, dryRun, fromFilePath, fileName, preparedMigrationConfigs, migrationComponentAliases, migrationComponentOverrides, }: MigrateItems, config: RequestBaseConfig) => Promise<void>;
79
84
  export {};
@@ -7,7 +7,10 @@ import Logger from "../../utils/logger.js";
7
7
  import { modifyOrCreateAppliedMigrationsFile } from "../../utils/migrations.js";
8
8
  import { isObjectEmpty } from "../../utils/object-utils.js";
9
9
  import { managementApi } from "../managementApi.js";
10
+ import { buildPreMigrationBackupBaseName, resolveOutputFileBaseName, shouldUseDatestampForArtifacts, } from "./file-naming.js";
11
+ import { extendMigrationMapperWithAliases, resolveMigrationComponentsToMigrate, } from "./migration-component-scope.js";
10
12
  import { discoverMigrationValidatorForMigrationFile, MigrationValidationFailedError, runPreparedMigrationValidator, } from "./migration-validation.js";
13
+ import { summarizeMutationWriteResults } from "./write-summary.js";
11
14
  export const normalizeMigrationConfigNames = (migrationConfig) => {
12
15
  if (Array.isArray(migrationConfig)) {
13
16
  return migrationConfig.filter((name) => Boolean(name));
@@ -107,7 +110,7 @@ export const prepareStoriesFromLocalFile = ({ from, fromFilePath, }) => {
107
110
  }
108
111
  return storiesFileContent;
109
112
  };
110
- export const prepareMigrationConfigs = ({ migrationConfig, componentsToMigrate, }) => {
113
+ export const prepareMigrationConfigs = ({ migrationConfig, componentsToMigrate, migrationComponentAliases, migrationComponentOverrides, }) => {
111
114
  const migrationConfigNames = normalizeMigrationConfigNames(migrationConfig);
112
115
  if (migrationConfigNames.length === 0) {
113
116
  throw new Error("Migration config is required. Pass at least one --migration value.");
@@ -138,13 +141,17 @@ export const prepareMigrationConfigs = ({ migrationConfig, componentsToMigrate,
138
141
  if (!validator) {
139
142
  Logger.warning(`[VALIDATION] No co-located validator found for migration '${migrationConfigName}'. Expected a sibling '*.validation.*' file.`);
140
143
  }
141
- const resolvedComponentsToMigrate = componentsToMigrate && componentsToMigrate.length > 0
142
- ? componentsToMigrate
143
- : Object.keys(migrationConfigFileContent);
144
+ const aliasedMigrationConfigFileContent = extendMigrationMapperWithAliases(migrationConfigFileContent, migrationComponentAliases?.[migrationConfigName]);
145
+ const resolvedComponentsToMigrate = resolveMigrationComponentsToMigrate({
146
+ mapper: aliasedMigrationConfigFileContent,
147
+ migrationName: migrationConfigName,
148
+ globalComponentsToMigrate: componentsToMigrate,
149
+ perMigrationOverrides: migrationComponentOverrides,
150
+ });
144
151
  return {
145
152
  migrationConfigName,
146
153
  migrationConfigPath,
147
- migrationConfigFileContent,
154
+ migrationConfigFileContent: aliasedMigrationConfigFileContent,
148
155
  componentsToMigrate: resolvedComponentsToMigrate,
149
156
  validator,
150
157
  };
@@ -162,23 +169,6 @@ export const prepareMigrationConfig = ({ migrationConfig, }) => {
162
169
  };
163
170
  const deepClone = (input) => JSON.parse(JSON.stringify(input));
164
171
  const sumValues = (obj) => Object.values(obj).reduce((sum, value) => sum + value, 0);
165
- const sanitizeOutputFileBaseName = (value) => {
166
- const sanitized = value
167
- .trim()
168
- .replace(/[\\/]/g, "-")
169
- .replace(/\s+/g, "-")
170
- .replace(/[^a-zA-Z0-9._-]/g, "-")
171
- .replace(/-+/g, "-")
172
- .replace(/^[-.]+|[-.]+$/g, "");
173
- return sanitized || "migration-output";
174
- };
175
- const resolveOutputFileBaseName = ({ from, fileName, }) => {
176
- if (typeof fileName === "string" && fileName.trim().length > 0) {
177
- return sanitizeOutputFileBaseName(fileName);
178
- }
179
- return sanitizeOutputFileBaseName(from);
180
- };
181
- const shouldUseDatestampForArtifacts = (fileName) => !(typeof fileName === "string" && fileName.trim().length > 0);
182
172
  const applySingleMigrationToItems = ({ itemType, itemsToMigrate, preparedMigrationConfig, }) => {
183
173
  const arrayOfMaxDepths = [];
184
174
  const replacementsByComponent = {};
@@ -388,10 +378,12 @@ const loadItemsToMigrate = async ({ itemType, migrateFrom, from, filters, fromFi
388
378
  spaceId: from,
389
379
  });
390
380
  };
391
- export const migrateAllComponentsDataInStories = async ({ itemType, migrationConfig, migrateFrom, from, to, filters, dryRun, fromFilePath, fileName, }, config) => {
381
+ export const migrateAllComponentsDataInStories = async ({ itemType, migrationConfig, migrateFrom, from, to, filters, dryRun, fromFilePath, fileName, migrationComponentAliases, migrationComponentOverrides, }, config) => {
392
382
  Logger.warning(`Trying to migrate all ${itemType} from ${migrateFrom}, ${from} to ${to}...`);
393
383
  const preparedMigrationConfigs = prepareMigrationConfigs({
394
384
  migrationConfig,
385
+ migrationComponentAliases,
386
+ migrationComponentOverrides,
395
387
  });
396
388
  if (storyblokConfig.debug) {
397
389
  Logger.warning("_________ Components in stories to migrate ___________");
@@ -496,20 +488,34 @@ export const doTheMigration = async ({ itemType = "story", from, itemsToMigrate,
496
488
  if (pipelineResult.changedItems.length === 0) {
497
489
  return;
498
490
  }
491
+ let writeResults = [];
499
492
  if (itemType === "story") {
500
- await managementApi.stories.updateStories({
493
+ writeResults = await managementApi.stories.updateStories({
501
494
  stories: pipelineResult.changedItems,
502
495
  spaceId: to,
503
496
  options: { publish: false },
504
497
  }, config);
505
498
  }
506
499
  else if (itemType === "preset") {
507
- await managementApi.presets.updatePresets({
500
+ writeResults = await managementApi.presets.updatePresets({
508
501
  presets: pipelineResult.changedItems,
509
502
  spaceId: to,
510
503
  options: {},
511
504
  }, config);
512
505
  }
506
+ const writeSummary = summarizeMutationWriteResults(writeResults);
507
+ if (writeSummary.failed === 0) {
508
+ Logger.success(`[MIGRATION] Update complete. ${writeSummary.successful}/${writeSummary.total} ${itemType}(s) updated successfully.`);
509
+ return;
510
+ }
511
+ Logger.warning(`[MIGRATION] Update complete with partial failures. ${writeSummary.successful}/${writeSummary.total} ${itemType}(s) updated successfully, ${writeSummary.failed} failed.`);
512
+ writeSummary.failedItems.slice(0, 10).forEach((item) => {
513
+ const label = item.slug || item.name || item.id || "unknown";
514
+ Logger.error(`[MIGRATION] Failed ${itemType}: ${String(label)}`);
515
+ });
516
+ if (writeSummary.failedItems.length > 10) {
517
+ Logger.warning(`[MIGRATION] Showing first 10 failed ${itemType}(s) only.`);
518
+ }
513
519
  };
514
520
  const saveBackupToFile = async ({ itemType, res, folder, filename }, config) => {
515
521
  await createAndSaveToFile({
@@ -521,11 +527,13 @@ const saveBackupToFile = async ({ itemType, res, folder, filename }, config) =>
521
527
  res: res,
522
528
  }, config);
523
529
  };
524
- export const migrateProvidedComponentsDataInStories = async ({ itemType, migrationConfig, migrateFrom, from, to, componentsToMigrate, filters, dryRun, fromFilePath, fileName, preparedMigrationConfigs, }, config) => {
530
+ export const migrateProvidedComponentsDataInStories = async ({ itemType, migrationConfig, migrateFrom, from, to, componentsToMigrate, filters, dryRun, fromFilePath, fileName, preparedMigrationConfigs, migrationComponentAliases, migrationComponentOverrides, }, config) => {
525
531
  const resolvedMigrationConfigs = preparedMigrationConfigs ||
526
532
  prepareMigrationConfigs({
527
533
  migrationConfig,
528
534
  componentsToMigrate,
535
+ migrationComponentAliases,
536
+ migrationComponentOverrides,
529
537
  });
530
538
  const itemsToMigrate = await loadItemsToMigrate({
531
539
  itemType,
@@ -536,12 +544,12 @@ export const migrateProvidedComponentsDataInStories = async ({ itemType, migrati
536
544
  }, config);
537
545
  if (migrateFrom === "space" && !dryRun) {
538
546
  const backupFolder = path.join("backup", itemType);
539
- const migrationLabel = resolvedMigrationConfigs
540
- .map((preparedMigrationConfig) => preparedMigrationConfig.migrationConfigName)
541
- .join("__");
542
547
  await saveBackupToFile({
543
548
  itemType,
544
- filename: `before__${migrationLabel}__${from}`,
549
+ filename: buildPreMigrationBackupBaseName({
550
+ from,
551
+ fileName,
552
+ }),
545
553
  folder: backupFolder,
546
554
  res: itemsToMigrate,
547
555
  }, config);
@@ -0,0 +1,14 @@
1
+ export declare const sanitizeOutputFileBaseName: (value: string) => string;
2
+ export declare const resolveOutputFileBaseName: ({ from, fileName, }: {
3
+ from: string;
4
+ fileName?: string;
5
+ }) => string;
6
+ export declare const shouldUseDatestampForArtifacts: (fileName?: string) => boolean;
7
+ export declare const buildPreMigrationBackupBaseName: ({ from, fileName, }: {
8
+ from: string;
9
+ fileName?: string;
10
+ }) => string;
11
+ export declare const buildStoryBackupBaseName: ({ from, fileName, }: {
12
+ from: string;
13
+ fileName?: string;
14
+ }) => string;
@@ -0,0 +1,19 @@
1
+ export const sanitizeOutputFileBaseName = (value) => {
2
+ const sanitized = value
3
+ .trim()
4
+ .replace(/[\\/]/g, "-")
5
+ .replace(/\s+/g, "-")
6
+ .replace(/[^a-zA-Z0-9._-]/g, "-")
7
+ .replace(/-+/g, "-")
8
+ .replace(/^[-.]+|[-.]+$/g, "");
9
+ return sanitized || "migration-output";
10
+ };
11
+ export const resolveOutputFileBaseName = ({ from, fileName, }) => {
12
+ if (typeof fileName === "string" && fileName.trim().length > 0) {
13
+ return sanitizeOutputFileBaseName(fileName);
14
+ }
15
+ return sanitizeOutputFileBaseName(from);
16
+ };
17
+ export const shouldUseDatestampForArtifacts = (fileName) => !(typeof fileName === "string" && fileName.trim().length > 0);
18
+ export const buildPreMigrationBackupBaseName = ({ from, fileName, }) => `before__${resolveOutputFileBaseName({ from, fileName })}`;
19
+ export const buildStoryBackupBaseName = ({ from, fileName, }) => `${resolveOutputFileBaseName({ from, fileName })}--backup-before-migration`;
@@ -0,0 +1,12 @@
1
+ import type { MapperDefinition } from "./component-data-migration.js";
2
+ export type MigrationComponentAliasesByMigration = Record<string, Record<string, string[]>>;
3
+ export type MigrationComponentOverridesByMigration = Record<string, string[]>;
4
+ export declare const parseMigrationComponentAliasFlags: (value: string | string[] | undefined) => MigrationComponentAliasesByMigration;
5
+ export declare const parseMigrationComponentOverrideFlags: (value: string | string[] | undefined) => MigrationComponentOverridesByMigration;
6
+ export declare const extendMigrationMapperWithAliases: (mapper: Record<string, MapperDefinition>, aliases: Record<string, string[]> | undefined) => Record<string, MapperDefinition>;
7
+ export declare const resolveMigrationComponentsToMigrate: ({ mapper, migrationName, globalComponentsToMigrate, perMigrationOverrides, }: {
8
+ mapper: Record<string, MapperDefinition>;
9
+ migrationName: string;
10
+ globalComponentsToMigrate?: string[];
11
+ perMigrationOverrides?: MigrationComponentOverridesByMigration;
12
+ }) => string[];
@@ -0,0 +1,73 @@
1
+ const normalizeFlagValues = (value) => {
2
+ if (Array.isArray(value)) {
3
+ return value.filter(Boolean);
4
+ }
5
+ if (typeof value === "string" && value.length > 0) {
6
+ return [value];
7
+ }
8
+ return [];
9
+ };
10
+ export const parseMigrationComponentAliasFlags = (value) => {
11
+ const result = {};
12
+ normalizeFlagValues(value).forEach((entry) => {
13
+ const [migrationName, mapping] = entry.split(":");
14
+ const [sourceComponent, aliasesRaw] = (mapping || "").split("=");
15
+ const aliases = (aliasesRaw || "")
16
+ .split(",")
17
+ .map((item) => item.trim())
18
+ .filter(Boolean);
19
+ if (!migrationName || !sourceComponent || aliases.length === 0) {
20
+ throw new Error(`Invalid --migrationComponentAlias value '${entry}'. Expected '<migration>:<source>=<alias1>,<alias2>'.`);
21
+ }
22
+ result[migrationName] = result[migrationName] || {};
23
+ result[migrationName][sourceComponent] = [
24
+ ...(result[migrationName][sourceComponent] || []),
25
+ ...aliases,
26
+ ];
27
+ });
28
+ return result;
29
+ };
30
+ export const parseMigrationComponentOverrideFlags = (value) => {
31
+ const result = {};
32
+ normalizeFlagValues(value).forEach((entry) => {
33
+ const [migrationName, componentsRaw] = entry.split(":");
34
+ const components = (componentsRaw || "")
35
+ .split(",")
36
+ .map((item) => item.trim())
37
+ .filter(Boolean);
38
+ if (!migrationName || components.length === 0) {
39
+ throw new Error(`Invalid --migrationComponents value '${entry}'. Expected '<migration>:<component1>,<component2>'.`);
40
+ }
41
+ result[migrationName] = components;
42
+ });
43
+ return result;
44
+ };
45
+ export const extendMigrationMapperWithAliases = (mapper, aliases) => {
46
+ if (!aliases) {
47
+ return mapper;
48
+ }
49
+ const extendedMapper = { ...mapper };
50
+ Object.entries(aliases).forEach(([sourceComponent, extraComponents]) => {
51
+ const sourceMapper = mapper[sourceComponent];
52
+ if (!sourceMapper) {
53
+ throw new Error(`Cannot alias migration component '${sourceComponent}' because it is not defined in the migration config.`);
54
+ }
55
+ extraComponents.forEach((extraComponent) => {
56
+ extendedMapper[extraComponent] = sourceMapper;
57
+ });
58
+ });
59
+ return extendedMapper;
60
+ };
61
+ export const resolveMigrationComponentsToMigrate = ({ mapper, migrationName, globalComponentsToMigrate, perMigrationOverrides, }) => {
62
+ const resolvedComponents = perMigrationOverrides?.[migrationName] &&
63
+ perMigrationOverrides[migrationName].length > 0
64
+ ? perMigrationOverrides[migrationName]
65
+ : globalComponentsToMigrate && globalComponentsToMigrate.length > 0
66
+ ? globalComponentsToMigrate
67
+ : Object.keys(mapper);
68
+ const missingComponents = resolvedComponents.filter((componentName) => !mapper[componentName]);
69
+ if (missingComponents.length > 0) {
70
+ throw new Error(`Migration '${migrationName}' cannot run for unknown components: ${missingComponents.join(", ")}. Add aliases first or adjust the component override list.`);
71
+ }
72
+ return resolvedComponents;
73
+ };
@@ -0,0 +1,14 @@
1
+ export interface MutationWriteResult {
2
+ ok: boolean;
3
+ id?: number | string;
4
+ name?: string;
5
+ slug?: string;
6
+ error?: unknown;
7
+ }
8
+ export interface MutationWriteSummary {
9
+ total: number;
10
+ successful: number;
11
+ failed: number;
12
+ failedItems: MutationWriteResult[];
13
+ }
14
+ export declare const summarizeMutationWriteResults: (results: PromiseSettledResult<MutationWriteResult>[]) => MutationWriteSummary;
@@ -0,0 +1,26 @@
1
+ export const summarizeMutationWriteResults = (results) => {
2
+ const failedItems = [];
3
+ let successful = 0;
4
+ for (const result of results) {
5
+ if (result.status === "fulfilled" && result.value?.ok) {
6
+ successful++;
7
+ continue;
8
+ }
9
+ if (result.status === "fulfilled") {
10
+ failedItems.push(result.value || {
11
+ ok: false,
12
+ });
13
+ continue;
14
+ }
15
+ failedItems.push({
16
+ ok: false,
17
+ error: result.reason,
18
+ });
19
+ }
20
+ return {
21
+ total: results.length,
22
+ successful,
23
+ failed: failedItems.length,
24
+ failedItems,
25
+ };
26
+ };
@@ -63,10 +63,21 @@ export const updatePreset = (args, config) => {
63
63
  })
64
64
  .then((res) => {
65
65
  Logger.warning(`Preset: '${p.preset.name}' with '${p.preset.id}' id has been updated.`);
66
- return res;
66
+ return {
67
+ ok: true,
68
+ id: p.preset.id,
69
+ name: p.preset.name,
70
+ data: res,
71
+ };
67
72
  })
68
- .catch(() => {
73
+ .catch((error) => {
69
74
  Logger.error(`Error happened. Preset: '${p.preset.name}' with '${p.preset.id}' id has been not updated.`);
75
+ return {
76
+ ok: false,
77
+ id: p.preset.id,
78
+ name: p.preset.name,
79
+ error,
80
+ };
70
81
  });
71
82
  };
72
83
  export const updatePresets = (args, config) => {
@@ -118,9 +118,24 @@ export const updateStory = (content, storyId, options, config) => {
118
118
  })
119
119
  .then((res) => {
120
120
  console.log(`${chalk.green(res.data.story.full_slug)} updated.`);
121
- return res.data;
121
+ return {
122
+ ok: true,
123
+ id: res.data.story.id,
124
+ name: res.data.story.name,
125
+ slug: res.data.story.full_slug,
126
+ data: res.data,
127
+ };
122
128
  })
123
- .catch((err) => console.error(err));
129
+ .catch((err) => {
130
+ console.error(err);
131
+ return {
132
+ ok: false,
133
+ id: storyId,
134
+ name: content?.name,
135
+ slug: content?.full_slug || content?.slug,
136
+ error: err,
137
+ };
138
+ });
124
139
  };
125
140
  export const updateStories = (args, config) => {
126
141
  const { stories, options, spaceId } = args;
@@ -1,7 +1,7 @@
1
1
  export declare const mainDescription = "\n USAGE\n $ sb-mig [command]\n \n COMMANDS\n sync Synchronize components, datasources, roles, stories, assets with Storyblok space.\n discover Discover components, migration configs and write to file or stdout.\n backup Command for backing up anything related to Storyblok\n migrate Migrate content from space to space, or from file to space.\n debug Output extra debugging information\n help This screen\n \n Examples\n $ sb-mig sync components --all\n $ sb-mig debug \n";
2
2
  export declare const syncDescription = "\n Usage\n $ sb-mig sync [components|roles|datasources|plugins|content] [space separated file names] or --all\n \n Description\n Synchronize components, roles, datasources, plugins, content with Storyblok space.\n \n COMMANDS\n components - sync components\n roles - sync roles\n datasources - sync datasources\n plugins - sync plugins\n content - sync content (stories, assets) - ! right now destructive, it will move content from 1 space to another, completelly overwriting it\n \n FLAGS\n --all - Sync all components, roles, datasources [components, roles, datasources]\n --presets - Pass it, if u want to sync also with presets (will take longer) [components only]\n \n --yes - Skip ask for confirmation (dangerous, but useful in CI/CD) [content only]\n --from - Space ID from which you want to sync content [content only]\n --to - Space ID to which you want to sync content [content only]\n --syncDirection [fromSpaceToFile|fromFileToSpace|fromSpaceToSpace|fromAWStoSpace] \n - Sync direction (from, to) [content only]\n \n EXAMPLES\n $ sb-mig sync components --all\n $ sb-mig sync components --all --presets\n $ sb-mig sync components accordion accordion-item\n $ sb-mig sync components accordion accordion-item --presets\n \n $ sb-mig sync roles --all\n \n $ sb-mig sync datasources --all\n \n $ sb-mig sync plugins my-awesome-plugin - (you have to be in catalog which has ./dist/export.js file with compiled plugin)\n \n $ sb-mig sync content --all --from 12345 --to 12345\n $ sb-mig sync content --stories --from 12345 --to 12345\n $ sb-mig sync content --assets --from 12345 --to 12345\n";
3
3
  export declare const copyDescription = "\n Usage\n $ sb-mig copy\n \n Description\n Copy stuff\n \n COMMANDS\n ?\n \n FLAGS\n ?\n \n EXAMPLES\n $ sb-mig copy ?\n";
4
- export declare const migrateDescription = "\n Usage\n $ sb-mig migrate [content] [space separated file names] or --all --from [spaceId] --to [spaceId] --migration [migration-config-filename]\n $ sb-mig migrate content --all --migration migration-a --migration migration-b --migration migration-c\n \n Description\n Migrate content from space to space, or from file to space. It's potentially dangerous command, so it will ask for confirmation.\n Use with care.\n \n COMMANDS\n content - migrate content \n \n FLAGS\n --from - Space ID from which you want to migrate / or file name if passed '--migrate-from file'\n --fromFilePath - Direct path to stories JSON file when using '--migrate-from file'\n --to - Space ID to which you want to migrate\n --migrate-from - Migrate from (space, file) default: space\n --migration - File name of migration file (without extension). Can be repeated for ordered pipeline in content migration.\n --withSlug - Filter stories by full slug (can be repeated)\n --startsWith - Filter stories by starts_with prefix\n --yes - Skip ask for confirmation (dangerous, but useful in CI/CD)\n --dry-run - Preview what would be migrated without making any API changes\n --fileName - Stable base name for migration output files (disables timestamp suffix for migration artifacts)\n\n EXAMPLES\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration file-with-migration\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration migration-a --migration migration-b --migration migration-c\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration file-with-migration --withSlug blog/home --withSlug docs/getting-started\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration file-with-migration --startsWith blog/\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration v3toV4AllMigrations --dry-run --fileName brand-hub-v3-v4-run\n $ sb-mig migrate content --all --migrate-from file --from file-with-stories --to 12345 --migration file-with-migration\n $ sb-mig migrate content --all --migrate-from file --fromFilePath sbmig/migrations/dry-run--123---story-to-migrate__2026-2-9_20-51.json --to 12345 --migration migration-a --migration migration-b\n $ sb-mig migrate content my-component-1 my-component-2 --from 12345 --to 12345 --migration file-with-migration\n $ sb-mig migrate content my-component-1 my-component-2 --migrate-from file --from file-with-stories --to 12345 --migration file-with-migration \n";
4
+ export declare const migrateDescription = "\n Usage\n $ sb-mig migrate [content] [space separated file names] or --all --from [spaceId] --to [spaceId] --migration [migration-config-filename]\n $ sb-mig migrate content --all --migration migration-a --migration migration-b --migration migration-c\n \n Description\n Migrate content from space to space, or from file to space. It's potentially dangerous command, so it will ask for confirmation.\n Use with care.\n \n COMMANDS\n content - migrate content \n \n FLAGS\n --from - Space ID from which you want to migrate / or file name if passed '--migrate-from file'\n --fromFilePath - Direct path to stories JSON file when using '--migrate-from file'\n --to - Space ID to which you want to migrate\n --migrate-from - Migrate from (space, file) default: space\n --migration - File name of migration file (without extension). Can be repeated for ordered pipeline in content migration.\n --migrationComponentAlias - Add extra component aliases for a migration. Repeatable. Format: <migration>:<source>=<alias1>,<alias2>\n --migrationComponents - Override the exact component scope for a migration. Repeatable. Format: <migration>:<component1>,<component2>\n --withSlug - Filter stories by full slug (can be repeated)\n --startsWith - Filter stories by starts_with prefix\n --yes - Skip ask for confirmation (dangerous, but useful in CI/CD)\n --dry-run - Preview what would be migrated without making any API changes\n --fileName - Stable base name for migration output files (disables timestamp suffix for migration artifacts)\n\n EXAMPLES\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration file-with-migration\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration migration-a --migration migration-b --migration migration-c\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration colorPickerModeValues --migrationComponentAlias colorPickerModeValues:sb-button=sb-open-drift-button\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration colorPickerModeValues --migrationComponentAlias colorPickerModeValues:sb-section=sb-tour-page-section --migrationComponents colorPickerModeValues:sb-section,sb-tour-page-section\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration file-with-migration --withSlug blog/home --withSlug docs/getting-started\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration file-with-migration --startsWith blog/\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration v3toV4AllMigrations --dry-run --fileName brand-hub-v3-v4-run\n $ sb-mig migrate content --all --migrate-from file --from file-with-stories --to 12345 --migration file-with-migration\n $ sb-mig migrate content --all --migrate-from file --fromFilePath sbmig/migrations/dry-run--123---story-to-migrate__2026-2-9_20-51.json --to 12345 --migration migration-a --migration migration-b\n $ sb-mig migrate content my-component-1 my-component-2 --from 12345 --to 12345 --migration file-with-migration\n $ sb-mig migrate content my-component-1 my-component-2 --migrate-from file --from file-with-stories --to 12345 --migration file-with-migration \n";
5
5
  export declare const revertDescription = "\n Usage\n $ sb-mig revert [content] --migration\n \n Description\n Revert content migration\n \n COMMANDS\n content - revert content migration \n \n FLAGS\n --migration - ???\n --yes - Skip ask for confirmation (dangerous, but useful in CI/CD) \n \n EXAMPLES\n $ sb-mig revert content --migration \n";
6
6
  export declare const discoverDescription = "\n Usage\n $ sb-mig discover [components|migrations] --all --write\n\n Description\n Discover all components or migration configs and write to file or stdout\n\n COMMANDS\n components - discover components\n migrations - discover migration config files\n\n FLAGS\n --all - Discover all components or migration configs\n --write - Write to file\n\n EXAMPLES\n $ sb-mig discover components --all\n $ sb-mig discover components --all --write\n $ sb-mig discover migrations --all\n";
7
7
  export declare const migrationsDescription = "\n Usage\n $ sb-mig migrations recognize\n \n Description\n Recognize migrations you need to apply\n \n COMMANDS\n recognize - recognize migrations\n \n FLAGS \n \n EXAMPLES\n $ sb-mig migrations recognize\n\n";
@@ -88,6 +88,8 @@ export const migrateDescription = `
88
88
  --to - Space ID to which you want to migrate
89
89
  --migrate-from - Migrate from (space, file) default: space
90
90
  --migration - File name of migration file (without extension). Can be repeated for ordered pipeline in content migration.
91
+ --migrationComponentAlias - Add extra component aliases for a migration. Repeatable. Format: <migration>:<source>=<alias1>,<alias2>
92
+ --migrationComponents - Override the exact component scope for a migration. Repeatable. Format: <migration>:<component1>,<component2>
91
93
  --withSlug - Filter stories by full slug (can be repeated)
92
94
  --startsWith - Filter stories by starts_with prefix
93
95
  --yes - Skip ask for confirmation (dangerous, but useful in CI/CD)
@@ -97,6 +99,8 @@ export const migrateDescription = `
97
99
  EXAMPLES
98
100
  $ sb-mig migrate content --all --from 12345 --to 12345 --migration file-with-migration
99
101
  $ sb-mig migrate content --all --from 12345 --to 12345 --migration migration-a --migration migration-b --migration migration-c
102
+ $ sb-mig migrate content --all --from 12345 --to 12345 --migration colorPickerModeValues --migrationComponentAlias colorPickerModeValues:sb-button=sb-open-drift-button
103
+ $ sb-mig migrate content --all --from 12345 --to 12345 --migration colorPickerModeValues --migrationComponentAlias colorPickerModeValues:sb-section=sb-tour-page-section --migrationComponents colorPickerModeValues:sb-section,sb-tour-page-section
100
104
  $ sb-mig migrate content --all --from 12345 --to 12345 --migration file-with-migration --withSlug blog/home --withSlug docs/getting-started
101
105
  $ sb-mig migrate content --all --from 12345 --to 12345 --migration file-with-migration --startsWith blog/
102
106
  $ sb-mig migrate content --all --from 12345 --to 12345 --migration v3toV4AllMigrations --dry-run --fileName brand-hub-v3-v4-run
@@ -1,5 +1,7 @@
1
1
  import path from "path";
2
2
  import { migrateAllComponentsDataInStories, migrateProvidedComponentsDataInStories, } from "../../api/data-migration/component-data-migration.js";
3
+ import { buildStoryBackupBaseName } from "../../api/data-migration/file-naming.js";
4
+ import { parseMigrationComponentAliasFlags, parseMigrationComponentOverrideFlags, } from "../../api/data-migration/migration-component-scope.js";
3
5
  import { managementApi } from "../../api/managementApi.js";
4
6
  import { backupStories } from "../../api/stories/backup.js";
5
7
  import { createAndSaveToFile } from "../../utils/files.js";
@@ -33,6 +35,8 @@ export const migrate = async (props) => {
33
35
  "fromFilePath",
34
36
  "pageId",
35
37
  "migration",
38
+ "migrationComponentAlias",
39
+ "migrationComponents",
36
40
  "yes",
37
41
  "withSlug",
38
42
  "startsWith",
@@ -53,6 +57,8 @@ export const migrate = async (props) => {
53
57
  getFrom(flags, apiConfig);
54
58
  const to = getTo(flags, apiConfig);
55
59
  const migrationConfigs = normalizeMigrationFlags(flags["migration"]);
60
+ const migrationComponentAliases = parseMigrationComponentAliasFlags(flags["migrationComponentAlias"]);
61
+ const migrationComponentOverrides = parseMigrationComponentOverrideFlags(flags["migrationComponents"]);
56
62
  const dryRun = flags["dryRun"];
57
63
  const fileName = flags["fileName"];
58
64
  const withSlugFlag = flags["withSlug"];
@@ -72,7 +78,10 @@ export const migrate = async (props) => {
72
78
  Logger.warning("Preparing to migrate...");
73
79
  if (!dryRun) {
74
80
  await backupStories({
75
- filename: `${from}--backup-before-migration___${migrationConfigs.join("__")}`,
81
+ filename: buildStoryBackupBaseName({
82
+ from,
83
+ fileName,
84
+ }),
76
85
  suffix: ".sb.stories",
77
86
  spaceId: from,
78
87
  }, apiConfig);
@@ -84,6 +93,8 @@ export const migrate = async (props) => {
84
93
  migrateFrom,
85
94
  componentsToMigrate,
86
95
  migrationConfig: migrationConfigs,
96
+ migrationComponentAliases,
97
+ migrationComponentOverrides,
87
98
  filters: { withSlug, startsWith },
88
99
  dryRun,
89
100
  fromFilePath,
@@ -109,6 +120,8 @@ export const migrate = async (props) => {
109
120
  to,
110
121
  migrateFrom,
111
122
  migrationConfig: migrationConfigs,
123
+ migrationComponentAliases,
124
+ migrationComponentOverrides,
112
125
  filters: { withSlug, startsWith },
113
126
  dryRun,
114
127
  fromFilePath,
@@ -134,6 +147,8 @@ export const migrate = async (props) => {
134
147
  case MIGRATE_COMMANDS.presets: {
135
148
  Logger.log(`Migrating content with command: ${command}`);
136
149
  const migrationConfigs = normalizeMigrationFlags(flags["migration"]);
150
+ const migrationComponentAliases = parseMigrationComponentAliasFlags(flags["migrationComponentAlias"]);
151
+ const migrationComponentOverrides = parseMigrationComponentOverrideFlags(flags["migrationComponents"]);
137
152
  const fromFilePath = flags["fromFilePath"];
138
153
  const migrateFromFlag = flags["migrateFrom"];
139
154
  const fromFallback = migrateFromFlag === "file" && fromFilePath
@@ -169,6 +184,8 @@ export const migrate = async (props) => {
169
184
  to,
170
185
  migrateFrom,
171
186
  migrationConfig: migrationConfigs[0],
187
+ migrationComponentAliases,
188
+ migrationComponentOverrides,
172
189
  dryRun,
173
190
  fromFilePath,
174
191
  fileName,
@@ -19,18 +19,23 @@ export const askForConfirmation = async (message, resolveYes, resolveNo, ci) =>
19
19
  output: process.stdout,
20
20
  prompt: chalk.red(`${message} (y/n) > `),
21
21
  });
22
- rl.prompt();
23
- for await (const deletionConfirmation of rl) {
24
- if (deletionConfirmation.trim() !== "y") {
25
- await resolveNo();
26
- process.exit(0);
27
- }
28
- else {
29
- if (deletionConfirmation) {
30
- await resolveYes();
31
- break;
22
+ try {
23
+ rl.prompt();
24
+ for await (const deletionConfirmation of rl) {
25
+ if (deletionConfirmation.trim() !== "y") {
26
+ await resolveNo();
27
+ process.exit(0);
28
+ }
29
+ else {
30
+ if (deletionConfirmation) {
31
+ await resolveYes();
32
+ break;
33
+ }
32
34
  }
35
+ rl.prompt();
33
36
  }
34
- rl.prompt();
37
+ }
38
+ finally {
39
+ rl.close();
35
40
  }
36
41
  };
package/dist/cli/index.js CHANGED
@@ -61,6 +61,14 @@ app.migrate = () => ({
61
61
  type: "string",
62
62
  isMultiple: true,
63
63
  },
64
+ migrationComponentAlias: {
65
+ type: "string",
66
+ isMultiple: true,
67
+ },
68
+ migrationComponents: {
69
+ type: "string",
70
+ isMultiple: true,
71
+ },
64
72
  withSlug: {
65
73
  type: "string",
66
74
  isMultiple: true,
@@ -72,10 +72,21 @@ const updatePreset = (args, config) => {
72
72
  })
73
73
  .then((res) => {
74
74
  logger_js_1.default.warning(`Preset: '${p.preset.name}' with '${p.preset.id}' id has been updated.`);
75
- return res;
75
+ return {
76
+ ok: true,
77
+ id: p.preset.id,
78
+ name: p.preset.name,
79
+ data: res,
80
+ };
76
81
  })
77
- .catch(() => {
82
+ .catch((error) => {
78
83
  logger_js_1.default.error(`Error happened. Preset: '${p.preset.name}' with '${p.preset.id}' id has been not updated.`);
84
+ return {
85
+ ok: false,
86
+ id: p.preset.id,
87
+ name: p.preset.name,
88
+ error,
89
+ };
79
90
  });
80
91
  };
81
92
  exports.updatePreset = updatePreset;
@@ -130,9 +130,24 @@ const updateStory = (content, storyId, options, config) => {
130
130
  })
131
131
  .then((res) => {
132
132
  console.log(`${chalk_1.default.green(res.data.story.full_slug)} updated.`);
133
- return res.data;
133
+ return {
134
+ ok: true,
135
+ id: res.data.story.id,
136
+ name: res.data.story.name,
137
+ slug: res.data.story.full_slug,
138
+ data: res.data,
139
+ };
134
140
  })
135
- .catch((err) => console.error(err));
141
+ .catch((err) => {
142
+ console.error(err);
143
+ return {
144
+ ok: false,
145
+ id: storyId,
146
+ name: content?.name,
147
+ slug: content?.full_slug || content?.slug,
148
+ error: err,
149
+ };
150
+ });
136
151
  };
137
152
  exports.updateStory = updateStory;
138
153
  const updateStories = (args, config) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sb-mig",
3
- "version": "5.6.2-beta.1",
3
+ "version": "5.7.0",
4
4
  "description": "CLI to rule the world. (and handle stuff related to Storyblok CMS)",
5
5
  "author": "Marcin Krawczyk <marckraw@icloud.com>",
6
6
  "license": "MIT",