sb-mig 6.3.0-beta.1 → 6.3.0-beta.2

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.
@@ -1,7 +1,9 @@
1
1
  import type { PublishLanguagesOption } from "../stories/stories.types.js";
2
2
  import type { RequestBaseConfig } from "../utils/request.js";
3
+ import { type LanguagePublishStateMap } from "../stories/language-publish-state.js";
3
4
  import { type MigrationComponentAliasesByMigration, type MigrationComponentOverridesByMigration } from "./migration-component-scope.js";
4
5
  import { type PreparedMigrationValidator } from "./migration-validation.js";
6
+ import { type PublishedLayerContext } from "./published-layer.js";
5
7
  export type MigrateFrom = "file" | "space";
6
8
  export type PublicationMode = "preserve-layers" | "collapse-draft" | "save-only";
7
9
  export interface PreparedMigrationConfig {
@@ -73,6 +75,60 @@ export declare const runMigrationPipelineInMemory: ({ itemType, itemsToMigrate,
73
75
  preparedMigrationConfigs: PreparedMigrationConfig[];
74
76
  }) => MigrationPipelineResult;
75
77
  export declare const migrateAllComponentsDataInStories: ({ itemType, migrationConfig, migrateFrom, from, to, filters, dryRun, publicationMode, publicationLanguages, fromFilePath, fileName, languagePublishStatePath, migrationComponentAliases, migrationComponentOverrides, }: Omit<MigrateItems, "componentsToMigrate" | "preparedMigrationConfigs">, config: RequestBaseConfig) => Promise<void>;
78
+ export interface ContinueManifest {
79
+ manifestVersion: 1;
80
+ createdAt?: string;
81
+ kind: "migrate-content-continue-manifest";
82
+ itemType: "story" | "preset";
83
+ from: string;
84
+ to: string;
85
+ migrateFrom: MigrateFrom;
86
+ fromFilePath?: string | null;
87
+ publicationMode: PublicationMode;
88
+ migrationConfigNames: string[];
89
+ publishLanguages?: {
90
+ requested?: PublishLanguagesOption;
91
+ } | null;
92
+ resolvedPublishLanguages?: string[] | null;
93
+ artifactBaseName: string;
94
+ useDatestamp: boolean;
95
+ artifacts: {
96
+ changedItems: string;
97
+ pipelineSummary: string;
98
+ inputFull?: string | null;
99
+ languageStateMap?: string | null;
100
+ draftAfterFull?: string | null;
101
+ publishedAfterFull?: string | null;
102
+ dirtyPublishedRecords?: string | null;
103
+ };
104
+ }
105
+ interface FinalizeMigrationArgs {
106
+ itemType: "story" | "preset";
107
+ from: string;
108
+ to: string;
109
+ migrateFrom: MigrateFrom;
110
+ fromFilePath?: string;
111
+ languagePublishStatePath?: string;
112
+ publicationMode: PublicationMode;
113
+ effectivePublicationLanguages?: PublishLanguagesOption;
114
+ resolvedPublishLanguages?: string[];
115
+ languagePublishStateMap?: LanguagePublishStateMap;
116
+ appliedMigrationNames: string[];
117
+ pipelineResult: MigrationPipelineResult;
118
+ publishedLayerContext?: PublishedLayerContext;
119
+ publishedLayerPipelineResult: MigrationPipelineResult | null;
120
+ artifactBaseName: string;
121
+ useDatestamp: boolean;
122
+ continuedFromManifest?: string;
123
+ }
124
+ /**
125
+ * Everything `doTheMigration` does AFTER the dry-run preview returns:
126
+ * mark migrations as applied, write the changes to Storyblok, and write the
127
+ * run log. Extracted so `migrate continue` can run the exact same path against
128
+ * inputs reconstructed from dry-run artifacts (instead of re-pulling + re-transforming).
129
+ */
130
+ export declare const finalizeMigration: ({ itemType, from, to, migrateFrom, fromFilePath, languagePublishStatePath, publicationMode, effectivePublicationLanguages, resolvedPublishLanguages, languagePublishStateMap, appliedMigrationNames, pipelineResult, publishedLayerContext, publishedLayerPipelineResult, artifactBaseName, useDatestamp, continuedFromManifest, }: FinalizeMigrationArgs, config: RequestBaseConfig) => Promise<void>;
131
+ export declare const findArtifactFilename: (files: string[], stem: string) => string | null;
76
132
  export declare const doTheMigration: ({ itemType, from, itemsToMigrate, migrationConfig, migrationConfigs, to, dryRun, publicationMode, publicationLanguages, migrateFrom, fromFilePath, fileName, languagePublishStatePath, }: {
77
133
  itemType?: "story" | "preset";
78
134
  from: string;
@@ -89,4 +145,29 @@ export declare const doTheMigration: ({ itemType, from, itemsToMigrate, migratio
89
145
  languagePublishStatePath?: string;
90
146
  }, config: RequestBaseConfig) => Promise<void>;
91
147
  export declare const migrateProvidedComponentsDataInStories: ({ itemType, migrationConfig, migrateFrom, from, to, componentsToMigrate, filters, dryRun, publicationMode, publicationLanguages, fromFilePath, fileName, languagePublishStatePath, preparedMigrationConfigs, migrationComponentAliases, migrationComponentOverrides, }: MigrateItems, config: RequestBaseConfig) => Promise<void>;
148
+ export interface ContinuePlanSummary {
149
+ manifestFileName: string;
150
+ from: string;
151
+ to: string;
152
+ itemType: "story" | "preset";
153
+ publicationMode: PublicationMode;
154
+ migrationConfigNames: string[];
155
+ resolvedPublishLanguages: string[];
156
+ changedCount: number;
157
+ dirtyPublishedCount: number;
158
+ artifactFiles: string[];
159
+ }
160
+ export interface ContinuePlan {
161
+ summary: ContinuePlanSummary;
162
+ run: () => Promise<void>;
163
+ }
164
+ /**
165
+ * Discover the dry-run `continue` manifest in the migrations folder, load the
166
+ * artifacts it points at, and reconstruct the exact inputs `finalizeMigration`
167
+ * needs. Returns a summary (for the confirmation prompt) and a `run()` thunk that
168
+ * performs the write. Does NOT pull stories, transform, or fetch published layers.
169
+ */
170
+ export declare const prepareContinueMigration: ({ manifestFileName }: {
171
+ manifestFileName?: string;
172
+ }, config: RequestBaseConfig) => Promise<ContinuePlan>;
92
173
  export {};
@@ -2,7 +2,7 @@ import path from "path";
2
2
  import chalk from "chalk";
3
3
  import { discoverMigrationConfig, discoverStories, LOOKUP_TYPE, SCOPE, } from "../../cli/utils/discover.js";
4
4
  import storyblokConfig from "../../config/config.js";
5
- import { createAndSaveToFile, getFileContentWithRequire, getFilesContentWithRequire, } from "../../utils/files.js";
5
+ import { createAndSaveToFile, getFileContentWithRequire, getFilesContentWithRequire, listFilesInDir, readFile, } from "../../utils/files.js";
6
6
  import Logger from "../../utils/logger.js";
7
7
  import { modifyOrCreateAppliedMigrationsFile } from "../../utils/migrations.js";
8
8
  import { isObjectEmpty } from "../../utils/object-utils.js";
@@ -810,6 +810,222 @@ export const migrateAllComponentsDataInStories = async ({ itemType, migrationCon
810
810
  preparedMigrationConfigs,
811
811
  }, config);
812
812
  };
813
+ /**
814
+ * Everything `doTheMigration` does AFTER the dry-run preview returns:
815
+ * mark migrations as applied, write the changes to Storyblok, and write the
816
+ * run log. Extracted so `migrate continue` can run the exact same path against
817
+ * inputs reconstructed from dry-run artifacts (instead of re-pulling + re-transforming).
818
+ */
819
+ export const finalizeMigration = async ({ itemType, from, to, migrateFrom, fromFilePath, languagePublishStatePath, publicationMode, effectivePublicationLanguages, resolvedPublishLanguages, languagePublishStateMap, appliedMigrationNames, pipelineResult, publishedLayerContext, publishedLayerPipelineResult, artifactBaseName, useDatestamp, continuedFromManifest, }, config) => {
820
+ for (const migrationConfigName of appliedMigrationNames) {
821
+ await modifyOrCreateAppliedMigrationsFile(migrationConfigName, itemType);
822
+ }
823
+ const publishedLayerChangedCount = publishedLayerPipelineResult?.changedItems.length ?? 0;
824
+ if (pipelineResult.changedItems.length === 0 &&
825
+ publishedLayerChangedCount === 0) {
826
+ return;
827
+ }
828
+ let writeResults = [];
829
+ if (itemType === "story") {
830
+ if (shouldUsePublishedLayerMode(publicationMode) &&
831
+ publishedLayerContext) {
832
+ const dirtyStoryIds = new Set(publishedLayerContext.dirtyPublishedRecords.map((record) => String(record.storyId)));
833
+ const nonDirtyChangedItems = pipelineResult.changedItems.filter((item) => !dirtyStoryIds.has(storyIdOf(item)));
834
+ const nonDirtyWriteResults = nonDirtyChangedItems.length > 0
835
+ ? await managementApi.stories.updateStories({
836
+ stories: nonDirtyChangedItems,
837
+ spaceId: to,
838
+ options: {
839
+ publish: shouldPublishForPublicationMode(publicationMode),
840
+ publishLanguages: resolvedPublishLanguages,
841
+ preservePublishState: shouldPublishForPublicationMode(publicationMode),
842
+ languagePublishStateMap,
843
+ },
844
+ }, config)
845
+ : [];
846
+ const dirtyWriteResults = publishedLayerPipelineResult
847
+ ? await writeDirtyPublishedLayerStories({
848
+ context: publishedLayerContext,
849
+ draftPipelineResult: pipelineResult,
850
+ publishedPipelineResult: publishedLayerPipelineResult,
851
+ to,
852
+ resolvedPublishLanguages,
853
+ languagePublishStateMap,
854
+ }, config)
855
+ : [];
856
+ writeResults = [...nonDirtyWriteResults, ...dirtyWriteResults];
857
+ }
858
+ else {
859
+ writeResults = await managementApi.stories.updateStories({
860
+ stories: pipelineResult.changedItems,
861
+ spaceId: to,
862
+ options: {
863
+ publish: shouldPublishForPublicationMode(publicationMode),
864
+ publishLanguages: resolvedPublishLanguages,
865
+ preservePublishState: shouldPublishForPublicationMode(publicationMode),
866
+ publishDirtyPublishedStories: shouldPublishDirtyDrafts(publicationMode),
867
+ languagePublishStateMap,
868
+ },
869
+ }, config);
870
+ }
871
+ }
872
+ else if (itemType === "preset") {
873
+ writeResults = await managementApi.presets.updatePresets({
874
+ presets: pipelineResult.changedItems,
875
+ spaceId: to,
876
+ options: {},
877
+ }, config);
878
+ }
879
+ const writeSummary = summarizeMutationWriteResults(writeResults);
880
+ try {
881
+ await saveMigrationRunLog({
882
+ artifactBaseName,
883
+ useDatestamp,
884
+ from,
885
+ to,
886
+ itemType,
887
+ dryRun: false,
888
+ publish: shouldPublishForPublicationMode(publicationMode),
889
+ publishLanguages: effectivePublicationLanguages,
890
+ resolvedPublishLanguages,
891
+ migrateFrom,
892
+ fromFilePath,
893
+ languagePublishStatePath,
894
+ pipelineResult,
895
+ writeResults,
896
+ writeSummary,
897
+ continuedFromManifest,
898
+ }, config);
899
+ }
900
+ catch (error) {
901
+ Logger.warning(`[MIGRATION] Could not write migration run log: ${error instanceof Error ? error.message : String(error)}`);
902
+ }
903
+ if (writeSummary.failed === 0) {
904
+ Logger.success(`[MIGRATION] Update complete. ${writeSummary.successful}/${writeSummary.total} ${itemType}(s) updated successfully.`);
905
+ return;
906
+ }
907
+ Logger.warning(`[MIGRATION] Update complete with partial failures. ${writeSummary.successful}/${writeSummary.total} ${itemType}(s) updated successfully, ${writeSummary.failed} failed.`);
908
+ writeSummary.failedItems.slice(0, 10).forEach((item) => {
909
+ const label = item.slug || item.name || item.id || "unknown";
910
+ Logger.error(`[MIGRATION] Failed ${itemType}: ${String(label)}`);
911
+ });
912
+ if (writeSummary.failedItems.length > 10) {
913
+ Logger.warning(`[MIGRATION] Showing first 10 failed ${itemType}(s) only.`);
914
+ }
915
+ };
916
+ /**
917
+ * Sortable key for an artifact's datestamp suffix.
918
+ *
919
+ * Datestamps use the non-zero-padded `YYYY-M-D_H-M` format from
920
+ * generateDatestamp, so a plain string sort is NOT chronological
921
+ * (e.g. "2026-1-15_9-30" sorts before "2026-1-9_9-30"). Parse the parts and
922
+ * compare numerically instead. Returns -1 for the non-datestamped variant
923
+ * (`${stem}.json`), which only exists when --fileName is used (single match,
924
+ * never mixed with datestamped files for the same stem).
925
+ */
926
+ const artifactDatestampKey = (filename, stem) => {
927
+ const prefix = `${stem}__`;
928
+ if (!filename.startsWith(prefix) || !filename.endsWith(".json")) {
929
+ return -1;
930
+ }
931
+ const stamp = filename.slice(prefix.length, -".json".length);
932
+ const match = /^(\d+)-(\d+)-(\d+)_(\d+)-(\d+)$/.exec(stamp);
933
+ if (!match) {
934
+ return -1;
935
+ }
936
+ const [, year, month, day, hour, minute] = match;
937
+ // month/day/hour/minute each fit in two decimal digits, so this packs the
938
+ // parts into one monotonically increasing key.
939
+ return (Number(year) * 100_000_000 +
940
+ Number(month) * 1_000_000 +
941
+ Number(day) * 10_000 +
942
+ Number(hour) * 100 +
943
+ Number(minute));
944
+ };
945
+ export const findArtifactFilename = (files, stem) => {
946
+ const matches = files.filter((file) => file === `${stem}.json` || file.startsWith(`${stem}__`));
947
+ if (matches.length === 0) {
948
+ return null;
949
+ }
950
+ // Pick the chronologically newest match. Do not lexicographically sort:
951
+ // the datestamp format is not zero-padded, so string order is not time order.
952
+ return matches.reduce((newest, candidate) => artifactDatestampKey(candidate, stem) >=
953
+ artifactDatestampKey(newest, stem)
954
+ ? candidate
955
+ : newest);
956
+ };
957
+ /**
958
+ * Written at the end of a dry-run only. A small machine file that lets
959
+ * `migrate continue` finish the migration without re-pulling/re-transforming:
960
+ * carries the scalars that aren't otherwise saved (target space, exact migration
961
+ * names, run-log metadata) and references the heavy dry-run artifacts by filename.
962
+ */
963
+ const saveContinueManifest = async ({ artifactBaseName, useDatestamp, dryRun, itemType, from, to, migrateFrom, fromFilePath, publicationMode, effectivePublicationLanguages, resolvedPublishLanguages, appliedMigrationNames, publishedLayerContext, publishedLayerPipelineResult, }, config) => {
964
+ if (!dryRun) {
965
+ return;
966
+ }
967
+ const isPreserveLayers = shouldUsePublishedLayerMode(publicationMode) &&
968
+ Boolean(publishedLayerContext);
969
+ if (isPreserveLayers && publishedLayerContext) {
970
+ const publishedChangedIds = (publishedLayerPipelineResult?.changedItems ?? []).map((item) => storyIdOf(item));
971
+ await createAndSaveToFile({
972
+ datestamp: useDatestamp,
973
+ ext: "json",
974
+ filename: `dry-run--${artifactBaseName}---dirty-published-records`,
975
+ folder: "migrations",
976
+ res: {
977
+ dirtyPublishedRecords: publishedLayerContext.dirtyPublishedRecords,
978
+ publishedChangedIds,
979
+ },
980
+ }, config);
981
+ }
982
+ // Resolve the real sibling filenames from disk. The datestamp suffix has
983
+ // minute granularity, so sibling artifacts can in rare cases differ; reading
984
+ // the folder avoids guessing the exact name.
985
+ const files = await listFilesInDir(`${config.sbmigWorkingDirectory}/migrations`);
986
+ const resolve = (stem) => findArtifactFilename(files, `dry-run--${artifactBaseName}---${stem}`);
987
+ const artifacts = {
988
+ changedItems: resolve(`${itemType}-to-migrate`) ?? "",
989
+ pipelineSummary: resolve(`${itemType}-migration-pipeline-summary`) ?? "",
990
+ inputFull: resolve(`${itemType}-input-full`),
991
+ languageStateMap: resolve("language-publish-state-map"),
992
+ draftAfterFull: isPreserveLayers
993
+ ? resolve("draft-current-after-full")
994
+ : null,
995
+ publishedAfterFull: isPreserveLayers
996
+ ? resolve("published-layer-after-full")
997
+ : null,
998
+ dirtyPublishedRecords: isPreserveLayers
999
+ ? resolve("dirty-published-records")
1000
+ : null,
1001
+ };
1002
+ const manifest = {
1003
+ manifestVersion: 1,
1004
+ createdAt: new Date().toISOString(),
1005
+ kind: "migrate-content-continue-manifest",
1006
+ itemType,
1007
+ from,
1008
+ to,
1009
+ migrateFrom,
1010
+ fromFilePath: fromFilePath ?? null,
1011
+ publicationMode,
1012
+ migrationConfigNames: appliedMigrationNames,
1013
+ publishLanguages: publicationMode === "save-only"
1014
+ ? null
1015
+ : { requested: effectivePublicationLanguages },
1016
+ resolvedPublishLanguages: resolvedPublishLanguages ?? null,
1017
+ artifactBaseName,
1018
+ useDatestamp,
1019
+ artifacts,
1020
+ };
1021
+ await createAndSaveToFile({
1022
+ datestamp: useDatestamp,
1023
+ ext: "json",
1024
+ filename: `dry-run--${artifactBaseName}---${itemType}-continue-manifest`,
1025
+ folder: "migrations",
1026
+ res: manifest,
1027
+ }, config);
1028
+ };
813
1029
  export const doTheMigration = async ({ itemType = "story", from, itemsToMigrate, migrationConfig, migrationConfigs, to, dryRun, publicationMode = DEFAULT_PUBLICATION_MODE, publicationLanguages, migrateFrom, fromFilePath, fileName, languagePublishStatePath, }, config) => {
814
1030
  const preparedMigrationConfigs = migrationConfigs ||
815
1031
  prepareMigrationConfigs({
@@ -952,6 +1168,23 @@ export const doTheMigration = async ({ itemType = "story", from, itemsToMigrate,
952
1168
  languagePublishStatePath,
953
1169
  pipelineResult,
954
1170
  }, config);
1171
+ await saveContinueManifest({
1172
+ artifactBaseName,
1173
+ useDatestamp,
1174
+ dryRun,
1175
+ itemType,
1176
+ from,
1177
+ to,
1178
+ migrateFrom,
1179
+ fromFilePath,
1180
+ publicationMode,
1181
+ effectivePublicationLanguages,
1182
+ resolvedPublishLanguages,
1183
+ appliedMigrationNames: preparedMigrationConfigs.map((preparedMigrationConfig) => preparedMigrationConfig.migrationConfigName),
1184
+ pipelineResult,
1185
+ publishedLayerContext,
1186
+ publishedLayerPipelineResult,
1187
+ }, config);
955
1188
  if (dryRun) {
956
1189
  console.log(" ");
957
1190
  if (publishedLayerPipelineResult) {
@@ -961,100 +1194,24 @@ export const doTheMigration = async ({ itemType = "story", from, itemsToMigrate,
961
1194
  Logger.success(`[DRY RUN] No API changes were made. Review the saved migration file for details.`);
962
1195
  return;
963
1196
  }
964
- for (const preparedMigrationConfig of preparedMigrationConfigs) {
965
- await modifyOrCreateAppliedMigrationsFile(preparedMigrationConfig.migrationConfigName, itemType);
966
- }
967
- const publishedLayerChangedCount = publishedLayerPipelineResult?.changedItems.length ?? 0;
968
- if (pipelineResult.changedItems.length === 0 &&
969
- publishedLayerChangedCount === 0) {
970
- return;
971
- }
972
- let writeResults = [];
973
- if (itemType === "story") {
974
- if (shouldUsePublishedLayerMode(publicationMode) &&
975
- publishedLayerContext) {
976
- const dirtyStoryIds = new Set(publishedLayerContext.dirtyPublishedRecords.map((record) => String(record.storyId)));
977
- const nonDirtyChangedItems = pipelineResult.changedItems.filter((item) => !dirtyStoryIds.has(storyIdOf(item)));
978
- const nonDirtyWriteResults = nonDirtyChangedItems.length > 0
979
- ? await managementApi.stories.updateStories({
980
- stories: nonDirtyChangedItems,
981
- spaceId: to,
982
- options: {
983
- publish: shouldPublishForPublicationMode(publicationMode),
984
- publishLanguages: resolvedPublishLanguages,
985
- preservePublishState: shouldPublishForPublicationMode(publicationMode),
986
- languagePublishStateMap,
987
- },
988
- }, config)
989
- : [];
990
- const dirtyWriteResults = publishedLayerPipelineResult
991
- ? await writeDirtyPublishedLayerStories({
992
- context: publishedLayerContext,
993
- draftPipelineResult: pipelineResult,
994
- publishedPipelineResult: publishedLayerPipelineResult,
995
- to,
996
- resolvedPublishLanguages,
997
- languagePublishStateMap,
998
- }, config)
999
- : [];
1000
- writeResults = [...nonDirtyWriteResults, ...dirtyWriteResults];
1001
- }
1002
- else {
1003
- writeResults = await managementApi.stories.updateStories({
1004
- stories: pipelineResult.changedItems,
1005
- spaceId: to,
1006
- options: {
1007
- publish: shouldPublishForPublicationMode(publicationMode),
1008
- publishLanguages: resolvedPublishLanguages,
1009
- preservePublishState: shouldPublishForPublicationMode(publicationMode),
1010
- publishDirtyPublishedStories: shouldPublishDirtyDrafts(publicationMode),
1011
- languagePublishStateMap,
1012
- },
1013
- }, config);
1014
- }
1015
- }
1016
- else if (itemType === "preset") {
1017
- writeResults = await managementApi.presets.updatePresets({
1018
- presets: pipelineResult.changedItems,
1019
- spaceId: to,
1020
- options: {},
1021
- }, config);
1022
- }
1023
- const writeSummary = summarizeMutationWriteResults(writeResults);
1024
- try {
1025
- await saveMigrationRunLog({
1026
- artifactBaseName,
1027
- useDatestamp,
1028
- from,
1029
- to,
1030
- itemType,
1031
- dryRun,
1032
- publish: shouldPublishForPublicationMode(publicationMode),
1033
- publishLanguages: effectivePublicationLanguages,
1034
- resolvedPublishLanguages,
1035
- migrateFrom,
1036
- fromFilePath,
1037
- languagePublishStatePath,
1038
- pipelineResult,
1039
- writeResults,
1040
- writeSummary,
1041
- }, config);
1042
- }
1043
- catch (error) {
1044
- Logger.warning(`[MIGRATION] Could not write migration run log: ${error instanceof Error ? error.message : String(error)}`);
1045
- }
1046
- if (writeSummary.failed === 0) {
1047
- Logger.success(`[MIGRATION] Update complete. ${writeSummary.successful}/${writeSummary.total} ${itemType}(s) updated successfully.`);
1048
- return;
1049
- }
1050
- Logger.warning(`[MIGRATION] Update complete with partial failures. ${writeSummary.successful}/${writeSummary.total} ${itemType}(s) updated successfully, ${writeSummary.failed} failed.`);
1051
- writeSummary.failedItems.slice(0, 10).forEach((item) => {
1052
- const label = item.slug || item.name || item.id || "unknown";
1053
- Logger.error(`[MIGRATION] Failed ${itemType}: ${String(label)}`);
1054
- });
1055
- if (writeSummary.failedItems.length > 10) {
1056
- Logger.warning(`[MIGRATION] Showing first 10 failed ${itemType}(s) only.`);
1057
- }
1197
+ await finalizeMigration({
1198
+ itemType,
1199
+ from,
1200
+ to,
1201
+ migrateFrom,
1202
+ fromFilePath,
1203
+ languagePublishStatePath,
1204
+ publicationMode,
1205
+ effectivePublicationLanguages,
1206
+ resolvedPublishLanguages,
1207
+ languagePublishStateMap,
1208
+ appliedMigrationNames: preparedMigrationConfigs.map((preparedMigrationConfig) => preparedMigrationConfig.migrationConfigName),
1209
+ pipelineResult,
1210
+ publishedLayerContext,
1211
+ publishedLayerPipelineResult,
1212
+ artifactBaseName,
1213
+ useDatestamp,
1214
+ }, config);
1058
1215
  };
1059
1216
  const saveBackupToFile = async ({ itemType, res, folder, filename }, config) => {
1060
1217
  await createAndSaveToFile({
@@ -1109,3 +1266,142 @@ export const migrateProvidedComponentsDataInStories = async ({ itemType, migrati
1109
1266
  languagePublishStatePath,
1110
1267
  }, config);
1111
1268
  };
1269
+ const CONTINUE_MANIFEST_RE = /---(?:story|preset)-continue-manifest(?:__.*)?\.json$/;
1270
+ const readMigrationJson = async (config, filename) => {
1271
+ const content = await readFile(`${config.sbmigWorkingDirectory}/migrations/${filename}`);
1272
+ if (content === undefined) {
1273
+ throw new Error(`Could not read migration artifact: migrations/${filename}`);
1274
+ }
1275
+ try {
1276
+ return JSON.parse(content);
1277
+ }
1278
+ catch {
1279
+ throw new Error(`Migration artifact is not valid JSON: migrations/${filename}`);
1280
+ }
1281
+ };
1282
+ /**
1283
+ * Discover the dry-run `continue` manifest in the migrations folder, load the
1284
+ * artifacts it points at, and reconstruct the exact inputs `finalizeMigration`
1285
+ * needs. Returns a summary (for the confirmation prompt) and a `run()` thunk that
1286
+ * performs the write. Does NOT pull stories, transform, or fetch published layers.
1287
+ */
1288
+ export const prepareContinueMigration = async ({ manifestFileName }, config) => {
1289
+ const files = await listFilesInDir(`${config.sbmigWorkingDirectory}/migrations`);
1290
+ const manifestFiles = files.filter((file) => CONTINUE_MANIFEST_RE.test(file));
1291
+ let chosen;
1292
+ if (manifestFileName) {
1293
+ if (!files.includes(manifestFileName)) {
1294
+ throw new Error(`Manifest not found in migrations folder: ${manifestFileName}`);
1295
+ }
1296
+ chosen = manifestFileName;
1297
+ }
1298
+ else if (manifestFiles.length === 1) {
1299
+ chosen = manifestFiles[0];
1300
+ }
1301
+ else if (manifestFiles.length === 0) {
1302
+ throw new Error("No dry-run found to continue. Run `sb-mig migrate content … --dryRun` first.");
1303
+ }
1304
+ else {
1305
+ throw new Error(`Multiple dry-run manifests found. Re-run with --manifest <filename>:\n${manifestFiles
1306
+ .map((file) => ` - ${file}`)
1307
+ .join("\n")}`);
1308
+ }
1309
+ const manifest = (await readMigrationJson(config, chosen));
1310
+ if (manifest.kind !== "migrate-content-continue-manifest") {
1311
+ throw new Error(`Not a continue manifest: ${chosen}`);
1312
+ }
1313
+ if (manifest.manifestVersion !== 1) {
1314
+ throw new Error(`Unsupported continue manifest version (${manifest.manifestVersion}) in ${chosen}.`);
1315
+ }
1316
+ const { artifacts } = manifest;
1317
+ if (!artifacts?.changedItems) {
1318
+ throw new Error(`Manifest ${chosen} is missing the changed-items artifact reference.`);
1319
+ }
1320
+ const changedItems = (await readMigrationJson(config, artifacts.changedItems));
1321
+ const pipelineSummary = artifacts.pipelineSummary
1322
+ ? await readMigrationJson(config, artifacts.pipelineSummary)
1323
+ : null;
1324
+ const languagePublishStateMap = artifacts.languageStateMap
1325
+ ? (await readMigrationJson(config, artifacts.languageStateMap))
1326
+ : undefined;
1327
+ const isPreserveLayers = shouldUsePublishedLayerMode(manifest.publicationMode) &&
1328
+ Boolean(artifacts.dirtyPublishedRecords);
1329
+ let publishedLayerContext;
1330
+ let publishedLayerPipelineResult = null;
1331
+ if (isPreserveLayers && artifacts.dirtyPublishedRecords) {
1332
+ const dirty = (await readMigrationJson(config, artifacts.dirtyPublishedRecords));
1333
+ publishedLayerContext = {
1334
+ records: [],
1335
+ publishedLayerInputItems: [],
1336
+ dirtyPublishedRecords: dirty.dirtyPublishedRecords ?? [],
1337
+ missingPublishedLayerRecords: [],
1338
+ };
1339
+ const publishedFinalItems = artifacts.publishedAfterFull
1340
+ ? (await readMigrationJson(config, artifacts.publishedAfterFull))
1341
+ : [];
1342
+ const publishedChangedIdSet = new Set((dirty.publishedChangedIds ?? []).map(String));
1343
+ publishedLayerPipelineResult = {
1344
+ changedItems: publishedFinalItems.filter((item) => publishedChangedIdSet.has(storyIdOf(item))),
1345
+ finalItems: publishedFinalItems,
1346
+ stepReports: [],
1347
+ totalItems: publishedFinalItems.length,
1348
+ };
1349
+ }
1350
+ const draftFinalItems = artifacts.draftAfterFull
1351
+ ? (await readMigrationJson(config, artifacts.draftAfterFull))
1352
+ : changedItems;
1353
+ const pipelineResult = {
1354
+ changedItems,
1355
+ finalItems: draftFinalItems,
1356
+ stepReports: Array.isArray(pipelineSummary?.steps)
1357
+ ? pipelineSummary.steps
1358
+ : [],
1359
+ totalItems: typeof pipelineSummary?.totalItems === "number"
1360
+ ? pipelineSummary.totalItems
1361
+ : changedItems.length,
1362
+ };
1363
+ const resolvedPublishLanguages = manifest.resolvedPublishLanguages ?? undefined;
1364
+ const effectivePublicationLanguages = manifest.publishLanguages?.requested;
1365
+ const artifactFiles = [
1366
+ artifacts.changedItems,
1367
+ artifacts.pipelineSummary,
1368
+ artifacts.draftAfterFull,
1369
+ artifacts.publishedAfterFull,
1370
+ artifacts.dirtyPublishedRecords,
1371
+ artifacts.languageStateMap,
1372
+ artifacts.inputFull,
1373
+ ].filter((file) => Boolean(file));
1374
+ const summary = {
1375
+ manifestFileName: chosen,
1376
+ from: manifest.from,
1377
+ to: manifest.to,
1378
+ itemType: manifest.itemType,
1379
+ publicationMode: manifest.publicationMode,
1380
+ migrationConfigNames: manifest.migrationConfigNames ?? [],
1381
+ resolvedPublishLanguages: resolvedPublishLanguages ?? [],
1382
+ changedCount: changedItems.length,
1383
+ dirtyPublishedCount: publishedLayerContext?.dirtyPublishedRecords.length ?? 0,
1384
+ artifactFiles,
1385
+ };
1386
+ const run = async () => {
1387
+ await finalizeMigration({
1388
+ itemType: manifest.itemType,
1389
+ from: manifest.from,
1390
+ to: manifest.to,
1391
+ migrateFrom: manifest.migrateFrom,
1392
+ fromFilePath: manifest.fromFilePath ?? undefined,
1393
+ publicationMode: manifest.publicationMode,
1394
+ effectivePublicationLanguages,
1395
+ resolvedPublishLanguages,
1396
+ languagePublishStateMap,
1397
+ appliedMigrationNames: manifest.migrationConfigNames ?? [],
1398
+ pipelineResult,
1399
+ publishedLayerContext,
1400
+ publishedLayerPipelineResult,
1401
+ artifactBaseName: manifest.artifactBaseName,
1402
+ useDatestamp: manifest.useDatestamp,
1403
+ continuedFromManifest: chosen,
1404
+ }, config);
1405
+ };
1406
+ return { summary, run };
1407
+ };
@@ -23,6 +23,7 @@ export interface MigrationRunLogRecord {
23
23
  resolved?: string[];
24
24
  };
25
25
  dryRun: boolean;
26
+ continuedFromManifest?: string;
26
27
  migrationConfigs: string[];
27
28
  totalItems: number;
28
29
  totalChangedItems: number;
@@ -76,8 +77,9 @@ interface SaveMigrationRunLogArgs {
76
77
  pipelineResult: MigrationPipelineResult;
77
78
  writeResults: PromiseSettledResult<MutationWriteResult>[];
78
79
  writeSummary: MutationWriteSummary;
80
+ continuedFromManifest?: string;
79
81
  }
80
- export declare const buildMigrationRunLogRecords: ({ from, to, itemType, dryRun, publish, publishLanguages, resolvedPublishLanguages, migrateFrom, fromFilePath, languagePublishStatePath, pipelineResult, writeResults, writeSummary, }: Omit<SaveMigrationRunLogArgs, "artifactBaseName" | "useDatestamp">) => MigrationRunLogRecord[];
82
+ export declare const buildMigrationRunLogRecords: ({ from, to, itemType, dryRun, publish, publishLanguages, resolvedPublishLanguages, migrateFrom, fromFilePath, languagePublishStatePath, pipelineResult, writeResults, writeSummary, continuedFromManifest, }: Omit<SaveMigrationRunLogArgs, "artifactBaseName" | "useDatestamp">) => MigrationRunLogRecord[];
81
83
  export declare const recordsToJsonl: (records: MigrationRunLogRecord[]) => string;
82
84
  export declare const saveMigrationRunLog: (args: SaveMigrationRunLogArgs, config: RequestBaseConfig) => Promise<void>;
83
85
  export {};
@@ -31,7 +31,7 @@ const resolveWriteResultValue = (result) => {
31
31
  error: result.reason,
32
32
  };
33
33
  };
34
- export const buildMigrationRunLogRecords = ({ from, to, itemType, dryRun, publish, publishLanguages, resolvedPublishLanguages, migrateFrom, fromFilePath, languagePublishStatePath, pipelineResult, writeResults, writeSummary, }) => {
34
+ export const buildMigrationRunLogRecords = ({ from, to, itemType, dryRun, publish, publishLanguages, resolvedPublishLanguages, migrateFrom, fromFilePath, languagePublishStatePath, pipelineResult, writeResults, writeSummary, continuedFromManifest, }) => {
35
35
  const timestamp = new Date().toISOString();
36
36
  const runId = `${itemType}-${timestamp}`;
37
37
  const baseRecord = {
@@ -57,6 +57,7 @@ export const buildMigrationRunLogRecords = ({ from, to, itemType, dryRun, publis
57
57
  }
58
58
  : {}),
59
59
  dryRun: Boolean(dryRun),
60
+ ...(continuedFromManifest ? { continuedFromManifest } : {}),
60
61
  migrationConfigs: pipelineResult.stepReports.map((step) => step.migrationConfig),
61
62
  totalItems: pipelineResult.totalItems,
62
63
  totalChangedItems: pipelineResult.changedItems.length,
@@ -5,7 +5,7 @@ export declare const publishedLayerExportDescription = "\n USAGE\n $ s
5
5
  export declare const languagePublishStateDescription = "\n USAGE\n $ sb-mig language-publish-state --from [spaceId]\n\n DESCRIPTION\n Read stories from a source Storyblok space and write a JSON map of default and translated language publication states.\n This command is read-only against Storyblok. It uses Management API for story listing and default-language state, and Delivery API for translated language published/draft comparisons.\n\n FLAGS\n --from Source space ID to inspect. Required.\n --accessToken Optional source space Delivery API access token override. Falls back to configured accessToken.\n --languages Languages to inspect: all, default,fr,de. Default: all.\n --withSlug Exact story full_slug to inspect. Can be repeated.\n --startsWith Filter stories by starts_with prefix.\n --fileName Stable output base name under sbmig/language-publish-state.\n --outputPath Explicit output path for the generated JSON file.\n\n SIDE EFFECTS\n Read-only against Storyblok. Writes a local JSON publish-state map.\n\n EXAMPLES\n $ sb-mig language-publish-state --from 12345 --startsWith about-ef --languages all --fileName about-ef-prod\n $ sb-mig language-publish-state --from 12345 --accessToken xxx --withSlug about-ef/testimonials --languages default,fr\n";
6
6
  export declare const syncDescription = "\n USAGE\n $ sb-mig sync components [component-name ...] | --all [--presets] [--ssot] [--dry-run]\n $ sb-mig sync roles [role-name ...] | --all [--dry-run]\n $ sb-mig sync datasources [datasource-name ...] | --all [--dry-run]\n $ sb-mig sync plugins [plugin-name ...] [--dry-run]\n $ sb-mig sync content (--all | --stories | --assets) --from [spaceId] --to [spaceId-or-file] --syncDirection [direction]\n\n DESCRIPTION\n Synchronize components, roles, datasources, plugins, stories, and assets with Storyblok.\n\n COMMANDS\n components Sync local component schema files to Storyblok.\n roles Sync local role schema files to Storyblok.\n datasources Sync local datasource schema files to Storyblok.\n plugins Sync a provided plugin. Run from a plugin folder with ./dist/export.js.\n content Sync stories and/or assets between spaces, local files, or AWS content hub data.\n\n FLAGS\n --all Sync all supported resources for the selected command.\n --stories Sync only stories. [content only]\n --assets Sync only assets. [content only]\n --presets Also sync component presets and set default presets. [components only]\n --ssot Single Source of Truth mode. Removes GUI-only components and replaces them with code versions. [components only]\n --packageName External package name used when resolving provided components. [components only]\n --dry-run Preview planned changes without making writes. [components, roles, datasources, plugins, content]\n --yes Skip confirmation prompts. [components --ssot, destructive content sync]\n --from Source space ID or local story file name, depending on --syncDirection. [content only]\n --to Target space ID or local output file name, depending on --syncDirection. [content only]\n --syncDirection Sync direction. Values: fromSpaceToFile, fromFileToSpace, fromSpaceToSpace, fromAWSToSpace. [content only]\n\n SIDE EFFECTS\n components, roles, datasources, and plugins write to Storyblok unless --dry-run is passed.\n content with fromSpaceToSpace or fromFileToSpace can delete all target stories before recreating them.\n content with fromSpaceToFile writes local story and asset backup files.\n\n GOTCHAS\n --syncDirection is required for content sync.\n fromFileToSpace is implemented for stories, but assets log that it is not implemented.\n fromAWSToSpace is implemented for stories. Assets log that unsupported directions are not implemented.\n --ssot is destructive for components and prompts unless --yes is passed.\n\n EXAMPLES\n $ sb-mig sync components --all\n $ sb-mig sync components --all --dry-run\n $ sb-mig sync components --all --presets\n $ sb-mig sync components --all --ssot --yes\n $ sb-mig sync components accordion accordion-item --presets\n\n $ sb-mig sync roles --all\n $ sb-mig sync roles admin editor --dry-run\n\n $ sb-mig sync datasources --all\n $ sb-mig sync datasources countries cities --dry-run\n\n $ sb-mig sync plugins my-awesome-plugin\n\n $ sb-mig sync content --all --from 12345 --to 67890 --syncDirection fromSpaceToSpace --yes\n $ sb-mig sync content --stories --from 12345 --to all-stories-backup --syncDirection fromSpaceToFile\n $ sb-mig sync content --stories --from all-stories-backup --to 67890 --syncDirection fromFileToSpace --dry-run\n $ sb-mig sync content --stories --from 12345 --to 67890 --syncDirection fromAWSToSpace\n";
7
7
  export declare const copyDescription = "\n USAGE\n $ sb-mig copy stories --from [spaceId] --to [spaceId] --source [full_slug] --destination [target_folder_full_slug]\n $ sb-mig copy stories --from [spaceId] --to [spaceId] --source [folder_full_slug]\n $ sb-mig copy stories --from [spaceId] --to [spaceId] --source [folder_full_slug]/* --destination [target_folder_full_slug]\n $ sb-mig copy stories --from [spaceId] --to [spaceId] --source [folder_full_slug] --mode self --destination /\n $ sb-mig copy assets --from [spaceId] --to [spaceId] --all\n $ sb-mig copy assets --from [spaceId] --to [spaceId] --asset [asset_id_or_filename]\n $ sb-mig copy assets --from [spaceId] --to [spaceId] --assetFolder [folder_id_or_path]\n $ sb-mig copy assets --from [spaceId] --to [spaceId] --referenced-by-stories --source [story_or_folder_full_slug]\n $ sb-mig copy assets --from [spaceId] --to [spaceId] --all --dry-run\n\n DESCRIPTION\n Copy Storyblok stories, folders, assets, or asset folders from one space to another.\n\n COMMANDS\n stories Copy one story, one folder subtree, a folder's children, or one folder shell.\n assets Copy or plan all assets and asset folders with durable manifests.\n\n FLAGS\n --from Source Storyblok space ID. Falls back to configured spaceId.\n --to Target Storyblok space ID. Falls back to configured spaceId.\n --source Source story or folder full_slug. Use folder/* to copy a folder's children without the folder root.\n --destination Target folder full_slug where copied stories are attached. Omit, '/', or 'root' to copy into target root.\n --mode Copy mode: subtree, children, or self. Default: subtree. folder/* defaults to children.\n --with-assets For copy stories, copy referenced assets first and rewrite copied stories to target asset IDs/filenames.\n --publicationMode\n How copy stories should preserve Storyblok publication state. Values: preserve-layers, collapse-draft, save-only. Default: preserve-layers. [stories only]\n --publicationLanguages\n Language scope to publish when publicationMode publishes stories. Values: default, all, or comma-separated Storyblok language codes. Default: all. [stories only]\n --all Select all assets and asset folders. [assets only]\n --asset Select one source asset by numeric ID, exact Storyblok asset URL, or unique file name. Repeatable. [assets only]\n --assetFolder Select one source asset folder by numeric ID or folder path. Includes descendants and assets in that subtree. Repeatable. [assets only]\n --referenced-by-stories\n Select assets referenced by a story/folder scope. Requires --source. [assets only]\n --dry-run Preview story paths, manifest-mapped references, and likely target conflicts without writing to Storyblok.\n --outputPath Optional JSON file path for a dry-run copy plan artifact. Only writes locally when passed.\n\n LEGACY FLAGS\n --sourceSpace Alias for --from.\n --targetSpace Alias for --to.\n --what Alias for --source.\n --where Alias for --destination.\n\n SIDE EFFECTS\n copy stories writes copied stories into the target Storyblok space unless --dry-run is passed.\n copy stories writes story ID/UUID manifests under .sb-mig/copy/<source>/<target>/ during apply.\n copy stories --with-assets writes referenced asset folders/assets before story writes unless --dry-run is passed.\n copy assets writes asset folders and assets into the target Storyblok space unless --dry-run is passed.\n copy assets writes JSONL manifests under .sb-mig/copy/<source>/<target>/ during apply.\n --outputPath writes a local JSON report for dry-run or apply.\n\n GOTCHAS\n --source must resolve to an existing source story or folder.\n --destination must resolve to an existing target folder unless it is omitted, '/', or 'root'.\n mode 'subtree' copies a folder and all descendants. This is the default for folders.\n mode 'children' copies a folder's descendants without the folder root.\n mode 'self' copies only the source story or folder shell.\n copy stories creates or matches target story shells, then fills them with rewritten source content.\n copy stories matches existing targets by manifest first, then target full_slug when safe, so reruns can reuse mapped target stories.\n copy stories rewrites mapped asset and story references after story manifests exist.\n copy stories creates shells as save-only drafts, then applies publicationMode after full rewritten content is saved.\n publicationMode preserve-layers publishes clean published source stories; for dirty published source stories it copies the source published version, publishes it in target, then restores the source draft/current layer as save-only.\n publicationMode collapse-draft publishes published source stories from their current draft/current JSON.\n publicationMode save-only never publishes copied stories.\n --publicationLanguages cannot be used with --publicationMode save-only.\n copy stories --with-assets scans selected stories with source component schemas and only copies referenced assets it can resolve from the source asset list.\n --dry-run checks likely target path conflicts and reports mapped/planned/unresolved story and asset references, with occurrence and unique-asset counts in JSON output.\n Without --with-assets, run copy assets first when copied stories should point to target-space assets.\n copy assets supports --all, --asset, or --assetFolder. Use only one selector family per run.\n copy assets --asset includes the selected asset's folder ancestors so target folder mappings can be preserved.\n copy assets --assetFolder includes the selected folder, descendants, folder ancestors, and assets inside that selected subtree.\n copy assets --referenced-by-stories scans selected stories with source component schemas and copies only source-space assets referenced by that story scope.\n copy assets matches by manifest first, then safe target folder path or unique asset file name before creating.\n copy assets uploads assets, finalizes the upload, and writes source-to-target asset/folder manifests.\n\n EXAMPLES\n $ sb-mig copy stories --from 12345 --to 67890 --source blog/post-1 --destination imported\n $ sb-mig copy stories --from 12345 --to 67890 --source blog --destination imported\n $ sb-mig copy stories --from 12345 --to 67890 --source blog/* --destination imported\n $ sb-mig copy stories --from 12345 --to 67890 --source blog --mode self --destination /\n $ sb-mig copy stories --from 12345 --to 67890 --source blog --destination imported --with-assets\n $ sb-mig copy stories --from 12345 --to 67890 --source blog --destination imported --with-assets --publicationMode preserve-layers\n $ sb-mig copy stories --from 12345 --to 67890 --source blog --destination imported --publicationMode collapse-draft --publicationLanguages default,fr,de\n $ sb-mig copy stories --from 12345 --to 67890 --source blog --destination imported --publicationMode save-only\n $ sb-mig copy stories --from 12345 --to 67890 --source blog --destination imported --dry-run\n $ sb-mig copy stories --from 12345 --to 67890 --source blog --destination imported --with-assets --dry-run --outputPath sbmig/copy-plans/blog-copy.json\n $ sb-mig copy stories --from 12345 --to 67890 --source blog --destination imported --dry-run --outputPath sbmig/copy-plans/blog-copy.json\n $ sb-mig copy assets --from 12345 --to 67890 --all\n $ sb-mig copy assets --from 12345 --to 67890 --asset 987654\n $ sb-mig copy assets --from 12345 --to 67890 --asset hero.jpg --dry-run\n $ sb-mig copy assets --from 12345 --to 67890 --assetFolder Marketing/Heroes --dry-run\n $ sb-mig copy assets --from 12345 --to 67890 --referenced-by-stories --source blog --dry-run --outputPath sbmig/copy-plans/blog-assets.json\n $ sb-mig copy assets --from 12345 --to 67890 --all --dry-run\n $ sb-mig copy assets --from 12345 --to 67890 --all --dry-run --outputPath sbmig/copy-plans/assets-copy.json\n";
8
- export declare const migrateDescription = "\n USAGE\n $ sb-mig migrate content [component-name ...] --from [spaceId] --to [spaceId] --migration [migration-config]\n $ sb-mig migrate content --all --from [spaceId] --to [spaceId] --migration [migration-config]\n $ sb-mig migrate presets --all --from [spaceId-or-file] --to [spaceId] --migration [migration-config]\n\n DESCRIPTION\n Migrate story content or presets using local migration config files.\n This is a potentially destructive command. It prompts for confirmation unless --yes or --dry-run is passed.\n\n COMMANDS\n content Migrate story content for all components or provided component names.\n presets Migrate presets. Supports --all and exactly one --migration value.\n\n FLAGS\n --from Source space ID, or local file name when --migrate-from file is used.\n --fromFilePath Direct path to stories or presets JSON when using --migrate-from file.\n --to Target Storyblok space ID.\n --migrate-from Migrate from space or file. Default: space.\n --migration Migration file name without extension. Can be repeated for ordered content pipelines. Presets support exactly one.\n --migrationComponentAlias\n Add extra component aliases for a migration. Repeatable. Format: <migration>:<source>=<alias1>,<alias2>.\n --migrationComponents\n Override the exact component scope for a migration. Repeatable. Format: <migration>:<component1>,<component2>.\n --withSlug Filter stories by full_slug. Can be repeated. [content only]\n --startsWith Filter stories by starts_with prefix. [content only]\n --yes Skip confirmation prompts.\n --dry-run Preview what would be migrated without making API changes.\n --publicationMode How migrate content should preserve Storyblok publication state. Values: preserve-layers, collapse-draft, save-only. Default: preserve-layers. [content only]\n --publicationLanguages\n Language scope to inspect and preserve when publicationMode publishes stories. Values: default, all, or comma-separated Storyblok language codes. Default: all. [content only]\n --languagePublishStatePath\n Optional JSON file generated by language-publish-state. When omitted, migrate builds the map automatically for selected stories. [content only]\n --fileName Stable base name for migration output files.\n\n SIDE EFFECTS\n content writes migrated stories to Storyblok unless --dry-run is passed.\n content creates a story backup before provided-component migrations unless --dry-run is passed.\n presets backs up all remote presets before writing migrated presets unless --dry-run is passed.\n\n GOTCHAS\n At least one --migration value is required.\n preserve-layers currently requires --migrate-from space and requires --from and --to to be the same Storyblok space.\n --publicationLanguages cannot be used with --publicationMode save-only.\n --languagePublishStatePath cannot be used with --publicationMode save-only.\n --publicationMode, --publicationLanguages, and --languagePublishStatePath are only supported for migrate content, not migrate presets.\n Legacy flags --publish, --publishLanguages, and --preservePublishedLayer are rejected. Use --publicationMode and --publicationLanguages instead.\n\n EXAMPLES\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration file-with-migration --dry-run\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration migration-a --migration migration-b --migration migration-c --yes\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 --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 file-with-migration --publicationMode preserve-layers --yes\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration file-with-migration --publicationMode collapse-draft --publicationLanguages default,fr,de --yes\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.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 presets --all --from 12345 --to 12345 --migration preset-migration --dry-run\n $ sb-mig migrate presets --all --migrate-from file --fromFilePath sbmig/presets/presets-backup.json --to 12345 --migration preset-migration\n";
8
+ export declare const migrateDescription = "\n USAGE\n $ sb-mig migrate content [component-name ...] --from [spaceId] --to [spaceId] --migration [migration-config]\n $ sb-mig migrate content --all --from [spaceId] --to [spaceId] --migration [migration-config]\n $ sb-mig migrate presets --all --from [spaceId-or-file] --to [spaceId] --migration [migration-config]\n\n DESCRIPTION\n Migrate story content or presets using local migration config files.\n This is a potentially destructive command. It prompts for confirmation unless --yes or --dry-run is passed.\n\n COMMANDS\n content Migrate story content for all components or provided component names.\n presets Migrate presets. Supports --all and exactly one --migration value.\n continue Finish a previous --dry-run by writing its already-computed result to\n Storyblok. Skips pulling stories and re-running the migration, so it is\n much faster. Requires a content freeze between the dry-run and continue.\n\n FLAGS\n --from Source space ID, or local file name when --migrate-from file is used.\n --manifest [continue only] Continue manifest filename inside the migrations folder.\n Only needed when more than one dry-run manifest is present.\n --fromFilePath Direct path to stories or presets JSON when using --migrate-from file.\n --to Target Storyblok space ID.\n --migrate-from Migrate from space or file. Default: space.\n --migration Migration file name without extension. Can be repeated for ordered content pipelines. Presets support exactly one.\n --migrationComponentAlias\n Add extra component aliases for a migration. Repeatable. Format: <migration>:<source>=<alias1>,<alias2>.\n --migrationComponents\n Override the exact component scope for a migration. Repeatable. Format: <migration>:<component1>,<component2>.\n --withSlug Filter stories by full_slug. Can be repeated. [content only]\n --startsWith Filter stories by starts_with prefix. [content only]\n --yes Skip confirmation prompts.\n --dry-run Preview what would be migrated without making API changes.\n --publicationMode How migrate content should preserve Storyblok publication state. Values: preserve-layers, collapse-draft, save-only. Default: preserve-layers. [content only]\n --publicationLanguages\n Language scope to inspect and preserve when publicationMode publishes stories. Values: default, all, or comma-separated Storyblok language codes. Default: all. [content only]\n --languagePublishStatePath\n Optional JSON file generated by language-publish-state. When omitted, migrate builds the map automatically for selected stories. [content only]\n --fileName Stable base name for migration output files.\n\n SIDE EFFECTS\n content writes migrated stories to Storyblok unless --dry-run is passed.\n content creates a story backup before provided-component migrations unless --dry-run is passed.\n content --dry-run also writes a continue manifest so the run can later be finished with migrate continue.\n continue writes the dry-run's migrated stories to Storyblok and updates applied-backpack-migrations.json. It does not pull or re-transform stories.\n presets backs up all remote presets before writing migrated presets unless --dry-run is passed.\n\n GOTCHAS\n At least one --migration value is required.\n preserve-layers currently requires --migrate-from space and requires --from and --to to be the same Storyblok space.\n --publicationLanguages cannot be used with --publicationMode save-only.\n --languagePublishStatePath cannot be used with --publicationMode save-only.\n --publicationMode, --publicationLanguages, and --languagePublishStatePath are only supported for migrate content, not migrate presets.\n Legacy flags --publish, --publishLanguages, and --preservePublishedLayer are rejected. Use --publicationMode and --publicationLanguages instead.\n\n EXAMPLES\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration file-with-migration --dry-run\n $ sb-mig migrate continue\n $ sb-mig migrate continue --manifest dry-run--12345--2026-6-2_13-5---story-continue-manifest.json --yes\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration migration-a --migration migration-b --migration migration-c --yes\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 --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 file-with-migration --publicationMode preserve-layers --yes\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration file-with-migration --publicationMode collapse-draft --publicationLanguages default,fr,de --yes\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.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 presets --all --from 12345 --to 12345 --migration preset-migration --dry-run\n $ sb-mig migrate presets --all --migrate-from file --fromFilePath sbmig/presets/presets-backup.json --to 12345 --migration preset-migration\n";
9
9
  export declare const revertDescription = "\n USAGE\n $ sb-mig revert content --from [stories-file-name] --to [spaceId] [--yes]\n\n DESCRIPTION\n Restore stories from a local story backup file into a Storyblok space.\n\n COMMANDS\n content Revert content by updating target stories from a local story backup.\n\n FLAGS\n --from Local story backup file name to discover and load.\n --to Target Storyblok space ID to update.\n --yes Skip confirmation prompt.\n\n SIDE EFFECTS\n Writes stories to the target Storyblok space.\n Creates a target-space story backup before restoring.\n Restored stories are updated with publish: false.\n\n GOTCHAS\n This command restores local story files directly and does not run migration configs.\n The --from value is resolved as a local story file name, not a source space ID.\n\n EXAMPLES\n $ sb-mig revert content --from 12345--backup-before-migration --to 12345\n $ sb-mig revert content --from prod-stories-backup --to 12345 --yes\n";
10
10
  export declare const discoverDescription = "\n USAGE\n $ sb-mig discover components --all [--write] [--file file-name]\n $ sb-mig discover migrations --all\n\n DESCRIPTION\n Discover local and external component schema files or migration config files.\n\n COMMANDS\n components Discover component schema files.\n migrations Discover migration config files.\n\n FLAGS\n --all Discover all components or migration config files.\n --write Write discovered component names to a local file. [components only]\n --file Output file name when --write is passed. [components only]\n\n SIDE EFFECTS\n Read-only unless --write is passed.\n --write creates or overwrites a local component list file.\n\n EXAMPLES\n $ sb-mig discover components --all\n $ sb-mig discover components --all --write\n $ sb-mig discover components --all --write --file all-components\n $ sb-mig discover migrations --all\n";
11
11
  export declare const migrationsDescription = "\n USAGE\n $ sb-mig migrations recognize --from [version] [--to version]\n\n DESCRIPTION\n Recognize migration commands to run for a package upgrade by comparing an old version with a target version.\n\n COMMANDS\n recognize Print recommended story and preset migration commands.\n\n FLAGS\n --from Previous package version. Required.\n --to Target package version. Optional. Falls back to the installed @ef-global/backpack dependency version.\n\n SIDE EFFECTS\n Reads local applied-backpack-migrations.json when present.\n Reads local package.json when --to is omitted.\n Does not write files or call Storyblok.\n\n EXAMPLES\n $ sb-mig migrations recognize --from 3.4.0\n $ sb-mig migrations recognize --from 3.4.0 --to 4.0.0\n";
@@ -315,9 +315,14 @@ export const migrateDescription = `
315
315
  COMMANDS
316
316
  content Migrate story content for all components or provided component names.
317
317
  presets Migrate presets. Supports --all and exactly one --migration value.
318
+ continue Finish a previous --dry-run by writing its already-computed result to
319
+ Storyblok. Skips pulling stories and re-running the migration, so it is
320
+ much faster. Requires a content freeze between the dry-run and continue.
318
321
 
319
322
  FLAGS
320
323
  --from Source space ID, or local file name when --migrate-from file is used.
324
+ --manifest [continue only] Continue manifest filename inside the migrations folder.
325
+ Only needed when more than one dry-run manifest is present.
321
326
  --fromFilePath Direct path to stories or presets JSON when using --migrate-from file.
322
327
  --to Target Storyblok space ID.
323
328
  --migrate-from Migrate from space or file. Default: space.
@@ -340,6 +345,8 @@ export const migrateDescription = `
340
345
  SIDE EFFECTS
341
346
  content writes migrated stories to Storyblok unless --dry-run is passed.
342
347
  content creates a story backup before provided-component migrations unless --dry-run is passed.
348
+ content --dry-run also writes a continue manifest so the run can later be finished with migrate continue.
349
+ continue writes the dry-run's migrated stories to Storyblok and updates applied-backpack-migrations.json. It does not pull or re-transform stories.
343
350
  presets backs up all remote presets before writing migrated presets unless --dry-run is passed.
344
351
 
345
352
  GOTCHAS
@@ -352,6 +359,8 @@ export const migrateDescription = `
352
359
 
353
360
  EXAMPLES
354
361
  $ sb-mig migrate content --all --from 12345 --to 12345 --migration file-with-migration --dry-run
362
+ $ sb-mig migrate continue
363
+ $ sb-mig migrate continue --manifest dry-run--12345--2026-6-2_13-5---story-continue-manifest.json --yes
355
364
  $ sb-mig migrate content --all --from 12345 --to 12345 --migration migration-a --migration migration-b --migration migration-c --yes
356
365
  $ sb-mig migrate content --all --from 12345 --to 12345 --migration colorPickerModeValues --migrationComponentAlias colorPickerModeValues:sb-button=sb-open-drift-button
357
366
  $ sb-mig migrate content --all --from 12345 --to 12345 --migration colorPickerModeValues --migrationComponents colorPickerModeValues:sb-section,sb-tour-page-section
@@ -1,5 +1,5 @@
1
1
  import path from "path";
2
- import { migrateAllComponentsDataInStories, migrateProvidedComponentsDataInStories, } from "../../api/data-migration/component-data-migration.js";
2
+ import { migrateAllComponentsDataInStories, migrateProvidedComponentsDataInStories, prepareContinueMigration, } from "../../api/data-migration/component-data-migration.js";
3
3
  import { buildStoryBackupBaseName } from "../../api/data-migration/file-naming.js";
4
4
  import { parseMigrationComponentAliasFlags, parseMigrationComponentOverrideFlags, } from "../../api/data-migration/migration-component-scope.js";
5
5
  import { managementApi } from "../../api/managementApi.js";
@@ -13,6 +13,7 @@ import { isItFactory, unpackElements, getFrom, getTo, } from "../utils/cli-utils
13
13
  const MIGRATE_COMMANDS = {
14
14
  content: "content",
15
15
  presets: "presets",
16
+ continue: "continue",
16
17
  };
17
18
  const normalizeMigrationFlags = (migrationFlag) => {
18
19
  if (Array.isArray(migrationFlag)) {
@@ -280,6 +281,31 @@ export const migrate = async (props) => {
280
281
  }
281
282
  break;
282
283
  }
284
+ case MIGRATE_COMMANDS.continue: {
285
+ const manifestFileName = flags["manifest"];
286
+ const plan = await prepareContinueMigration({ manifestFileName }, apiConfig);
287
+ const { summary } = plan;
288
+ Logger.log("Found dry-run to continue:");
289
+ console.log(` manifest: ${summary.manifestFileName}`);
290
+ console.log(` space (from→to): ${summary.from} → ${summary.to}`);
291
+ console.log(` publication mode: ${summary.publicationMode}`);
292
+ console.log(` migrations: ${summary.migrationConfigNames.join(", ") || "(none recorded)"}`);
293
+ if (summary.resolvedPublishLanguages.length > 0) {
294
+ console.log(` languages: ${summary.resolvedPublishLanguages.join(", ")}`);
295
+ }
296
+ console.log(` ${summary.itemType}s to write: ${summary.changedCount} changed${summary.dirtyPublishedCount > 0
297
+ ? ` (${summary.dirtyPublishedCount} dirty-published dual-layer write(s))`
298
+ : ""}`);
299
+ console.log(" using artifacts:");
300
+ summary.artifactFiles.forEach((file) => console.log(` - ${file}`));
301
+ console.log(" ");
302
+ await askForConfirmation(`Continue and write these to Storyblok space ${summary.to}? (it will overwrite ${summary.itemType}s)`, async () => {
303
+ await plan.run();
304
+ }, () => {
305
+ Logger.warning("Continue not started, exiting the program...");
306
+ }, flags["yes"]);
307
+ break;
308
+ }
283
309
  default:
284
310
  console.log(`no command like that: ${command}`);
285
311
  }
package/dist/cli/index.js CHANGED
@@ -83,6 +83,9 @@ app.migrate = () => ({
83
83
  fileName: {
84
84
  type: "string",
85
85
  },
86
+ manifest: {
87
+ type: "string",
88
+ },
86
89
  },
87
90
  }),
88
91
  action: async (cli) => {
@@ -60,6 +60,7 @@ type CreateAndSaveToFile = (args: {
60
60
  }, config: RequestBaseConfig) => void;
61
61
  export declare const createAndSaveToFile: CreateAndSaveToFile;
62
62
  export declare const createAndSaveComponentListToFile: CreateAndSaveComponentListToFile;
63
+ export declare const listFilesInDir: (dirPath: string) => Promise<string[]>;
63
64
  export declare const readFile: (pathToFile: string) => Promise<string | undefined>;
64
65
  export declare const dumpToFile: (path: string, content: string) => Promise<void>;
65
66
  export declare const getConsumerPackageJson: () => Promise<any>;
@@ -253,6 +253,15 @@ export const createAndSaveComponentListToFile = async ({ file, folder, res, time
253
253
  : `${sbmigWorkingDirectory}/${filename}.ts`, timestamp);
254
254
  Logger.success(`All components written to a file: ${filename}`);
255
255
  };
256
+ export const listFilesInDir = async (dirPath) => {
257
+ const absolutePath = resolveFromCwd(dirPath);
258
+ try {
259
+ return await fs.promises.readdir(absolutePath);
260
+ }
261
+ catch {
262
+ return [];
263
+ }
264
+ };
256
265
  export const readFile = async (pathToFile) => {
257
266
  const absolutePath = resolveFromCwd(pathToFile);
258
267
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sb-mig",
3
- "version": "6.3.0-beta.1",
3
+ "version": "6.3.0-beta.2",
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",