sb-mig 6.0.0-beta.3 → 6.0.0-beta.5

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,3 +1,4 @@
1
+ import type { PublishLanguagesOption } from "../stories/stories.types.js";
1
2
  import type { RequestBaseConfig } from "../utils/request.js";
2
3
  import { type MigrationComponentAliasesByMigration, type MigrationComponentOverridesByMigration } from "./migration-component-scope.js";
3
4
  import { type PreparedMigrationValidator } from "./migration-validation.js";
@@ -44,6 +45,7 @@ interface MigrateItems {
44
45
  };
45
46
  dryRun?: boolean;
46
47
  publish?: boolean;
48
+ publishLanguages?: PublishLanguagesOption;
47
49
  fromFilePath?: string;
48
50
  fileName?: string;
49
51
  preparedMigrationConfigs?: PreparedMigrationConfig[];
@@ -68,8 +70,8 @@ export declare const runMigrationPipelineInMemory: ({ itemType, itemsToMigrate,
68
70
  itemsToMigrate: any[];
69
71
  preparedMigrationConfigs: PreparedMigrationConfig[];
70
72
  }) => MigrationPipelineResult;
71
- export declare const migrateAllComponentsDataInStories: ({ itemType, migrationConfig, migrateFrom, from, to, filters, dryRun, publish, fromFilePath, fileName, migrationComponentAliases, migrationComponentOverrides, }: Omit<MigrateItems, "componentsToMigrate" | "preparedMigrationConfigs">, config: RequestBaseConfig) => Promise<void>;
72
- export declare const doTheMigration: ({ itemType, from, itemsToMigrate, migrationConfig, migrationConfigs, to, dryRun, publish, migrateFrom, fromFilePath, fileName, }: {
73
+ export declare const migrateAllComponentsDataInStories: ({ itemType, migrationConfig, migrateFrom, from, to, filters, dryRun, publish, publishLanguages, fromFilePath, fileName, migrationComponentAliases, migrationComponentOverrides, }: Omit<MigrateItems, "componentsToMigrate" | "preparedMigrationConfigs">, config: RequestBaseConfig) => Promise<void>;
74
+ export declare const doTheMigration: ({ itemType, from, itemsToMigrate, migrationConfig, migrationConfigs, to, dryRun, publish, publishLanguages, migrateFrom, fromFilePath, fileName, }: {
73
75
  itemType?: "story" | "preset";
74
76
  from: string;
75
77
  itemsToMigrate: any[];
@@ -78,9 +80,10 @@ export declare const doTheMigration: ({ itemType, from, itemsToMigrate, migratio
78
80
  to: string;
79
81
  dryRun?: boolean;
80
82
  publish?: boolean;
83
+ publishLanguages?: PublishLanguagesOption;
81
84
  migrateFrom: MigrateFrom;
82
85
  fromFilePath?: string;
83
86
  fileName?: string;
84
87
  }, config: RequestBaseConfig) => Promise<void>;
85
- export declare const migrateProvidedComponentsDataInStories: ({ itemType, migrationConfig, migrateFrom, from, to, componentsToMigrate, filters, dryRun, publish, fromFilePath, fileName, preparedMigrationConfigs, migrationComponentAliases, migrationComponentOverrides, }: MigrateItems, config: RequestBaseConfig) => Promise<void>;
88
+ export declare const migrateProvidedComponentsDataInStories: ({ itemType, migrationConfig, migrateFrom, from, to, componentsToMigrate, filters, dryRun, publish, publishLanguages, fromFilePath, fileName, preparedMigrationConfigs, migrationComponentAliases, migrationComponentOverrides, }: MigrateItems, config: RequestBaseConfig) => Promise<void>;
86
89
  export {};
@@ -9,6 +9,7 @@ import { isObjectEmpty } from "../../utils/object-utils.js";
9
9
  import { managementApi } from "../managementApi.js";
10
10
  import { buildPreMigrationBackupBaseName, resolveOutputFileBaseName, shouldUseDatestampForArtifacts, } from "./file-naming.js";
11
11
  import { extendMigrationMapperWithAliases, resolveMigrationComponentsToMigrate, } from "./migration-component-scope.js";
12
+ import { saveMigrationRunLog } from "./migration-run-log.js";
12
13
  import { discoverMigrationValidatorForMigrationFile, MigrationValidationFailedError, runPreparedMigrationValidator, } from "./migration-validation.js";
13
14
  import { summarizeMutationWriteResults } from "./write-summary.js";
14
15
  export const normalizeMigrationConfigNames = (migrationConfig) => {
@@ -295,7 +296,7 @@ export const runMigrationPipelineInMemory = ({ itemType, itemsToMigrate, prepare
295
296
  totalItems: workingItems.length,
296
297
  };
297
298
  };
298
- const savePipelineSummary = async ({ artifactBaseName, useDatestamp, from, itemType, dryRun, publish, migrateFrom, fromFilePath, pipelineResult, }, config) => {
299
+ const savePipelineSummary = async ({ artifactBaseName, useDatestamp, from, itemType, dryRun, publish, publishLanguages, migrateFrom, fromFilePath, pipelineResult, }, config) => {
299
300
  await createAndSaveToFile({
300
301
  datestamp: useDatestamp,
301
302
  ext: "json",
@@ -309,6 +310,11 @@ const savePipelineSummary = async ({ artifactBaseName, useDatestamp, from, itemT
309
310
  fromFilePath: fromFilePath || null,
310
311
  },
311
312
  writeMode: itemType === "story" && publish ? "publish" : "save",
313
+ publishLanguages: itemType === "story" && publish
314
+ ? {
315
+ requested: publishLanguages,
316
+ }
317
+ : null,
312
318
  totalItems: pipelineResult.totalItems,
313
319
  totalChangedItems: pipelineResult.changedItems.length,
314
320
  steps: pipelineResult.stepReports,
@@ -379,7 +385,7 @@ const loadItemsToMigrate = async ({ itemType, migrateFrom, from, filters, fromFi
379
385
  spaceId: from,
380
386
  });
381
387
  };
382
- export const migrateAllComponentsDataInStories = async ({ itemType, migrationConfig, migrateFrom, from, to, filters, dryRun, publish, fromFilePath, fileName, migrationComponentAliases, migrationComponentOverrides, }, config) => {
388
+ export const migrateAllComponentsDataInStories = async ({ itemType, migrationConfig, migrateFrom, from, to, filters, dryRun, publish, publishLanguages, fromFilePath, fileName, migrationComponentAliases, migrationComponentOverrides, }, config) => {
383
389
  Logger.warning(`Trying to migrate all ${itemType} from ${migrateFrom}, ${from} to ${to}...`);
384
390
  const preparedMigrationConfigs = prepareMigrationConfigs({
385
391
  migrationConfig,
@@ -399,12 +405,13 @@ export const migrateAllComponentsDataInStories = async ({ itemType, migrationCon
399
405
  filters,
400
406
  dryRun,
401
407
  publish,
408
+ publishLanguages,
402
409
  fromFilePath,
403
410
  fileName,
404
411
  preparedMigrationConfigs,
405
412
  }, config);
406
413
  };
407
- export const doTheMigration = async ({ itemType = "story", from, itemsToMigrate, migrationConfig, migrationConfigs, to, dryRun, publish, migrateFrom, fromFilePath, fileName, }, config) => {
414
+ export const doTheMigration = async ({ itemType = "story", from, itemsToMigrate, migrationConfig, migrationConfigs, to, dryRun, publish, publishLanguages, migrateFrom, fromFilePath, fileName, }, config) => {
408
415
  const preparedMigrationConfigs = migrationConfigs ||
409
416
  prepareMigrationConfigs({
410
417
  migrationConfig: migrationConfig || [],
@@ -475,6 +482,7 @@ export const doTheMigration = async ({ itemType = "story", from, itemsToMigrate,
475
482
  itemType,
476
483
  dryRun,
477
484
  publish,
485
+ publishLanguages,
478
486
  migrateFrom,
479
487
  fromFilePath,
480
488
  pipelineResult,
@@ -492,11 +500,22 @@ export const doTheMigration = async ({ itemType = "story", from, itemsToMigrate,
492
500
  return;
493
501
  }
494
502
  let writeResults = [];
503
+ let resolvedPublishLanguages;
495
504
  if (itemType === "story") {
505
+ if (publish && publishLanguages !== undefined) {
506
+ resolvedPublishLanguages =
507
+ await managementApi.stories.resolvePublishLanguageCodes(publishLanguages, {
508
+ ...config,
509
+ spaceId: to,
510
+ });
511
+ }
496
512
  writeResults = await managementApi.stories.updateStories({
497
513
  stories: pipelineResult.changedItems,
498
514
  spaceId: to,
499
- options: { publish: Boolean(publish) },
515
+ options: {
516
+ publish: Boolean(publish),
517
+ publishLanguages: resolvedPublishLanguages,
518
+ },
500
519
  }, config);
501
520
  }
502
521
  else if (itemType === "preset") {
@@ -507,6 +526,27 @@ export const doTheMigration = async ({ itemType = "story", from, itemsToMigrate,
507
526
  }, config);
508
527
  }
509
528
  const writeSummary = summarizeMutationWriteResults(writeResults);
529
+ try {
530
+ await saveMigrationRunLog({
531
+ artifactBaseName,
532
+ useDatestamp,
533
+ from,
534
+ to,
535
+ itemType,
536
+ dryRun,
537
+ publish,
538
+ publishLanguages,
539
+ resolvedPublishLanguages,
540
+ migrateFrom,
541
+ fromFilePath,
542
+ pipelineResult,
543
+ writeResults,
544
+ writeSummary,
545
+ }, config);
546
+ }
547
+ catch (error) {
548
+ Logger.warning(`[MIGRATION] Could not write migration run log: ${error instanceof Error ? error.message : String(error)}`);
549
+ }
510
550
  if (writeSummary.failed === 0) {
511
551
  Logger.success(`[MIGRATION] Update complete. ${writeSummary.successful}/${writeSummary.total} ${itemType}(s) updated successfully.`);
512
552
  return;
@@ -530,7 +570,7 @@ const saveBackupToFile = async ({ itemType, res, folder, filename }, config) =>
530
570
  res: res,
531
571
  }, config);
532
572
  };
533
- export const migrateProvidedComponentsDataInStories = async ({ itemType, migrationConfig, migrateFrom, from, to, componentsToMigrate, filters, dryRun, publish, fromFilePath, fileName, preparedMigrationConfigs, migrationComponentAliases, migrationComponentOverrides, }, config) => {
573
+ export const migrateProvidedComponentsDataInStories = async ({ itemType, migrationConfig, migrateFrom, from, to, componentsToMigrate, filters, dryRun, publish, publishLanguages, fromFilePath, fileName, preparedMigrationConfigs, migrationComponentAliases, migrationComponentOverrides, }, config) => {
534
574
  const resolvedMigrationConfigs = preparedMigrationConfigs ||
535
575
  prepareMigrationConfigs({
536
576
  migrationConfig,
@@ -566,6 +606,7 @@ export const migrateProvidedComponentsDataInStories = async ({ itemType, migrati
566
606
  to,
567
607
  dryRun,
568
608
  publish,
609
+ publishLanguages,
569
610
  migrateFrom,
570
611
  fromFilePath,
571
612
  fileName,
@@ -0,0 +1,73 @@
1
+ import type { MigrateFrom, MigrationPipelineResult } from "./component-data-migration.js";
2
+ import type { MutationWriteResult, MutationWriteSummary } from "./write-summary.js";
3
+ import type { PublishLanguagesOption } from "../stories/stories.types.js";
4
+ import type { RequestBaseConfig } from "../utils/request.js";
5
+ type MigrationRunLogEvent = "update_success" | "update_failed" | "publish_success" | "publish_failed" | "migration_write_summary";
6
+ export interface MigrationRunLogRecord {
7
+ timestamp: string;
8
+ event: MigrationRunLogEvent;
9
+ runId: string;
10
+ itemType: "story" | "preset";
11
+ source: {
12
+ migrateFrom: MigrateFrom;
13
+ from: string;
14
+ fromFilePath: string | null;
15
+ };
16
+ target: {
17
+ to: string;
18
+ };
19
+ writeMode: "publish" | "save";
20
+ publishLanguages?: {
21
+ requested?: PublishLanguagesOption;
22
+ resolved?: string[];
23
+ };
24
+ dryRun: boolean;
25
+ migrationConfigs: string[];
26
+ totalItems: number;
27
+ totalChangedItems: number;
28
+ writeSummary?: {
29
+ total: number;
30
+ successful: number;
31
+ failed: number;
32
+ failedItems: Array<{
33
+ id?: number | string;
34
+ name?: string;
35
+ slug?: string;
36
+ spaceId?: string;
37
+ status?: number | string;
38
+ response?: string | null;
39
+ stage?: "update" | "publish";
40
+ }>;
41
+ };
42
+ item?: {
43
+ index: number;
44
+ id?: number | string;
45
+ name?: string;
46
+ slug?: string;
47
+ spaceId?: string;
48
+ };
49
+ status?: number | string | null;
50
+ response?: string | null;
51
+ stage?: "update" | "publish";
52
+ error?: unknown;
53
+ }
54
+ interface SaveMigrationRunLogArgs {
55
+ artifactBaseName: string;
56
+ useDatestamp: boolean;
57
+ from: string;
58
+ to: string;
59
+ itemType: "story" | "preset";
60
+ dryRun?: boolean;
61
+ publish?: boolean;
62
+ publishLanguages?: PublishLanguagesOption;
63
+ resolvedPublishLanguages?: string[];
64
+ migrateFrom: MigrateFrom;
65
+ fromFilePath?: string;
66
+ pipelineResult: MigrationPipelineResult;
67
+ writeResults: PromiseSettledResult<MutationWriteResult>[];
68
+ writeSummary: MutationWriteSummary;
69
+ }
70
+ export declare const buildMigrationRunLogRecords: ({ from, to, itemType, dryRun, publish, publishLanguages, resolvedPublishLanguages, migrateFrom, fromFilePath, pipelineResult, writeResults, writeSummary, }: Omit<SaveMigrationRunLogArgs, "artifactBaseName" | "useDatestamp">) => MigrationRunLogRecord[];
71
+ export declare const recordsToJsonl: (records: MigrationRunLogRecord[]) => string;
72
+ export declare const saveMigrationRunLog: (args: SaveMigrationRunLogArgs, config: RequestBaseConfig) => Promise<void>;
73
+ export {};
@@ -0,0 +1,124 @@
1
+ import { generateDatestamp } from "../../utils/date-utils.js";
2
+ import { createDir, createJsonFile } from "../../utils/files.js";
3
+ import Logger from "../../utils/logger.js";
4
+ const serializeError = (error) => {
5
+ if (!error) {
6
+ return null;
7
+ }
8
+ if (error instanceof Error) {
9
+ return {
10
+ name: error.name,
11
+ message: error.message,
12
+ stack: error.stack,
13
+ };
14
+ }
15
+ try {
16
+ return JSON.parse(JSON.stringify(error));
17
+ }
18
+ catch {
19
+ return String(error);
20
+ }
21
+ };
22
+ const resolveChangedItemPayload = (item) => item?.story || item;
23
+ const resolveWriteResultValue = (result) => {
24
+ if (result.status === "fulfilled") {
25
+ return (result.value || {
26
+ ok: false,
27
+ });
28
+ }
29
+ return {
30
+ ok: false,
31
+ error: result.reason,
32
+ };
33
+ };
34
+ export const buildMigrationRunLogRecords = ({ from, to, itemType, dryRun, publish, publishLanguages, resolvedPublishLanguages, migrateFrom, fromFilePath, pipelineResult, writeResults, writeSummary, }) => {
35
+ const timestamp = new Date().toISOString();
36
+ const runId = `${itemType}-${timestamp}`;
37
+ const baseRecord = {
38
+ timestamp,
39
+ runId,
40
+ itemType,
41
+ source: {
42
+ migrateFrom,
43
+ from,
44
+ fromFilePath: fromFilePath || null,
45
+ },
46
+ target: {
47
+ to,
48
+ },
49
+ writeMode: itemType === "story" && publish ? "publish" : "save",
50
+ ...(publish
51
+ ? {
52
+ publishLanguages: {
53
+ requested: publishLanguages,
54
+ resolved: resolvedPublishLanguages,
55
+ },
56
+ }
57
+ : {}),
58
+ dryRun: Boolean(dryRun),
59
+ migrationConfigs: pipelineResult.stepReports.map((step) => step.migrationConfig),
60
+ totalItems: pipelineResult.totalItems,
61
+ totalChangedItems: pipelineResult.changedItems.length,
62
+ };
63
+ const updateRecords = writeResults.map((result, index) => {
64
+ const value = resolveWriteResultValue(result);
65
+ const changedItem = resolveChangedItemPayload(pipelineResult.changedItems[index]);
66
+ const stage = value.stage || "update";
67
+ const event = value.ok
68
+ ? stage === "publish"
69
+ ? "publish_success"
70
+ : "update_success"
71
+ : stage === "publish"
72
+ ? "publish_failed"
73
+ : "update_failed";
74
+ return {
75
+ ...baseRecord,
76
+ event,
77
+ item: {
78
+ index,
79
+ id: value.id || changedItem?.id,
80
+ name: value.name || changedItem?.name,
81
+ slug: value.slug ||
82
+ changedItem?.full_slug ||
83
+ changedItem?.slug,
84
+ spaceId: value.spaceId || to,
85
+ },
86
+ status: value.status || null,
87
+ response: value.response || null,
88
+ stage,
89
+ ...(value.ok ? {} : { error: serializeError(value.error) }),
90
+ };
91
+ });
92
+ const summaryRecord = {
93
+ ...baseRecord,
94
+ event: "migration_write_summary",
95
+ writeSummary: {
96
+ total: writeSummary.total,
97
+ successful: writeSummary.successful,
98
+ failed: writeSummary.failed,
99
+ failedItems: writeSummary.failedItems.map((item) => ({
100
+ id: item.id,
101
+ name: item.name,
102
+ slug: item.slug,
103
+ spaceId: item.spaceId,
104
+ status: item.status,
105
+ response: item.response || null,
106
+ stage: item.stage,
107
+ })),
108
+ },
109
+ };
110
+ return [...updateRecords, summaryRecord];
111
+ };
112
+ export const recordsToJsonl = (records) => `${records.map((record) => JSON.stringify(record)).join("\n")}\n`;
113
+ export const saveMigrationRunLog = async (args, config) => {
114
+ const { sbmigWorkingDirectory } = config;
115
+ const { artifactBaseName, useDatestamp, itemType, dryRun } = args;
116
+ const timestamp = generateDatestamp(new Date());
117
+ const finalFilename = `${dryRun ? "dry-run--" : ""}${artifactBaseName}---${itemType}-migration-run-log${useDatestamp ? `__${timestamp}` : ""}.jsonl`;
118
+ const folderPath = `${sbmigWorkingDirectory}/migrations/`;
119
+ const fullPath = `${folderPath}${finalFilename}`;
120
+ const records = buildMigrationRunLogRecords(args);
121
+ await createDir(folderPath);
122
+ await createJsonFile(recordsToJsonl(records), fullPath);
123
+ Logger.success(`Migration run log written to a file: ${fullPath}`);
124
+ };
@@ -1,8 +1,13 @@
1
1
  export interface MutationWriteResult {
2
2
  ok: boolean;
3
+ stage?: "update" | "publish";
3
4
  id?: number | string;
4
5
  name?: string;
5
6
  slug?: string;
7
+ spaceId?: string;
8
+ status?: number | string;
9
+ response?: string | null;
10
+ publishLanguages?: string[];
6
11
  error?: unknown;
7
12
  }
8
13
  export interface MutationWriteSummary {
@@ -58,6 +58,19 @@ export declare const managementApi: {
58
58
  removeStory: import("./stories/stories.types.js").RemoveStory;
59
59
  getStoryBySlug: import("./stories/stories.types.js").GetStoryBySlug;
60
60
  updateStories: import("./stories/stories.types.js").UpdateStories;
61
+ publishStoryLanguages: ({ storyId, story, languages, }: {
62
+ storyId: string | number;
63
+ story?: Record<string, any>;
64
+ languages: string[];
65
+ }, config: {
66
+ spaceId: string;
67
+ sbApi: any;
68
+ }) => Promise<any>;
69
+ parsePublishLanguagesOption: (publishLanguages?: string) => import("./stories/stories.types.js").PublishLanguagesOption;
70
+ resolvePublishLanguageCodes: (publishLanguages: import("./stories/stories.types.js").PublishLanguagesOption | undefined, config: {
71
+ spaceId: string;
72
+ sbApi: any;
73
+ }) => Promise<string[]>;
61
74
  getAllStories: import("./stories/stories.types.js").GetAllStories;
62
75
  removeAllStories: import("./stories/stories.types.js").RemoveAllStories;
63
76
  upsertStory: import("./stories/stories.types.js").UpsertStory;
@@ -1,2 +1,2 @@
1
- export { createStory, updateStory, getStoryById, removeStory, getStoryBySlug, updateStories, getAllStories, removeAllStories, upsertStory, } from "./stories.js";
1
+ export { createStory, updateStory, getStoryById, removeStory, getStoryBySlug, updateStories, publishStoryLanguages, parsePublishLanguagesOption, resolvePublishLanguageCodes, getAllStories, removeAllStories, upsertStory, } from "./stories.js";
2
2
  export { backupStories } from "./backup.js";
@@ -1,2 +1,2 @@
1
- export { createStory, updateStory, getStoryById, removeStory, getStoryBySlug, updateStories, getAllStories, removeAllStories, upsertStory, } from "./stories.js";
1
+ export { createStory, updateStory, getStoryById, removeStory, getStoryBySlug, updateStories, publishStoryLanguages, parsePublishLanguagesOption, resolvePublishLanguageCodes, getAllStories, removeAllStories, upsertStory, } from "./stories.js";
2
2
  export { backupStories } from "./backup.js";
@@ -1,4 +1,9 @@
1
- import type { GetAllStories, GetStoryById, RemoveStory, CreateStory, UpdateStory, UpdateStories, RemoveAllStories, UpsertStory, DeepUpsertStory, GetStoryBySlug } from "./stories.types.js";
1
+ import type { GetAllStories, GetStoryById, RemoveStory, CreateStory, UpdateStory, UpdateStories, RemoveAllStories, UpsertStory, DeepUpsertStory, GetStoryBySlug, PublishLanguagesOption } from "./stories.types.js";
2
+ export declare const parsePublishLanguagesOption: (publishLanguages?: string) => PublishLanguagesOption;
3
+ export declare const resolvePublishLanguageCodes: (publishLanguages: PublishLanguagesOption | undefined, config: {
4
+ spaceId: string;
5
+ sbApi: any;
6
+ }) => Promise<string[]>;
2
7
  export declare const removeStory: RemoveStory;
3
8
  export declare const removeAllStories: RemoveAllStories;
4
9
  export declare const getAllStories: GetAllStories;
@@ -6,6 +11,14 @@ export declare const getStoryById: GetStoryById;
6
11
  export declare const getStoryBySlug: GetStoryBySlug;
7
12
  export declare const createStory: CreateStory;
8
13
  export declare const updateStory: UpdateStory;
14
+ export declare const publishStoryLanguages: ({ storyId, story, languages, }: {
15
+ storyId: string | number;
16
+ story?: Record<string, any>;
17
+ languages: string[];
18
+ }, config: {
19
+ spaceId: string;
20
+ sbApi: any;
21
+ }) => Promise<any>;
9
22
  export declare const updateStories: UpdateStories;
10
23
  export declare const upsertStory: UpsertStory;
11
24
  export declare const deepUpsertStory: DeepUpsertStory;
@@ -3,6 +3,81 @@ import Logger from "../../utils/logger.js";
3
3
  import { notNullish } from "../../utils/object-utils.js";
4
4
  import { getAllItemsWithPagination } from "../utils/request.js";
5
5
  const resolveStoryLabel = (content, storyId) => content?.full_slug || content?.slug || content?.name || String(storyId);
6
+ const DEFAULT_PUBLISH_LANGUAGE = "[default]";
7
+ const isDefaultLanguageToken = (language) => language.toLowerCase() === "default" ||
8
+ language === DEFAULT_PUBLISH_LANGUAGE;
9
+ const normalizePublishLanguageCodes = (languages) => {
10
+ const normalized = languages.map((language) => {
11
+ const trimmed = language.trim();
12
+ return isDefaultLanguageToken(trimmed)
13
+ ? DEFAULT_PUBLISH_LANGUAGE
14
+ : trimmed;
15
+ });
16
+ const cleanLanguages = normalized.filter((language) => language.length > 0);
17
+ if (cleanLanguages.length === 0) {
18
+ throw new Error("Publish languages cannot be empty.");
19
+ }
20
+ return Array.from(new Set(cleanLanguages));
21
+ };
22
+ export const parsePublishLanguagesOption = (publishLanguages) => {
23
+ if (!publishLanguages) {
24
+ return "default";
25
+ }
26
+ const trimmed = publishLanguages.trim();
27
+ if (trimmed.length === 0) {
28
+ return "default";
29
+ }
30
+ if (trimmed.toLowerCase() === "all") {
31
+ return "all";
32
+ }
33
+ const languages = normalizePublishLanguageCodes(trimmed.split(","));
34
+ if (languages.length === 1 && languages[0] === DEFAULT_PUBLISH_LANGUAGE) {
35
+ return "default";
36
+ }
37
+ return languages;
38
+ };
39
+ const readSpaceLanguageCodes = (spaceResponse) => {
40
+ const space = spaceResponse?.data?.space || spaceResponse?.space || {};
41
+ const languageSources = [
42
+ space.languages,
43
+ space.language_codes,
44
+ space.options?.languages,
45
+ ];
46
+ for (const source of languageSources) {
47
+ if (!Array.isArray(source)) {
48
+ continue;
49
+ }
50
+ return source
51
+ .map((language) => {
52
+ if (typeof language === "string") {
53
+ return language;
54
+ }
55
+ if (typeof language?.code === "string") {
56
+ return language.code;
57
+ }
58
+ return "";
59
+ })
60
+ .filter((language) => language.trim().length > 0);
61
+ }
62
+ return [];
63
+ };
64
+ export const resolvePublishLanguageCodes = async (publishLanguages, config) => {
65
+ if (!publishLanguages || publishLanguages === "default") {
66
+ return [DEFAULT_PUBLISH_LANGUAGE];
67
+ }
68
+ if (Array.isArray(publishLanguages)) {
69
+ return normalizePublishLanguageCodes(publishLanguages);
70
+ }
71
+ const spaceResponse = await config.sbApi.get(`spaces/${config.spaceId}`);
72
+ const spaceLanguageCodes = readSpaceLanguageCodes(spaceResponse);
73
+ if (spaceLanguageCodes.length === 0) {
74
+ Logger.warning(`No configured Storyblok languages were found for space '${config.spaceId}'. Publishing only ${DEFAULT_PUBLISH_LANGUAGE}.`);
75
+ }
76
+ return normalizePublishLanguageCodes([
77
+ DEFAULT_PUBLISH_LANGUAGE,
78
+ ...spaceLanguageCodes,
79
+ ]);
80
+ };
6
81
  const resolveStoryblokErrorResponse = (err) => {
7
82
  if (typeof err?.response === "string" && err.response.trim().length > 0) {
8
83
  return err.response.trim();
@@ -139,6 +214,7 @@ export const updateStory = (content, storyId, options, config) => {
139
214
  console.log(`${chalk.green(res.data.story.full_slug)} updated.`);
140
215
  return {
141
216
  ok: true,
217
+ stage: "update",
142
218
  id: res.data.story.id,
143
219
  name: res.data.story.name,
144
220
  slug: res.data.story.full_slug,
@@ -155,6 +231,7 @@ export const updateStory = (content, storyId, options, config) => {
155
231
  Logger.error(`Failed to update story '${storyLabel}' in space '${spaceId}' (${statusLabel}).${responseLabel}`);
156
232
  return {
157
233
  ok: false,
234
+ stage: "update",
158
235
  id: storyId,
159
236
  name: content?.name,
160
237
  slug: content?.full_slug || content?.slug,
@@ -165,12 +242,78 @@ export const updateStory = (content, storyId, options, config) => {
165
242
  };
166
243
  });
167
244
  };
168
- export const updateStories = (args, config) => {
245
+ export const publishStoryLanguages = async ({ storyId, story, languages, }, config) => {
246
+ const { spaceId, sbApi } = config;
247
+ const lang = languages.join(",");
248
+ const storyLabel = resolveStoryLabel(story, String(storyId));
249
+ Logger.log(`Publishing story '${storyLabel}' in space '${spaceId}' for languages: ${lang}`);
250
+ return sbApi
251
+ .get(`spaces/${spaceId}/stories/${storyId}/publish`, { lang })
252
+ .then((res) => {
253
+ const publishedStory = res?.data?.story || story || {};
254
+ Logger.success(`Published story '${storyLabel}' for languages: ${lang}`);
255
+ return {
256
+ ok: true,
257
+ stage: "publish",
258
+ id: publishedStory.id || storyId,
259
+ name: publishedStory.name || story?.name,
260
+ slug: publishedStory.full_slug ||
261
+ publishedStory.slug ||
262
+ story?.full_slug ||
263
+ story?.slug,
264
+ spaceId,
265
+ publishLanguages: languages,
266
+ data: res?.data,
267
+ };
268
+ })
269
+ .catch((err) => {
270
+ const status = err?.status || err?.response?.status;
271
+ const responseMessage = resolveStoryblokErrorResponse(err);
272
+ const statusLabel = status ? `status ${status}` : "unknown status";
273
+ const responseLabel = responseMessage
274
+ ? ` Response: ${responseMessage}`
275
+ : "";
276
+ Logger.error(`Failed to publish story '${storyLabel}' in space '${spaceId}' (${statusLabel}).${responseLabel}`);
277
+ return {
278
+ ok: false,
279
+ stage: "publish",
280
+ id: storyId,
281
+ name: story?.name,
282
+ slug: story?.full_slug || story?.slug,
283
+ spaceId,
284
+ status,
285
+ response: responseMessage,
286
+ publishLanguages: languages,
287
+ error: err,
288
+ };
289
+ });
290
+ };
291
+ export const updateStories = async (args, config) => {
169
292
  const { stories, options, spaceId } = args;
293
+ const shouldPublishLanguages = options.publish && options.publishLanguages !== undefined;
294
+ const publishLanguages = shouldPublishLanguages
295
+ ? await resolvePublishLanguageCodes(options.publishLanguages, {
296
+ ...config,
297
+ spaceId,
298
+ })
299
+ : undefined;
170
300
  return Promise.allSettled(
171
301
  // Run through stories, and update the space with migrated version of stories
172
302
  stories.map(async (stories) => {
173
- return updateStory(stories.story, stories.story.id, { publish: options.publish }, { ...config, spaceId });
303
+ const story = stories.story;
304
+ const updateResult = await updateStory(story, story.id, {
305
+ publish: options.publish && !shouldPublishLanguages,
306
+ }, { ...config, spaceId });
307
+ if (!shouldPublishLanguages ||
308
+ !publishLanguages ||
309
+ !updateResult?.ok) {
310
+ return updateResult;
311
+ }
312
+ return publishStoryLanguages({
313
+ storyId: story.id,
314
+ story,
315
+ languages: publishLanguages,
316
+ }, { ...config, spaceId });
174
317
  }));
175
318
  };
176
319
  export const upsertStory = async (args, config) => {
@@ -6,7 +6,9 @@ export interface ExtendedISbStoriesParams extends ISbStoriesParams {
6
6
  interface ModifyStoryOptions {
7
7
  publish?: boolean;
8
8
  force_update?: boolean;
9
+ publishLanguages?: PublishLanguagesOption;
9
10
  }
11
+ export type PublishLanguagesOption = "default" | "all" | string[];
10
12
  export type RemoveStory = (args: {
11
13
  storyId: string;
12
14
  }, config: RequestBaseConfig) => Promise<any>;
@@ -58,6 +58,19 @@ export declare const testApi: {
58
58
  removeStory: import("./stories/stories.types.js").RemoveStory;
59
59
  getStoryBySlug: import("./stories/stories.types.js").GetStoryBySlug;
60
60
  updateStories: import("./stories/stories.types.js").UpdateStories;
61
+ publishStoryLanguages: ({ storyId, story, languages, }: {
62
+ storyId: string | number;
63
+ story?: Record<string, any>;
64
+ languages: string[];
65
+ }, config: {
66
+ spaceId: string;
67
+ sbApi: any;
68
+ }) => Promise<any>;
69
+ parsePublishLanguagesOption: (publishLanguages?: string) => import("./stories/stories.types.js").PublishLanguagesOption;
70
+ resolvePublishLanguageCodes: (publishLanguages: import("./stories/stories.types.js").PublishLanguagesOption | undefined, config: {
71
+ spaceId: string;
72
+ sbApi: any;
73
+ }) => Promise<string[]>;
61
74
  getAllStories: import("./stories/stories.types.js").GetAllStories;
62
75
  removeAllStories: import("./stories/stories.types.js").RemoveAllStories;
63
76
  upsertStory: import("./stories/stories.types.js").UpsertStory;
@@ -1,7 +1,7 @@
1
1
  export declare const mainDescription = "\n USAGE\n $ sb-mig [command]\n \n COMMANDS\n sync Synchronize components, datasources, roles, stories, assets with Storyblok space.\n discover Discover components, migration configs and write to file or stdout.\n backup Command for backing up anything related to Storyblok\n migrate Migrate content from space to space, or from file to space.\n debug Output extra debugging information\n help This screen\n \n Examples\n $ sb-mig sync components --all\n $ sb-mig debug \n";
2
2
  export declare const syncDescription = "\n Usage\n $ sb-mig sync [components|roles|datasources|plugins|content] [space separated file names] or --all\n \n Description\n Synchronize components, roles, datasources, plugins, content with Storyblok space.\n \n COMMANDS\n components - sync components\n roles - sync roles\n datasources - sync datasources\n plugins - sync plugins\n content - sync content (stories, assets) - ! right now destructive, it will move content from 1 space to another, completelly overwriting it\n \n FLAGS\n --all - Sync all components, roles, datasources [components, roles, datasources]\n --presets - Pass it, if u want to sync also with presets (will take longer) [components only]\n --dry-run - Preview planned changes without making writes [components, roles, datasources, plugins, content]\n \n --yes - Skip ask for confirmation (dangerous, but useful in CI/CD) [content only]\n --from - Space ID from which you want to sync content [content only]\n --to - Space ID to which you want to sync content [content only]\n --syncDirection [fromSpaceToFile|fromFileToSpace|fromSpaceToSpace|fromAWStoSpace] \n - Sync direction (from, to) [content only]\n \n EXAMPLES\n $ sb-mig sync components --all\n $ sb-mig sync components --all --dry-run\n $ sb-mig sync components --all --presets\n $ sb-mig sync components accordion accordion-item\n $ sb-mig sync components accordion accordion-item --presets\n \n $ sb-mig sync roles --all\n $ sb-mig sync roles --all --dry-run\n \n $ sb-mig sync datasources --all\n $ sb-mig sync datasources --all --dry-run\n \n $ sb-mig sync plugins my-awesome-plugin - (you have to be in catalog which has ./dist/export.js file with compiled plugin)\n \n $ sb-mig sync content --all --from 12345 --to 12345\n $ sb-mig sync content --stories --from 12345 --to 12345\n $ sb-mig sync content --assets --from 12345 --to 12345\n";
3
3
  export declare const copyDescription = "\n Usage\n $ sb-mig copy\n \n Description\n Copy stuff\n \n COMMANDS\n ?\n \n FLAGS\n ?\n \n EXAMPLES\n $ sb-mig copy ?\n";
4
- export declare const migrateDescription = "\n Usage\n $ sb-mig migrate [content] [space separated file names] or --all --from [spaceId] --to [spaceId] --migration [migration-config-filename]\n $ sb-mig migrate content --all --migration migration-a --migration migration-b --migration migration-c\n \n Description\n Migrate content from space to space, or from file to space. It's potentially dangerous command, so it will ask for confirmation.\n Use with care.\n \n COMMANDS\n content - migrate content \n \n FLAGS\n --from - Space ID from which you want to migrate / or file name if passed '--migrate-from file'\n --fromFilePath - Direct path to stories JSON file when using '--migrate-from file'\n --to - Space ID to which you want to migrate\n --migrate-from - Migrate from (space, file) default: space\n --migration - File name of migration file (without extension). Can be repeated for ordered pipeline in content migration.\n --migrationComponentAlias - Add extra component aliases for a migration. Repeatable. Format: <migration>:<source>=<alias1>,<alias2>\n --migrationComponents - Override the exact component scope for a migration. Repeatable. Format: <migration>:<component1>,<component2>\n --withSlug - Filter stories by full slug (can be repeated)\n --startsWith - Filter stories by starts_with prefix\n --yes - Skip ask for confirmation (dangerous, but useful in CI/CD)\n --dry-run - Preview what would be migrated without making any API changes\n --publish - Publish changed stories immediately after migration. Default: save draft. [content only]\n --fileName - Stable base name for migration output files (disables timestamp suffix for migration artifacts)\n\n EXAMPLES\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration file-with-migration\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration migration-a --migration migration-b --migration migration-c\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration colorPickerModeValues --migrationComponentAlias colorPickerModeValues:sb-button=sb-open-drift-button\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration colorPickerModeValues --migrationComponentAlias colorPickerModeValues:sb-section=sb-tour-page-section --migrationComponents colorPickerModeValues:sb-section,sb-tour-page-section\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration file-with-migration --withSlug blog/home --withSlug docs/getting-started\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration file-with-migration --startsWith blog/\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration file-with-migration --publish --yes\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration v3toV4AllMigrations --dry-run --fileName brand-hub-v3-v4-run\n $ sb-mig migrate content --all --migrate-from file --from file-with-stories --to 12345 --migration file-with-migration\n $ sb-mig migrate content --all --migrate-from file --fromFilePath sbmig/migrations/dry-run--123---story-to-migrate__2026-2-9_20-51.json --to 12345 --migration migration-a --migration migration-b\n $ sb-mig migrate content my-component-1 my-component-2 --from 12345 --to 12345 --migration file-with-migration\n $ sb-mig migrate content my-component-1 my-component-2 --migrate-from file --from file-with-stories --to 12345 --migration file-with-migration \n";
4
+ export declare const migrateDescription = "\n Usage\n $ sb-mig migrate [content] [space separated file names] or --all --from [spaceId] --to [spaceId] --migration [migration-config-filename]\n $ sb-mig migrate content --all --migration migration-a --migration migration-b --migration migration-c\n \n Description\n Migrate content from space to space, or from file to space. It's potentially dangerous command, so it will ask for confirmation.\n Use with care.\n \n COMMANDS\n content - migrate content \n \n FLAGS\n --from - Space ID from which you want to migrate / or file name if passed '--migrate-from file'\n --fromFilePath - Direct path to stories JSON file when using '--migrate-from file'\n --to - Space ID to which you want to migrate\n --migrate-from - Migrate from (space, file) default: space\n --migration - File name of migration file (without extension). Can be repeated for ordered pipeline in content migration.\n --migrationComponentAlias - Add extra component aliases for a migration. Repeatable. Format: <migration>:<source>=<alias1>,<alias2>\n --migrationComponents - Override the exact component scope for a migration. Repeatable. Format: <migration>:<component1>,<component2>\n --withSlug - Filter stories by full slug (can be repeated)\n --startsWith - Filter stories by starts_with prefix\n --yes - Skip ask for confirmation (dangerous, but useful in CI/CD)\n --dry-run - Preview what would be migrated without making any API changes\n --publish - Publish changed stories immediately after migration. Default: save draft. [content only]\n --publishLanguages - Languages to publish when --publish is set. Values: default, all, or comma-separated Storyblok language codes. [content only]\n --fileName - Stable base name for migration output files (disables timestamp suffix for migration artifacts)\n\n EXAMPLES\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration file-with-migration\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration migration-a --migration migration-b --migration migration-c\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration colorPickerModeValues --migrationComponentAlias colorPickerModeValues:sb-button=sb-open-drift-button\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration colorPickerModeValues --migrationComponentAlias colorPickerModeValues:sb-section=sb-tour-page-section --migrationComponents colorPickerModeValues:sb-section,sb-tour-page-section\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration file-with-migration --withSlug blog/home --withSlug docs/getting-started\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration file-with-migration --startsWith blog/\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration file-with-migration --publish --yes\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration file-with-migration --publish --publishLanguages all --yes\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration file-with-migration --publish --publishLanguages default,fr,de --yes\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration v3toV4AllMigrations --dry-run --fileName brand-hub-v3-v4-run\n $ sb-mig migrate content --all --migrate-from file --from file-with-stories --to 12345 --migration file-with-migration\n $ sb-mig migrate content --all --migrate-from file --fromFilePath sbmig/migrations/dry-run--123---story-to-migrate__2026-2-9_20-51.json --to 12345 --migration migration-a --migration migration-b\n $ sb-mig migrate content my-component-1 my-component-2 --from 12345 --to 12345 --migration file-with-migration\n $ sb-mig migrate content my-component-1 my-component-2 --migrate-from file --from file-with-stories --to 12345 --migration file-with-migration \n";
5
5
  export declare const revertDescription = "\n Usage\n $ sb-mig revert [content] --migration\n \n Description\n Revert content migration\n \n COMMANDS\n content - revert content migration \n \n FLAGS\n --migration - ???\n --yes - Skip ask for confirmation (dangerous, but useful in CI/CD) \n \n EXAMPLES\n $ sb-mig revert content --migration \n";
6
6
  export declare const discoverDescription = "\n Usage\n $ sb-mig discover [components|migrations] --all --write\n\n Description\n Discover all components or migration configs and write to file or stdout\n\n COMMANDS\n components - discover components\n migrations - discover migration config files\n\n FLAGS\n --all - Discover all components or migration configs\n --write - Write to file\n\n EXAMPLES\n $ sb-mig discover components --all\n $ sb-mig discover components --all --write\n $ sb-mig discover migrations --all\n";
7
7
  export declare const migrationsDescription = "\n Usage\n $ sb-mig migrations recognize\n \n Description\n Recognize migrations you need to apply\n \n COMMANDS\n recognize - recognize migrations\n \n FLAGS \n \n EXAMPLES\n $ sb-mig migrations recognize\n\n";
@@ -99,6 +99,7 @@ export const migrateDescription = `
99
99
  --yes - Skip ask for confirmation (dangerous, but useful in CI/CD)
100
100
  --dry-run - Preview what would be migrated without making any API changes
101
101
  --publish - Publish changed stories immediately after migration. Default: save draft. [content only]
102
+ --publishLanguages - Languages to publish when --publish is set. Values: default, all, or comma-separated Storyblok language codes. [content only]
102
103
  --fileName - Stable base name for migration output files (disables timestamp suffix for migration artifacts)
103
104
 
104
105
  EXAMPLES
@@ -109,6 +110,8 @@ export const migrateDescription = `
109
110
  $ sb-mig migrate content --all --from 12345 --to 12345 --migration file-with-migration --withSlug blog/home --withSlug docs/getting-started
110
111
  $ sb-mig migrate content --all --from 12345 --to 12345 --migration file-with-migration --startsWith blog/
111
112
  $ sb-mig migrate content --all --from 12345 --to 12345 --migration file-with-migration --publish --yes
113
+ $ sb-mig migrate content --all --from 12345 --to 12345 --migration file-with-migration --publish --publishLanguages all --yes
114
+ $ sb-mig migrate content --all --from 12345 --to 12345 --migration file-with-migration --publish --publishLanguages default,fr,de --yes
112
115
  $ sb-mig migrate content --all --from 12345 --to 12345 --migration v3toV4AllMigrations --dry-run --fileName brand-hub-v3-v4-run
113
116
  $ sb-mig migrate content --all --migrate-from file --from file-with-stories --to 12345 --migration file-with-migration
114
117
  $ sb-mig migrate content --all --migrate-from file --fromFilePath sbmig/migrations/dry-run--123---story-to-migrate__2026-2-9_20-51.json --to 12345 --migration migration-a --migration migration-b
@@ -4,6 +4,7 @@ import { buildStoryBackupBaseName } from "../../api/data-migration/file-naming.j
4
4
  import { parseMigrationComponentAliasFlags, parseMigrationComponentOverrideFlags, } from "../../api/data-migration/migration-component-scope.js";
5
5
  import { managementApi } from "../../api/managementApi.js";
6
6
  import { backupStories } from "../../api/stories/backup.js";
7
+ import { parsePublishLanguagesOption } from "../../api/stories/stories.js";
7
8
  import { createAndSaveToFile } from "../../utils/files.js";
8
9
  import Logger from "../../utils/logger.js";
9
10
  import { apiConfig } from "../api-config.js";
@@ -42,6 +43,7 @@ export const migrate = async (props) => {
42
43
  "startsWith",
43
44
  "dryRun",
44
45
  "publish",
46
+ "publishLanguages",
45
47
  "fileName",
46
48
  ]);
47
49
  Logger.warning(`This feature is in BETA. Use it at your own risk. The API might change in the future. (Probably in a standard Prisma like migration way)`);
@@ -62,6 +64,10 @@ export const migrate = async (props) => {
62
64
  const migrationComponentOverrides = parseMigrationComponentOverrideFlags(flags["migrationComponents"]);
63
65
  const dryRun = flags["dryRun"];
64
66
  const publish = Boolean(flags["publish"]);
67
+ const publishLanguagesFlag = flags["publishLanguages"];
68
+ const publishLanguages = publishLanguagesFlag
69
+ ? parsePublishLanguagesOption(publishLanguagesFlag)
70
+ : undefined;
65
71
  const fileName = flags["fileName"];
66
72
  const withSlugFlag = flags["withSlug"];
67
73
  const withSlug = Array.isArray(withSlugFlag)
@@ -73,6 +79,9 @@ export const migrate = async (props) => {
73
79
  if (migrationConfigs.length === 0) {
74
80
  throw new Error("Missing migration config. Pass at least one --migration value.");
75
81
  }
82
+ if (!publish && publishLanguagesFlag) {
83
+ throw new Error("--publishLanguages requires --publish for 'migrate content'.");
84
+ }
76
85
  if (isIt("empty")) {
77
86
  const componentsToMigrate = unpackElements(input) || [""];
78
87
  const migrateFrom = "space";
@@ -100,6 +109,7 @@ export const migrate = async (props) => {
100
109
  filters: { withSlug, startsWith },
101
110
  dryRun,
102
111
  publish,
112
+ publishLanguages,
103
113
  fromFilePath,
104
114
  fileName,
105
115
  }, apiConfig);
@@ -128,6 +138,7 @@ export const migrate = async (props) => {
128
138
  filters: { withSlug, startsWith },
129
139
  dryRun,
130
140
  publish,
141
+ publishLanguages,
131
142
  fromFilePath,
132
143
  fileName,
133
144
  }, apiConfig);
@@ -163,12 +174,16 @@ export const migrate = async (props) => {
163
174
  getFrom(flags, apiConfig);
164
175
  const to = getTo(flags, apiConfig);
165
176
  const publish = Boolean(flags["publish"]);
177
+ const publishLanguagesFlag = flags["publishLanguages"];
166
178
  if (migrationConfigs.length === 0) {
167
179
  throw new Error("Missing migration config. Pass exactly one --migration value for presets.");
168
180
  }
169
181
  if (publish) {
170
182
  throw new Error("--publish is only supported for 'migrate content'. Presets cannot be published.");
171
183
  }
184
+ if (publishLanguagesFlag) {
185
+ throw new Error("--publishLanguages is only supported for 'migrate content'. Presets cannot be published.");
186
+ }
172
187
  if (migrationConfigs.length > 1) {
173
188
  throw new Error("Multiple --migration values are currently supported only for 'migrate content'. Presets support a single migration config.");
174
189
  }
package/dist/cli/index.js CHANGED
@@ -84,6 +84,9 @@ app.migrate = () => ({
84
84
  type: "boolean",
85
85
  default: false,
86
86
  },
87
+ publishLanguages: {
88
+ type: "string",
89
+ },
87
90
  fileName: {
88
91
  type: "string",
89
92
  },
@@ -3,12 +3,89 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.deepUpsertStory = exports.upsertStory = exports.updateStories = exports.updateStory = exports.createStory = exports.getStoryBySlug = exports.getStoryById = exports.getAllStories = exports.removeAllStories = exports.removeStory = void 0;
6
+ exports.deepUpsertStory = exports.upsertStory = exports.updateStories = exports.publishStoryLanguages = exports.updateStory = exports.createStory = exports.getStoryBySlug = exports.getStoryById = exports.getAllStories = exports.removeAllStories = exports.removeStory = exports.resolvePublishLanguageCodes = exports.parsePublishLanguagesOption = void 0;
7
7
  const chalk_1 = __importDefault(require("chalk"));
8
8
  const logger_js_1 = __importDefault(require("../../utils/logger.js"));
9
9
  const object_utils_js_1 = require("../../utils/object-utils.js");
10
10
  const request_js_1 = require("../utils/request.js");
11
11
  const resolveStoryLabel = (content, storyId) => content?.full_slug || content?.slug || content?.name || String(storyId);
12
+ const DEFAULT_PUBLISH_LANGUAGE = "[default]";
13
+ const isDefaultLanguageToken = (language) => language.toLowerCase() === "default" ||
14
+ language === DEFAULT_PUBLISH_LANGUAGE;
15
+ const normalizePublishLanguageCodes = (languages) => {
16
+ const normalized = languages.map((language) => {
17
+ const trimmed = language.trim();
18
+ return isDefaultLanguageToken(trimmed)
19
+ ? DEFAULT_PUBLISH_LANGUAGE
20
+ : trimmed;
21
+ });
22
+ const cleanLanguages = normalized.filter((language) => language.length > 0);
23
+ if (cleanLanguages.length === 0) {
24
+ throw new Error("Publish languages cannot be empty.");
25
+ }
26
+ return Array.from(new Set(cleanLanguages));
27
+ };
28
+ const parsePublishLanguagesOption = (publishLanguages) => {
29
+ if (!publishLanguages) {
30
+ return "default";
31
+ }
32
+ const trimmed = publishLanguages.trim();
33
+ if (trimmed.length === 0) {
34
+ return "default";
35
+ }
36
+ if (trimmed.toLowerCase() === "all") {
37
+ return "all";
38
+ }
39
+ const languages = normalizePublishLanguageCodes(trimmed.split(","));
40
+ if (languages.length === 1 && languages[0] === DEFAULT_PUBLISH_LANGUAGE) {
41
+ return "default";
42
+ }
43
+ return languages;
44
+ };
45
+ exports.parsePublishLanguagesOption = parsePublishLanguagesOption;
46
+ const readSpaceLanguageCodes = (spaceResponse) => {
47
+ const space = spaceResponse?.data?.space || spaceResponse?.space || {};
48
+ const languageSources = [
49
+ space.languages,
50
+ space.language_codes,
51
+ space.options?.languages,
52
+ ];
53
+ for (const source of languageSources) {
54
+ if (!Array.isArray(source)) {
55
+ continue;
56
+ }
57
+ return source
58
+ .map((language) => {
59
+ if (typeof language === "string") {
60
+ return language;
61
+ }
62
+ if (typeof language?.code === "string") {
63
+ return language.code;
64
+ }
65
+ return "";
66
+ })
67
+ .filter((language) => language.trim().length > 0);
68
+ }
69
+ return [];
70
+ };
71
+ const resolvePublishLanguageCodes = async (publishLanguages, config) => {
72
+ if (!publishLanguages || publishLanguages === "default") {
73
+ return [DEFAULT_PUBLISH_LANGUAGE];
74
+ }
75
+ if (Array.isArray(publishLanguages)) {
76
+ return normalizePublishLanguageCodes(publishLanguages);
77
+ }
78
+ const spaceResponse = await config.sbApi.get(`spaces/${config.spaceId}`);
79
+ const spaceLanguageCodes = readSpaceLanguageCodes(spaceResponse);
80
+ if (spaceLanguageCodes.length === 0) {
81
+ logger_js_1.default.warning(`No configured Storyblok languages were found for space '${config.spaceId}'. Publishing only ${DEFAULT_PUBLISH_LANGUAGE}.`);
82
+ }
83
+ return normalizePublishLanguageCodes([
84
+ DEFAULT_PUBLISH_LANGUAGE,
85
+ ...spaceLanguageCodes,
86
+ ]);
87
+ };
88
+ exports.resolvePublishLanguageCodes = resolvePublishLanguageCodes;
12
89
  const resolveStoryblokErrorResponse = (err) => {
13
90
  if (typeof err?.response === "string" && err.response.trim().length > 0) {
14
91
  return err.response.trim();
@@ -151,6 +228,7 @@ const updateStory = (content, storyId, options, config) => {
151
228
  console.log(`${chalk_1.default.green(res.data.story.full_slug)} updated.`);
152
229
  return {
153
230
  ok: true,
231
+ stage: "update",
154
232
  id: res.data.story.id,
155
233
  name: res.data.story.name,
156
234
  slug: res.data.story.full_slug,
@@ -167,6 +245,7 @@ const updateStory = (content, storyId, options, config) => {
167
245
  logger_js_1.default.error(`Failed to update story '${storyLabel}' in space '${spaceId}' (${statusLabel}).${responseLabel}`);
168
246
  return {
169
247
  ok: false,
248
+ stage: "update",
170
249
  id: storyId,
171
250
  name: content?.name,
172
251
  slug: content?.full_slug || content?.slug,
@@ -178,12 +257,79 @@ const updateStory = (content, storyId, options, config) => {
178
257
  });
179
258
  };
180
259
  exports.updateStory = updateStory;
181
- const updateStories = (args, config) => {
260
+ const publishStoryLanguages = async ({ storyId, story, languages, }, config) => {
261
+ const { spaceId, sbApi } = config;
262
+ const lang = languages.join(",");
263
+ const storyLabel = resolveStoryLabel(story, String(storyId));
264
+ logger_js_1.default.log(`Publishing story '${storyLabel}' in space '${spaceId}' for languages: ${lang}`);
265
+ return sbApi
266
+ .get(`spaces/${spaceId}/stories/${storyId}/publish`, { lang })
267
+ .then((res) => {
268
+ const publishedStory = res?.data?.story || story || {};
269
+ logger_js_1.default.success(`Published story '${storyLabel}' for languages: ${lang}`);
270
+ return {
271
+ ok: true,
272
+ stage: "publish",
273
+ id: publishedStory.id || storyId,
274
+ name: publishedStory.name || story?.name,
275
+ slug: publishedStory.full_slug ||
276
+ publishedStory.slug ||
277
+ story?.full_slug ||
278
+ story?.slug,
279
+ spaceId,
280
+ publishLanguages: languages,
281
+ data: res?.data,
282
+ };
283
+ })
284
+ .catch((err) => {
285
+ const status = err?.status || err?.response?.status;
286
+ const responseMessage = resolveStoryblokErrorResponse(err);
287
+ const statusLabel = status ? `status ${status}` : "unknown status";
288
+ const responseLabel = responseMessage
289
+ ? ` Response: ${responseMessage}`
290
+ : "";
291
+ logger_js_1.default.error(`Failed to publish story '${storyLabel}' in space '${spaceId}' (${statusLabel}).${responseLabel}`);
292
+ return {
293
+ ok: false,
294
+ stage: "publish",
295
+ id: storyId,
296
+ name: story?.name,
297
+ slug: story?.full_slug || story?.slug,
298
+ spaceId,
299
+ status,
300
+ response: responseMessage,
301
+ publishLanguages: languages,
302
+ error: err,
303
+ };
304
+ });
305
+ };
306
+ exports.publishStoryLanguages = publishStoryLanguages;
307
+ const updateStories = async (args, config) => {
182
308
  const { stories, options, spaceId } = args;
309
+ const shouldPublishLanguages = options.publish && options.publishLanguages !== undefined;
310
+ const publishLanguages = shouldPublishLanguages
311
+ ? await (0, exports.resolvePublishLanguageCodes)(options.publishLanguages, {
312
+ ...config,
313
+ spaceId,
314
+ })
315
+ : undefined;
183
316
  return Promise.allSettled(
184
317
  // Run through stories, and update the space with migrated version of stories
185
318
  stories.map(async (stories) => {
186
- return (0, exports.updateStory)(stories.story, stories.story.id, { publish: options.publish }, { ...config, spaceId });
319
+ const story = stories.story;
320
+ const updateResult = await (0, exports.updateStory)(story, story.id, {
321
+ publish: options.publish && !shouldPublishLanguages,
322
+ }, { ...config, spaceId });
323
+ if (!shouldPublishLanguages ||
324
+ !publishLanguages ||
325
+ !updateResult?.ok) {
326
+ return updateResult;
327
+ }
328
+ return (0, exports.publishStoryLanguages)({
329
+ storyId: story.id,
330
+ story,
331
+ languages: publishLanguages,
332
+ }, { ...config, spaceId });
187
333
  }));
188
334
  };
189
335
  exports.updateStories = updateStories;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sb-mig",
3
- "version": "6.0.0-beta.3",
3
+ "version": "6.0.0-beta.5",
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",