sb-mig 6.2.0 → 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.
Files changed (44) hide show
  1. package/README.md +15 -4
  2. package/dist/api/assets/asset-folders.d.ts +3 -0
  3. package/dist/api/assets/asset-folders.js +56 -0
  4. package/dist/api/assets/asset-folders.types.d.ts +29 -0
  5. package/dist/api/assets/asset-folders.types.js +1 -0
  6. package/dist/api/assets/assets.d.ts +4 -1
  7. package/dist/api/assets/assets.js +61 -16
  8. package/dist/api/assets/assets.types.d.ts +12 -1
  9. package/dist/api/assets/index.d.ts +4 -2
  10. package/dist/api/assets/index.js +2 -1
  11. package/dist/api/copy/assets.d.ts +12 -0
  12. package/dist/api/copy/assets.js +84 -0
  13. package/dist/api/copy/graph.d.ts +8 -0
  14. package/dist/api/copy/graph.js +27 -0
  15. package/dist/api/copy/index.d.ts +6 -0
  16. package/dist/api/copy/index.js +6 -0
  17. package/dist/api/copy/manifest.d.ts +23 -0
  18. package/dist/api/copy/manifest.js +115 -0
  19. package/dist/api/copy/reference-rewriter.d.ts +6 -0
  20. package/dist/api/copy/reference-rewriter.js +153 -0
  21. package/dist/api/copy/reference-scanner.d.ts +23 -0
  22. package/dist/api/copy/reference-scanner.js +322 -0
  23. package/dist/api/copy/types.d.ts +196 -0
  24. package/dist/api/copy/types.js +1 -0
  25. package/dist/api/data-migration/component-data-migration.d.ts +81 -0
  26. package/dist/api/data-migration/component-data-migration.js +391 -95
  27. package/dist/api/data-migration/migration-run-log.d.ts +3 -1
  28. package/dist/api/data-migration/migration-run-log.js +2 -1
  29. package/dist/api/managementApi.d.ts +6 -0
  30. package/dist/api/stories/stories.js +3 -3
  31. package/dist/api/stories/stories.types.d.ts +3 -1
  32. package/dist/api/testApi.d.ts +6 -0
  33. package/dist/api-v2/requestConfig.d.ts +17 -1
  34. package/dist/api-v2/requestConfig.js +28 -0
  35. package/dist/cli/cli-descriptions.d.ts +3 -3
  36. package/dist/cli/cli-descriptions.js +92 -18
  37. package/dist/cli/commands/copy.js +2091 -145
  38. package/dist/cli/commands/migrate.js +27 -1
  39. package/dist/cli/index.js +3 -0
  40. package/dist/utils/files.d.ts +2 -0
  41. package/dist/utils/files.js +86 -3
  42. package/dist-cjs/api/stories/stories.js +3 -3
  43. package/dist-cjs/api-v2/requestConfig.js +29 -0
  44. package/package.json +2 -2
@@ -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,
@@ -1,3 +1,4 @@
1
+ import * as assets from "./assets/index.js";
1
2
  import * as inspect from "./inspect/index.js";
2
3
  import * as stories from "./stories/index.js";
3
4
  export declare const managementApi: {
@@ -8,7 +9,12 @@ export declare const managementApi: {
8
9
  getAsset: (assetName: string | undefined) => Promise<void>;
9
10
  getAssetById: import("./assets/assets.types.js").GetAssetById;
10
11
  getAssetByName: import("./assets/assets.types.js").GetAssetByName;
12
+ createAssetAndFinalize: assets.CreateAssetAndFinalize;
13
+ downloadAsset: import("./assets/assets.types.js").DownloadAsset;
14
+ finishAssetUpload: assets.FinishAssetUpload;
11
15
  updateAsset: import("./assets/assets.types.js").UpdateAsset;
16
+ createAssetFolder: assets.CreateAssetFolder;
17
+ getAllAssetFolders: import("./assets/asset-folders.types.js").GetAllAssetFolders;
12
18
  };
13
19
  auth: {
14
20
  getCurrentUser: import("./auth/auth.types.js").GetCurrentUser;