storyblok 4.19.2 → 4.20.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -26,6 +26,7 @@ import { Octokit } from 'octokit';
26
26
  import { pipeline as pipeline$1 } from 'node:stream/promises';
27
27
  import { Buffer } from 'node:buffer';
28
28
  import Storyblok from 'storyblok-js-client';
29
+ import { createTwoFilesPatch } from 'diff';
29
30
 
30
31
  const commands = {
31
32
  LOGIN: "login",
@@ -41,7 +42,8 @@ const commands = {
41
42
  LOGS: "logs",
42
43
  REPORTS: "reports",
43
44
  ASSETS: "assets",
44
- STORIES: "stories"
45
+ STORIES: "stories",
46
+ SCHEMA: "schema"
45
47
  };
46
48
  const colorPalette = {
47
49
  PRIMARY: "#8d60ff",
@@ -61,7 +63,8 @@ const colorPalette = {
61
63
  LOGS: "#4ade80",
62
64
  REPORTS: "#4ade80",
63
65
  ASSETS: "#f97316",
64
- STORIES: "#a185ff"
66
+ STORIES: "#a185ff",
67
+ SCHEMA: "#e91e63"
65
68
  };
66
69
  const regions = {
67
70
  EU: "eu",
@@ -695,6 +698,8 @@ const API_ACTIONS = {
695
698
  pull_component_internal_tags: "Failed to pull component internal tags",
696
699
  push_component: "Failed to push component",
697
700
  push_component_group: "Failed to push component group",
701
+ push_component_folder: "Failed to create component folder",
702
+ delete_component_folder: "Failed to delete component folder",
698
703
  push_component_preset: "Failed to push component preset",
699
704
  push_component_internal_tag: "Failed to push component internal tag",
700
705
  update_component: "Failed to update component",
@@ -702,6 +707,7 @@ const API_ACTIONS = {
702
707
  update_component_group: "Failed to update component group",
703
708
  update_component_preset: "Failed to update component preset",
704
709
  delete_component_preset: "Failed to delete component preset",
710
+ delete_component: "Failed to delete component",
705
711
  pull_stories: "Failed to pull stories",
706
712
  pull_story: "Failed to pull story",
707
713
  create_story: "Failed to create story",
@@ -729,6 +735,8 @@ const API_ACTIONS = {
729
735
  push_datasource: "Failed to push datasource",
730
736
  update_datasource: "Failed to update datasource",
731
737
  delete_datasource: "Failed to delete datasource",
738
+ push_datasource_entry: "Failed to push datasource entry",
739
+ update_datasource_entry: "Failed to update datasource entry",
732
740
  delete_datasource_entry: "Failed to delete datasource entry",
733
741
  create_space: "Failed to create space",
734
742
  pull_spaces: "Failed to pull spaces",
@@ -800,7 +808,11 @@ class APIError extends Error {
800
808
  if (this.code === 422) {
801
809
  const responseData = this.response?.data;
802
810
  if (responseData?.name?.[0] === "has already been taken") {
803
- this.message = "A component with this name already exists";
811
+ if (action === "push_component_folder") {
812
+ this.message = "A component folder with this name already exists";
813
+ } else if (action === "push_component" || action === "update_component") {
814
+ this.message = "A component with this name already exists";
815
+ }
804
816
  }
805
817
  Object.entries(responseData || {}).forEach(([key, errors]) => {
806
818
  if (Array.isArray(errors)) {
@@ -1078,6 +1090,7 @@ function maskToken(token) {
1078
1090
  const maskedPart = "*".repeat(token.length - 4);
1079
1091
  return `${visiblePart}${maskedPart}`;
1080
1092
  }
1093
+ const slugify = (text) => text.toString().toLowerCase().replace(/\s+/g, "-").replace(/[^\w-]+/g, "").replace(/-{2,}/g, "-").replace(/^-+/, "").replace(/-+$/, "");
1081
1094
  function createRegexFromGlob(pattern) {
1082
1095
  return new RegExp(`^${pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\\*/g, ".*")}$`);
1083
1096
  }
@@ -1159,6 +1172,25 @@ function getPackageJson() {
1159
1172
  return packageJson$1;
1160
1173
  }
1161
1174
 
1175
+ async function fetchAllPages(fetchFunction, extractDataFunction) {
1176
+ const items = [];
1177
+ let page = 1;
1178
+ while (true) {
1179
+ const { data, response } = await fetchFunction(page);
1180
+ const totalHeader = response.headers.get("total");
1181
+ const fetchedItems = extractDataFunction(data);
1182
+ items.push(...fetchedItems);
1183
+ if (!totalHeader) {
1184
+ return items;
1185
+ }
1186
+ const total = Number(totalHeader);
1187
+ if (Number.isNaN(total) || items.length >= total || fetchedItems.length === 0) {
1188
+ return items;
1189
+ }
1190
+ page++;
1191
+ }
1192
+ }
1193
+
1162
1194
  const __filename$1 = fileURLToPath(import.meta.url);
1163
1195
  const __dirname$1 = dirname(__filename$1);
1164
1196
  function isRegion(value) {
@@ -1247,6 +1279,10 @@ class UI {
1247
1279
  this.br();
1248
1280
  }
1249
1281
  }
1282
+ /** Plain console.log passthrough — use for preformatted or multi-line text. */
1283
+ log(message) {
1284
+ this.console?.log(message);
1285
+ }
1250
1286
  list(items) {
1251
1287
  for (const item of items) {
1252
1288
  this.console?.log(` ${item}`);
@@ -2164,14 +2200,14 @@ async function performInteractiveLogin(options) {
2164
2200
  }
2165
2201
  }
2166
2202
 
2167
- const program$e = getProgram();
2203
+ const program$f = getProgram();
2168
2204
  const allRegionsText = Object.values(regions).join(",");
2169
- program$e.command(commands.LOGIN).description("Login to the Storyblok CLI").option("-t, --token <token>", "Token to login directly without questions, like for CI environments").option(
2205
+ program$f.command(commands.LOGIN).description("Login to the Storyblok CLI").option("-t, --token <token>", "Token to login directly without questions, like for CI environments").option(
2170
2206
  "-r, --region <region>",
2171
2207
  `The region you would like to work in. Please keep in mind that the region must match the region of your space. This region flag will be used for the other cli's commands. You can use the values: ${allRegionsText}.`
2172
2208
  ).action(async (options) => {
2173
2209
  konsola.title(`${commands.LOGIN}`, colorPalette.LOGIN);
2174
- const verbose = program$e.opts().verbose;
2210
+ const verbose = program$f.opts().verbose;
2175
2211
  const { token, region } = options;
2176
2212
  const { state, updateSession, persistCredentials } = session();
2177
2213
  if (state.isLoggedIn && !state.envLogin) {
@@ -2229,10 +2265,10 @@ program$e.command(commands.LOGIN).description("Login to the Storyblok CLI").opti
2229
2265
  konsola.br();
2230
2266
  });
2231
2267
 
2232
- const program$d = getProgram();
2233
- program$d.command(commands.LOGOUT).description("Logout from the Storyblok CLI").action(async () => {
2268
+ const program$e = getProgram();
2269
+ program$e.command(commands.LOGOUT).description("Logout from the Storyblok CLI").action(async () => {
2234
2270
  konsola.title(`${commands.LOGOUT}`, colorPalette.LOGOUT);
2235
- const verbose = program$d.opts().verbose;
2271
+ const verbose = program$e.opts().verbose;
2236
2272
  try {
2237
2273
  const { state } = session();
2238
2274
  if (!state.isLoggedIn || !state.password || !state.region) {
@@ -2279,10 +2315,10 @@ async function openSignupInBrowser(url) {
2279
2315
  }
2280
2316
  }
2281
2317
 
2282
- const program$c = getProgram();
2283
- program$c.command(commands.SIGNUP).description("Sign up for Storyblok").action(async () => {
2318
+ const program$d = getProgram();
2319
+ program$d.command(commands.SIGNUP).description("Sign up for Storyblok").action(async () => {
2284
2320
  konsola.title(`${commands.SIGNUP}`, colorPalette.SIGNUP);
2285
- const verbose = program$c.opts().verbose;
2321
+ const verbose = program$d.opts().verbose;
2286
2322
  const { state } = session();
2287
2323
  if (state.isLoggedIn && !state.envLogin) {
2288
2324
  konsola.ok(`You are already logged in. If you want to signup with a different account, please logout first.`);
@@ -2303,10 +2339,10 @@ program$c.command(commands.SIGNUP).description("Sign up for Storyblok").action(a
2303
2339
  konsola.br();
2304
2340
  });
2305
2341
 
2306
- const program$b = getProgram();
2307
- program$b.command(commands.USER).description("Get the current user").action(async () => {
2342
+ const program$c = getProgram();
2343
+ program$c.command(commands.USER).description("Get the current user").action(async () => {
2308
2344
  konsola.title(`${commands.USER}`, colorPalette.USER);
2309
- const verbose = program$b.opts().verbose;
2345
+ const verbose = program$c.opts().verbose;
2310
2346
  const { state } = session();
2311
2347
  if (!requireAuthentication(state)) {
2312
2348
  return;
@@ -2334,10 +2370,10 @@ program$b.command(commands.USER).description("Get the current user").action(asyn
2334
2370
  konsola.br();
2335
2371
  });
2336
2372
 
2337
- const program$a = getProgram();
2338
- const componentsCommand = program$a.command(commands.COMPONENTS).alias("comp").description(`Manage your space's block schema`);
2373
+ const program$b = getProgram();
2374
+ const componentsCommand = program$b.command(commands.COMPONENTS).alias("comp").description(`Manage your space's block schema`);
2339
2375
 
2340
- function isComponent(item) {
2376
+ function isComponent$1(item) {
2341
2377
  return "schema" in item;
2342
2378
  }
2343
2379
  function isPreset(item) {
@@ -2371,7 +2407,7 @@ async function loadComponents(directoryPath, options) {
2371
2407
  throw new Error('Internal tag is missing "id"!');
2372
2408
  }
2373
2409
  tagMap.set(item.id, item);
2374
- } else if (isComponent(item)) {
2410
+ } else if (isComponent$1(item)) {
2375
2411
  const existing = componentMap.get(item.name);
2376
2412
  if (existing) {
2377
2413
  duplicates.push(`Component "${item.name}" found in both "${existing.file}" and "${file}"`);
@@ -2407,44 +2443,20 @@ To fix this, either:
2407
2443
  }
2408
2444
 
2409
2445
  const DEFAULT_COMPONENTS_FILENAME = "components";
2410
- const DEFAULT_GROUPS_FILENAME = "groups";
2446
+ const DEFAULT_GROUPS_FILENAME$1 = "groups";
2411
2447
  const DEFAULT_PRESETS_FILENAME = "presets";
2412
2448
  const DEFAULT_TAGS_FILENAME = "tags";
2413
2449
 
2414
- async function fetchAllPages(fetchFunction, extractDataFunction) {
2415
- const items = [];
2416
- let page = 1;
2417
- while (true) {
2418
- const { data, response } = await fetchFunction(page);
2419
- const totalHeader = response.headers.get("total");
2420
- const fetchedItems = extractDataFunction(data);
2421
- items.push(...fetchedItems);
2422
- if (!totalHeader) {
2423
- return items;
2424
- }
2425
- const total = Number(totalHeader);
2426
- if (Number.isNaN(total) || items.length >= total || fetchedItems.length === 0) {
2427
- return items;
2428
- }
2429
- page++;
2430
- }
2431
- }
2432
-
2433
2450
  const fetchComponents = async (spaceId) => {
2434
2451
  try {
2435
2452
  const client = getMapiClient();
2436
- return await fetchAllPages(
2437
- (page) => client.components.list({
2438
- path: {
2439
- space_id: Number(spaceId)
2440
- },
2441
- query: {
2442
- page
2443
- },
2444
- throwOnError: true
2445
- }),
2446
- (data) => data?.components ?? []
2447
- );
2453
+ const { data } = await client.components.list({
2454
+ path: {
2455
+ space_id: Number(spaceId)
2456
+ },
2457
+ throwOnError: true
2458
+ });
2459
+ return data?.components ?? [];
2448
2460
  } catch (error) {
2449
2461
  handleAPIError("pull_components", error);
2450
2462
  }
@@ -2452,19 +2464,16 @@ const fetchComponents = async (spaceId) => {
2452
2464
  const fetchComponent = async (spaceId, componentName) => {
2453
2465
  try {
2454
2466
  const client = getMapiClient();
2455
- const matches = await fetchAllPages(
2456
- (page) => client.components.list({
2457
- path: {
2458
- space_id: Number(spaceId)
2459
- },
2460
- query: {
2461
- page,
2462
- search: componentName
2463
- },
2464
- throwOnError: true
2465
- }),
2466
- (data) => data?.components ?? []
2467
- );
2467
+ const { data } = await client.components.list({
2468
+ path: {
2469
+ space_id: Number(spaceId)
2470
+ },
2471
+ query: {
2472
+ search: componentName
2473
+ },
2474
+ throwOnError: true
2475
+ });
2476
+ const matches = data?.components ?? [];
2468
2477
  return matches.find((c) => c.name === componentName);
2469
2478
  } catch (error) {
2470
2479
  handleAPIError("pull_components", error, `Failed to fetch component ${componentName}`);
@@ -2531,7 +2540,7 @@ const saveComponentsToFiles = async (space, spaceData, options) => {
2531
2540
  const presetsFilePath = join(resolvedPath, suffix ? `${sanitizedName}.${DEFAULT_PRESETS_FILENAME}.${suffix}.json` : `${sanitizedName}.${DEFAULT_PRESETS_FILENAME}.json`);
2532
2541
  await saveToFile(presetsFilePath, JSON.stringify(componentPresets, null, 2));
2533
2542
  }
2534
- const groupsFilePath = join(resolvedPath, suffix ? `${DEFAULT_GROUPS_FILENAME}.${suffix}.json` : `${DEFAULT_GROUPS_FILENAME}.json`);
2543
+ const groupsFilePath = join(resolvedPath, suffix ? `${DEFAULT_GROUPS_FILENAME$1}.${suffix}.json` : `${DEFAULT_GROUPS_FILENAME$1}.json`);
2535
2544
  await saveToFile(groupsFilePath, JSON.stringify(groups, null, 2));
2536
2545
  const internalTagsFilePath = join(resolvedPath, suffix ? `${DEFAULT_TAGS_FILENAME}.${suffix}.json` : `${DEFAULT_TAGS_FILENAME}.json`);
2537
2546
  await saveToFile(internalTagsFilePath, JSON.stringify(internalTags, null, 2));
@@ -2541,7 +2550,7 @@ const saveComponentsToFiles = async (space, spaceData, options) => {
2541
2550
  const componentsFilePath = join(resolvedPath, suffix ? `${filename}.${suffix}.json` : `${filename}.json`);
2542
2551
  await saveToFile(componentsFilePath, JSON.stringify(components, null, 2));
2543
2552
  if (groups.length > 0) {
2544
- const groupsFilePath = join(resolvedPath, suffix ? `${DEFAULT_GROUPS_FILENAME}.${suffix}.json` : `${DEFAULT_GROUPS_FILENAME}.json`);
2553
+ const groupsFilePath = join(resolvedPath, suffix ? `${DEFAULT_GROUPS_FILENAME$1}.${suffix}.json` : `${DEFAULT_GROUPS_FILENAME$1}.json`);
2545
2554
  await saveToFile(groupsFilePath, JSON.stringify(groups, null, 2));
2546
2555
  }
2547
2556
  if (presets.length > 0) {
@@ -2557,6 +2566,27 @@ const saveComponentsToFiles = async (space, spaceData, options) => {
2557
2566
  }
2558
2567
  };
2559
2568
 
2569
+ function isSchemaField$1(value) {
2570
+ return typeof value === "object" && value !== null && "type" in value;
2571
+ }
2572
+ function toWritableSchema(schema) {
2573
+ if (!schema) {
2574
+ return void 0;
2575
+ }
2576
+ const result = {};
2577
+ for (const [key, value] of Object.entries(schema)) {
2578
+ if (isSchemaField$1(value)) {
2579
+ result[key] = value;
2580
+ }
2581
+ }
2582
+ return result;
2583
+ }
2584
+ function toRequestTagIds(tagIds) {
2585
+ if (!tagIds) {
2586
+ return void 0;
2587
+ }
2588
+ return tagIds.map((id) => Number(id));
2589
+ }
2560
2590
  const pushComponent = async (space, component) => {
2561
2591
  try {
2562
2592
  const client = getMapiClient();
@@ -2591,10 +2621,23 @@ const updateComponent = async (space, componentId, component) => {
2591
2621
  }
2592
2622
  };
2593
2623
  const upsertComponent = async (space, component, existingId) => {
2624
+ const { name, display_name, schema, is_root, is_nestable, component_group_uuid, color, icon, preview_field, internal_tag_ids } = component;
2625
+ const payload = {
2626
+ name,
2627
+ display_name: display_name ?? void 0,
2628
+ schema: toWritableSchema(schema),
2629
+ is_root,
2630
+ is_nestable,
2631
+ component_group_uuid: component_group_uuid ?? void 0,
2632
+ color: color ?? void 0,
2633
+ icon: icon ?? void 0,
2634
+ preview_field: preview_field ?? void 0,
2635
+ internal_tag_ids: toRequestTagIds(internal_tag_ids)
2636
+ };
2594
2637
  if (existingId) {
2595
- return await updateComponent(space, existingId, component);
2638
+ return await updateComponent(space, existingId, payload);
2596
2639
  } else {
2597
- return await pushComponent(space, component);
2640
+ return await pushComponent(space, payload);
2598
2641
  }
2599
2642
  };
2600
2643
  const pushComponentGroup = async (space, componentGroup) => {
@@ -2646,7 +2689,14 @@ const pushComponentPreset = async (space, preset) => {
2646
2689
  space_id: Number(space)
2647
2690
  },
2648
2691
  body: {
2649
- preset
2692
+ preset: {
2693
+ ...preset,
2694
+ preset: preset.preset ?? void 0,
2695
+ image: preset.image ?? void 0,
2696
+ color: preset.color ?? void 0,
2697
+ icon: preset.icon ?? void 0,
2698
+ description: preset.description ?? void 0
2699
+ }
2650
2700
  },
2651
2701
  throwOnError: true
2652
2702
  });
@@ -2663,7 +2713,14 @@ const updateComponentPreset = async (space, presetId, preset) => {
2663
2713
  space_id: Number(space)
2664
2714
  },
2665
2715
  body: {
2666
- preset
2716
+ preset: {
2717
+ ...preset,
2718
+ preset: preset.preset ?? void 0,
2719
+ image: preset.image ?? void 0,
2720
+ color: preset.color ?? void 0,
2721
+ icon: preset.icon ?? void 0,
2722
+ description: preset.description ?? void 0
2723
+ }
2667
2724
  },
2668
2725
  throwOnError: true
2669
2726
  });
@@ -2697,7 +2754,7 @@ const pushComponentInternalTag = async (space, componentInternalTag) => {
2697
2754
  path: {
2698
2755
  space_id: Number(space)
2699
2756
  },
2700
- body: componentInternalTag,
2757
+ body: { internal_tag: componentInternalTag },
2701
2758
  throwOnError: true
2702
2759
  });
2703
2760
  return data.internal_tag;
@@ -2712,7 +2769,7 @@ const updateComponentInternalTag = async (space, tagId, componentInternalTag) =>
2712
2769
  path: {
2713
2770
  space_id: Number(space)
2714
2771
  },
2715
- body: componentInternalTag,
2772
+ body: { internal_tag: componentInternalTag },
2716
2773
  throwOnError: true
2717
2774
  });
2718
2775
  return data.internal_tag;
@@ -3389,7 +3446,7 @@ function filterSpaceData(spaceData, selectors) {
3389
3446
  if (filter && !minimatch(component.name, filter)) {
3390
3447
  return false;
3391
3448
  }
3392
- if (groupUuids && !(component.component_group_uuid !== void 0 && groupUuids.has(component.component_group_uuid))) {
3449
+ if (groupUuids && !(component.component_group_uuid != null && groupUuids.has(component.component_group_uuid))) {
3393
3450
  return false;
3394
3451
  }
3395
3452
  if (tagIds) {
@@ -3453,7 +3510,7 @@ function resolveGroupSelector(groups, selector) {
3453
3510
  while (p && !visited.has(p)) {
3454
3511
  visited.add(p);
3455
3512
  parts.unshift(nameOf(p) ?? p);
3456
- p = byUuid.get(p)?.parent_uuid;
3513
+ p = byUuid.get(p)?.parent_uuid ?? null;
3457
3514
  }
3458
3515
  return parts.join("/");
3459
3516
  });
@@ -4057,16 +4114,14 @@ const fetchSpace = async (spaceId) => {
4057
4114
  handleAPIError("pull_spaces", error, `Failed to fetch space ${spaceId}`);
4058
4115
  }
4059
4116
  };
4060
- const createSpace = async (space) => {
4117
+ const createSpace = async (space, query) => {
4061
4118
  try {
4062
- const { in_org, assign_partner, ...spaceData } = space;
4063
4119
  const client = getMapiClient();
4064
4120
  const { data } = await client.spaces.create({
4065
4121
  body: {
4066
- space: spaceData,
4067
- ...in_org && { in_org },
4068
- ...assign_partner && { assign_partner }
4069
- }
4122
+ space
4123
+ },
4124
+ query
4070
4125
  });
4071
4126
  return data?.space;
4072
4127
  } catch (error) {
@@ -4077,10 +4132,10 @@ const createSpace = async (space) => {
4077
4132
  const fetchLanguages = async (spaceId) => {
4078
4133
  try {
4079
4134
  const space = await fetchSpace(spaceId);
4080
- if (space?.default_lang_name !== void 0 && space?.languages?.length) {
4135
+ if (space?.default_lang_name && space?.languages?.length) {
4081
4136
  return {
4082
- default_lang_name: space?.default_lang_name,
4083
- languages: space?.languages
4137
+ default_lang_name: space.default_lang_name,
4138
+ languages: space.languages
4084
4139
  };
4085
4140
  }
4086
4141
  } catch (error) {
@@ -4100,8 +4155,8 @@ const saveLanguagesToFile = async (space, internationalizationOptions, options)
4100
4155
  }
4101
4156
  };
4102
4157
 
4103
- const program$9 = getProgram();
4104
- const languagesCommand = program$9.command(commands.LANGUAGES).alias("lang").description(`Manage your space's languages`);
4158
+ const program$a = getProgram();
4159
+ const languagesCommand = program$a.command(commands.LANGUAGES).alias("lang").description(`Manage your space's languages`);
4105
4160
  const pullCmd$3 = languagesCommand.command("pull").description(`Download your space's languages schema as json`).option("-f, --filename <filename>", "filename to save the file as <filename>.<suffix>.json").option("--su, --suffix <suffix>", "suffix to add to the file name (e.g. languages.<suffix>.json). By default, the space ID is used.").option("-s, --space <space>", "space ID");
4106
4161
  pullCmd$3.action(async (options, command) => {
4107
4162
  konsola.title(`${commands.LANGUAGES}`, colorPalette.LANGUAGES);
@@ -4147,8 +4202,8 @@ pullCmd$3.action(async (options, command) => {
4147
4202
  konsola.br();
4148
4203
  });
4149
4204
 
4150
- const program$8 = getProgram();
4151
- const migrationsCommand = program$8.command(commands.MIGRATIONS).alias("mig").description(`Manage your space's migrations`);
4205
+ const program$9 = getProgram();
4206
+ const migrationsCommand = program$9.command(commands.MIGRATIONS).alias("mig").description(`Manage your space's migrations`);
4152
4207
 
4153
4208
  const getMigrationTemplate = () => {
4154
4209
  return `export default function (block) {
@@ -4252,7 +4307,7 @@ const fetchStories = async (spaceId, params) => {
4252
4307
  const fetchStory = async (spaceId, storyId) => {
4253
4308
  try {
4254
4309
  const client = getMapiClient();
4255
- const { data } = await client.stories.get(storyId, {
4310
+ const { data } = await client.stories.get(Number(storyId), {
4256
4311
  path: {
4257
4312
  space_id: Number(spaceId)
4258
4313
  },
@@ -4297,9 +4352,11 @@ const updateStory = async (spaceId, storyId, payload) => {
4297
4352
  ...payload.story,
4298
4353
  // StoryUpdate2 expects `parent_id?: number`; normalize null → undefined.
4299
4354
  parent_id: payload.story.parent_id ?? void 0
4300
- },
4301
- force_update: payload.force_update === "1" ? "1" : "0",
4302
- ...payload.publish ? { publish: payload.publish } : {}
4355
+ }
4356
+ },
4357
+ query: {
4358
+ force_update: payload.force_update === "1",
4359
+ ...payload.publish ? { publish: Boolean(payload.publish) } : {}
4303
4360
  },
4304
4361
  throwOnError: true
4305
4362
  });
@@ -4396,6 +4453,35 @@ const prefetchTargetStoriesByKeys = async (spaceId, keys, options) => {
4396
4453
  return result;
4397
4454
  };
4398
4455
 
4456
+ function parseFilterQuery(input) {
4457
+ const trimmed = input.trim();
4458
+ if (!trimmed) {
4459
+ return {};
4460
+ }
4461
+ if (trimmed.startsWith("{")) {
4462
+ return JSON.parse(trimmed);
4463
+ }
4464
+ const result = {};
4465
+ for (const clause of trimmed.split("&")) {
4466
+ if (!clause) {
4467
+ continue;
4468
+ }
4469
+ const eq = clause.indexOf("=");
4470
+ if (eq === -1) {
4471
+ continue;
4472
+ }
4473
+ const path = clause.slice(0, eq);
4474
+ const value = clause.slice(eq + 1);
4475
+ const keys = [...path.matchAll(/\[([^\]]+)\]/g)].map((match) => match[1]);
4476
+ if (keys.length < 2) {
4477
+ continue;
4478
+ }
4479
+ const [field, operation] = keys;
4480
+ result[field] = { ...result[field], [operation]: value };
4481
+ }
4482
+ return result;
4483
+ }
4484
+
4399
4485
  const PIPELINE_BACKPRESSURE_MULTIPLIER = 2;
4400
4486
  const DEFAULT_PIPELINE_BACKPRESSURE = 12;
4401
4487
  function createPipelineBackpressureLock(limit) {
@@ -4419,16 +4505,13 @@ const ERROR_CODES = {
4419
4505
  async function* storiesIterator(spaceId, params, onTotal) {
4420
4506
  try {
4421
4507
  let perPage = 500;
4422
- const transformedParams = {
4423
- ...params
4424
- };
4425
- if (params?.componentName && typeof params.componentName === "string") {
4426
- transformedParams.contain_component = params.componentName;
4427
- delete transformedParams.componentName;
4508
+ const { componentName, query, ...rest } = params ?? {};
4509
+ const transformedParams = { ...rest };
4510
+ if (componentName) {
4511
+ transformedParams.contain_component = componentName;
4428
4512
  }
4429
- if (params?.query && typeof params.query === "string") {
4430
- transformedParams.filter_query = params.query.startsWith("filter_query") ? params.query : `filter_query${params.query}`;
4431
- delete transformedParams.query;
4513
+ if (query) {
4514
+ transformedParams.filter_query = parseFilterQuery(query);
4432
4515
  }
4433
4516
  const result = await fetchStories(spaceId, {
4434
4517
  ...transformedParams,
@@ -4753,8 +4836,8 @@ class MigrationStream extends Transform {
4753
4836
  id: story.id,
4754
4837
  name: story.name || "",
4755
4838
  content: story.content,
4756
- published: story.published,
4757
- unpublished_changes: story.unpublished_changes
4839
+ published: story.published ?? void 0,
4840
+ unpublished_changes: story.unpublished_changes ?? void 0
4758
4841
  },
4759
4842
  migrationTimestamp: this.timestamp,
4760
4843
  migrationNames
@@ -4770,8 +4853,8 @@ class MigrationStream extends Transform {
4770
4853
  storyId: story.id,
4771
4854
  name: story.name,
4772
4855
  content: storyContent,
4773
- published: story.published,
4774
- unpublished_changes: story.unpublished_changes
4856
+ published: story.published ?? void 0,
4857
+ unpublished_changes: story.unpublished_changes ?? void 0
4775
4858
  };
4776
4859
  } else if (processed && !contentChanged) {
4777
4860
  this.results.skipped.push({
@@ -4913,7 +4996,6 @@ class UpdateStream extends Writable {
4913
4996
  const payload = {
4914
4997
  story: {
4915
4998
  content,
4916
- id: storyId,
4917
4999
  name: storyName
4918
5000
  },
4919
5001
  force_update: "1"
@@ -5162,7 +5244,6 @@ rollbackCmd.action(async (migrationFile, _options, command) => {
5162
5244
  const payload = {
5163
5245
  story: {
5164
5246
  content: story.content,
5165
- id: story.storyId,
5166
5247
  name: story.name
5167
5248
  },
5168
5249
  force_update: "1"
@@ -5202,8 +5283,8 @@ rollbackCmd.action(async (migrationFile, _options, command) => {
5202
5283
  }
5203
5284
  });
5204
5285
 
5205
- const program$7 = getProgram();
5206
- const typesCommand = program$7.command(commands.TYPES).alias("ts").description(`Generate types d.ts for your component schemas`);
5286
+ const program$8 = getProgram();
5287
+ const typesCommand = program$8.command(commands.TYPES).alias("ts").description(`Generate types d.ts for your component schemas`);
5207
5288
 
5208
5289
  const getAssetJSONSchema = (title) => ({
5209
5290
  $id: "#/asset",
@@ -6233,7 +6314,7 @@ const deleteDatasourceEntry = async (spaceId, entryId) => {
6233
6314
  handleAPIError("delete_datasource_entry", error, `Failed to delete datasource entry ${entryId}`);
6234
6315
  }
6235
6316
  };
6236
- function isDatasource(item) {
6317
+ function isDatasource$1(item) {
6237
6318
  return typeof item === "object" && item !== null && "slug" in item && typeof item.slug === "string";
6238
6319
  }
6239
6320
  const readDatasourcesFiles = async (options) => {
@@ -6266,7 +6347,7 @@ const readDatasourcesFiles = async (options) => {
6266
6347
  continue;
6267
6348
  }
6268
6349
  for (const item of data) {
6269
- if (isDatasource(item)) {
6350
+ if (isDatasource$1(item)) {
6270
6351
  const existing = datasourceMap.get(item.slug);
6271
6352
  if (existing) {
6272
6353
  duplicates.push(`Datasource "${item.slug}" found in both "${existing.file}" and "${file}"`);
@@ -6366,8 +6447,8 @@ generateCmd.action(async (options, command) => {
6366
6447
  }
6367
6448
  });
6368
6449
 
6369
- const program$6 = getProgram();
6370
- const datasourcesCommand = program$6.command(commands.DATASOURCES).alias("ds").description(`Manage your space's datasources`);
6450
+ const program$7 = getProgram();
6451
+ const datasourcesCommand = program$7.command(commands.DATASOURCES).alias("ds").description(`Manage your space's datasources`);
6371
6452
 
6372
6453
  const DEFAULT_DATASOURCES_FILENAME = "datasources";
6373
6454
 
@@ -6383,7 +6464,7 @@ const fetchDatasourceEntries = async (spaceId, datasourceId, dimensionId) => {
6383
6464
  datasource_id: datasourceId,
6384
6465
  page,
6385
6466
  // MAPI matches `dimension` against the numeric dimension id.
6386
- ...dimensionId != null && { dimension: String(dimensionId) }
6467
+ ...dimensionId != null && { dimension: dimensionId }
6387
6468
  },
6388
6469
  throwOnError: true
6389
6470
  }),
@@ -7092,13 +7173,13 @@ async function promptForLogin(verbose) {
7092
7173
  return null;
7093
7174
  }
7094
7175
  }
7095
- const program$5 = getProgram();
7096
- program$5.command(`${commands.CREATE} [project-path]`).alias("c").description(`Scaffold a new project using Storyblok`).option("-t, --template <template>", "technology starter template").option("-b, --blueprint <blueprint>", "[DEPRECATED] use --template instead").option("--skip-space", "skip space creation").option("--token <token>", "Storyblok access token (skip space creation and use this token)").option(
7176
+ const program$6 = getProgram();
7177
+ program$6.command(`${commands.CREATE} [project-path]`).alias("c").description(`Scaffold a new project using Storyblok`).option("-t, --template <template>", "technology starter template").option("-b, --blueprint <blueprint>", "[DEPRECATED] use --template instead").option("--skip-space", "skip space creation").option("--token <token>", "Storyblok access token (skip space creation and use this token)").option(
7097
7178
  "-r, --region <region>",
7098
7179
  `The region to apply to the generated project template (does not affect space creation).`
7099
7180
  ).action(async (projectPath, options) => {
7100
7181
  ui.title(`${commands.CREATE}`, colorPalette.CREATE);
7101
- const verbose = program$5.opts().verbose;
7182
+ const verbose = program$6.opts().verbose;
7102
7183
  const { template, blueprint, token } = options;
7103
7184
  if (options.region && !isRegion(options.region)) {
7104
7185
  handleError(new CommandError(`The provided region: ${options.region} is not valid. Please use one of the following values: ${Object.values(regions).join(" | ")}`));
@@ -7271,13 +7352,13 @@ program$5.command(`${commands.CREATE} [project-path]`).alias("c").description(`S
7271
7352
  name: toHumanReadable(projectName),
7272
7353
  domain: blueprintDomain
7273
7354
  };
7355
+ const createSpaceQuery = {};
7274
7356
  if (whereToCreateSpace === "org") {
7275
- spaceToCreate.org = userData.org;
7276
- spaceToCreate.in_org = true;
7357
+ createSpaceQuery.in_org = true;
7277
7358
  } else if (whereToCreateSpace === "partner") {
7278
- spaceToCreate.assign_partner = true;
7359
+ createSpaceQuery.assign_partner = true;
7279
7360
  }
7280
- createdSpace = await createSpace(spaceToCreate);
7361
+ createdSpace = await createSpace(spaceToCreate, createSpaceQuery);
7281
7362
  spinnerSpace.succeed(`Space "${chalk.hex(colorPalette.PRIMARY)(toHumanReadable(projectName))}" created successfully`);
7282
7363
  if (createdSpace?.first_token) {
7283
7364
  await handleEnvFileCreation(resolvedPath, createdSpace.first_token, region, technologyTemplate);
@@ -7317,8 +7398,8 @@ program$5.command(`${commands.CREATE} [project-path]`).alias("c").description(`S
7317
7398
  ui.br();
7318
7399
  });
7319
7400
 
7320
- const program$4 = getProgram();
7321
- const logsCommand = program$4.command(commands.LOGS).alias("lg").description(`Inspect and manage logs.`);
7401
+ const program$5 = getProgram();
7402
+ const logsCommand = program$5.command(commands.LOGS).alias("lg").description(`Inspect and manage logs.`);
7322
7403
 
7323
7404
  const listCmd$1 = logsCommand.command("list").description("List logs").option("-s, --space <space>", "space ID");
7324
7405
  listCmd$1.action(async (_options, command) => {
@@ -7343,8 +7424,8 @@ pruneCmd$1.action(async (options, command) => {
7343
7424
  ui.info(`Deleted ${deletedFilesCount} log file${deletedFilesCount === 1 ? "" : "s"}`);
7344
7425
  });
7345
7426
 
7346
- const program$3 = getProgram();
7347
- const reportsCommand = program$3.command(commands.REPORTS).alias("rp").description("Inspect and manage reports.");
7427
+ const program$4 = getProgram();
7428
+ const reportsCommand = program$4.command(commands.REPORTS).alias("rp").description("Inspect and manage reports.");
7348
7429
 
7349
7430
  const listCmd = reportsCommand.command("list").description("List reports").option("-s, --space <space>", "space ID");
7350
7431
  listCmd.action(async (_options, command) => {
@@ -7369,8 +7450,8 @@ pruneCmd.action(async (options, command) => {
7369
7450
  ui.info(`Deleted ${deletedFilesCount} report file${deletedFilesCount === 1 ? "" : "s"}`);
7370
7451
  });
7371
7452
 
7372
- const program$2 = getProgram();
7373
- const assetsCommand = program$2.command(commands.ASSETS).description(`Manage your space's assets`);
7453
+ const program$3 = getProgram();
7454
+ const assetsCommand = program$3.command(commands.ASSETS).description(`Manage your space's assets`);
7374
7455
 
7375
7456
  const fetchAssets = async ({ spaceId, params }) => {
7376
7457
  try {
@@ -7418,7 +7499,7 @@ const createAssetInternalTag = async (spaceId, name) => {
7418
7499
  const client = getMapiClient();
7419
7500
  const { data } = await client.internalTags.create({
7420
7501
  path: { space_id: Number(spaceId) },
7421
- body: { name, object_type: "asset" },
7502
+ body: { internal_tag: { name, object_type: "asset" } },
7422
7503
  throwOnError: true
7423
7504
  });
7424
7505
  const tag = data?.internal_tag;
@@ -7542,7 +7623,7 @@ const updateAsset = async (id, asset, { spaceId, fileBuffer }) => {
7542
7623
  const createAsset = async (asset, fileBuffer, { spaceId }) => {
7543
7624
  try {
7544
7625
  const client = getMapiClient();
7545
- const { id: _id, ...assetBody } = asset;
7626
+ const { id: _id, filename: _filename, ...assetBody } = asset;
7546
7627
  return await client.assets.create({
7547
7628
  body: assetBody,
7548
7629
  file: fileBuffer,
@@ -7762,6 +7843,26 @@ const parseAssetData = (raw) => {
7762
7843
  throw new Error(`Invalid --data JSON: ${toError(maybeError).message}`);
7763
7844
  }
7764
7845
  };
7846
+ const toAssetUpload = (partial, shortFilename) => {
7847
+ const nullToUndef = (value) => value ?? void 0;
7848
+ return {
7849
+ id: partial.id,
7850
+ filename: partial.filename,
7851
+ short_filename: shortFilename,
7852
+ asset_folder_id: nullToUndef(partial.asset_folder_id),
7853
+ ext_id: nullToUndef(partial.ext_id),
7854
+ alt: nullToUndef(partial.alt),
7855
+ copyright: nullToUndef(partial.copyright),
7856
+ title: nullToUndef(partial.title),
7857
+ source: nullToUndef(partial.source),
7858
+ expire_at: nullToUndef(partial.expire_at),
7859
+ publish_at: nullToUndef(partial.publish_at),
7860
+ focus: nullToUndef(partial.focus),
7861
+ is_private: nullToUndef(partial.is_private),
7862
+ internal_tag_ids: partial.internal_tag_ids,
7863
+ meta_data: nullToUndef(partial.meta_data)
7864
+ };
7865
+ };
7765
7866
  const getSidecarFilename = (assetBinaryPath) => {
7766
7867
  return join(dirname(assetBinaryPath), `${basename(assetBinaryPath, extname(assetBinaryPath))}.json`);
7767
7868
  };
@@ -7790,7 +7891,7 @@ const isRemoteSource = (assetBinaryPath) => {
7790
7891
  return false;
7791
7892
  }
7792
7893
  };
7793
- const isValidManifestEntry = (entry) => Boolean(typeof entry.old_id === "number" && typeof entry.new_id === "number" && entry.old_filename && entry.new_filename);
7894
+ const isValidManifestEntry = (entry) => Boolean(typeof entry.old_id === "number" && typeof entry.new_id === "number" && entry.new_filename);
7794
7895
  const loadAssetMap = async (manifestFile) => {
7795
7896
  const manifest = await loadManifest(manifestFile);
7796
7897
  const entries = manifest.filter(isValidManifestEntry).map((e) => [
@@ -8219,8 +8320,10 @@ const readLocalAssetsStream = ({
8219
8320
  const sidecar = await loadSidecarAssetData(binaryFilePath);
8220
8321
  const shortFilename = sidecar.short_filename || (sidecar.filename ? basename(sidecar.filename) : void 0) || file;
8221
8322
  const asset = {
8222
- ...sidecar,
8223
- short_filename: shortFilename
8323
+ ...toAssetUpload(sidecar, shortFilename),
8324
+ // Carry the read-only tag detail for source→target tag-name
8325
+ // translation in `processAsset`; it is stripped before the API call.
8326
+ ...sidecar.internal_tags_list ? { internal_tags_list: sidecar.internal_tags_list } : {}
8224
8327
  };
8225
8328
  const fileBuffer = await readFile$1(binaryFilePath);
8226
8329
  const sidecarPath = getSidecarFilename(binaryFilePath);
@@ -8304,7 +8407,7 @@ const mapInternalTagIds = (sourceIds, sourceTags, assetInternalTagsByName, onUnm
8304
8407
  const sourceName = sourceNamesById.get(sourceId);
8305
8408
  const targetId = typeof sourceName === "string" ? assetInternalTagsByName.get(sourceName) : void 0;
8306
8409
  if (typeof targetId === "number") {
8307
- mapped.push(String(targetId));
8410
+ mapped.push(targetId);
8308
8411
  } else {
8309
8412
  onUnmappedTag?.({ sourceId, name: sourceName });
8310
8413
  }
@@ -8358,12 +8461,13 @@ const processAsset = async ({
8358
8461
  if (maps.resolveSharedTagIds) {
8359
8462
  return maps.resolveSharedTagIds(sourceIds, sourceTags);
8360
8463
  }
8361
- return maps.assetInternalTagsByName ? mapInternalTagIds(sourceIds, sourceTags, maps.assetInternalTagsByName, onUnmappedTag) : (sourceIds ?? []).map((id) => String(id));
8464
+ return maps.assetInternalTagsByName ? mapInternalTagIds(sourceIds, sourceTags, maps.assetInternalTagsByName, onUnmappedTag) : (sourceIds ?? []).map((id) => Number(id));
8362
8465
  };
8363
8466
  let newRemoteAsset;
8364
8467
  let status;
8365
8468
  if (remoteAsset) {
8366
- const updateTagIds = "internal_tag_ids" in localAsset ? await resolveInternalTagIds(localAsset.internal_tag_ids) : remoteAsset.internal_tag_ids;
8469
+ const updateTagIds = localAsset.internal_tag_ids ? await resolveInternalTagIds(localAsset.internal_tag_ids) : remoteAsset.internal_tag_ids;
8470
+ const nullToUndef = (v) => v ?? void 0;
8367
8471
  const updatePayload = {
8368
8472
  asset_folder_id: remoteFolderId,
8369
8473
  alt: "alt" in localAsset ? localAsset.alt : remoteAsset.alt,
@@ -8372,25 +8476,29 @@ const processAsset = async ({
8372
8476
  source: "source" in localAsset ? localAsset.source : remoteAsset.source,
8373
8477
  is_private: "is_private" in localAsset ? localAsset.is_private : remoteAsset.is_private,
8374
8478
  focus: "focus" in localAsset ? localAsset.focus : remoteAsset.focus,
8375
- expire_at: "expire_at" in localAsset ? localAsset.expire_at : remoteAsset.expire_at,
8376
- publish_at: "publish_at" in localAsset ? localAsset.publish_at : remoteAsset.publish_at,
8479
+ expire_at: nullToUndef("expire_at" in localAsset ? localAsset.expire_at : remoteAsset.expire_at),
8480
+ publish_at: nullToUndef("publish_at" in localAsset ? localAsset.publish_at : remoteAsset.publish_at),
8377
8481
  internal_tag_ids: updateTagIds,
8378
- meta_data: "meta_data" in localAsset ? localAsset.meta_data : remoteAsset.meta_data
8482
+ meta_data: nullToUndef("meta_data" in localAsset ? localAsset.meta_data : remoteAsset.meta_data)
8379
8483
  };
8380
8484
  await transports.updateAsset(
8381
8485
  remoteAsset.id,
8382
8486
  { ...updatePayload, short_filename: remoteAsset.short_filename },
8383
8487
  fileBuffer
8384
8488
  );
8385
- newRemoteAsset = { ...remoteAsset, ...updatePayload };
8489
+ newRemoteAsset = {
8490
+ ...remoteAsset,
8491
+ ...updatePayload,
8492
+ is_private: updatePayload.is_private ?? remoteAsset.is_private
8493
+ };
8386
8494
  status = "updated";
8387
8495
  } else if (hasProp(localAsset, "short_filename")) {
8388
- const { internal_tags_list: _internalTagsList, internal_tag_ids: _sourceTagIds, ...rest } = localAsset;
8389
- const mappedTagIds = "internal_tag_ids" in localAsset ? await resolveInternalTagIds(localAsset.internal_tag_ids) : void 0;
8390
- const size = hasProp(rest, "size") ? rest.size : hasProp(localAsset, "filename") ? extractAssetSizeFromFilename(localAsset.filename) : void 0;
8496
+ const mappedTagIds = localAsset.internal_tag_ids ? await resolveInternalTagIds(localAsset.internal_tag_ids) : void 0;
8497
+ const size = hasProp(localAsset, "size") ? localAsset.size : hasProp(localAsset, "filename") ? extractAssetSizeFromFilename(localAsset.filename) : void 0;
8498
+ const { internal_tag_ids: _sourceTagIds, ...uploadBase } = toAssetUpload(localAsset, localAsset.short_filename);
8391
8499
  const createPayload = {
8392
- ...rest,
8393
- asset_folder_id: remoteFolderId,
8500
+ ...uploadBase,
8501
+ asset_folder_id: remoteFolderId ?? void 0,
8394
8502
  ...mappedTagIds !== void 0 ? { internal_tag_ids: mappedTagIds } : {},
8395
8503
  ...size !== void 0 ? { size } : {}
8396
8504
  };
@@ -8506,7 +8614,7 @@ const makeSharedTagResolver = ({ spaceId, libraryId }) => {
8506
8614
  }
8507
8615
  }
8508
8616
  if (sharedId !== void 0) {
8509
- remapped.push(String(sharedId));
8617
+ remapped.push(sharedId);
8510
8618
  }
8511
8619
  }
8512
8620
  return remapped;
@@ -8916,8 +9024,8 @@ const traverseAndMapBySchema = (data, {
8916
9024
  const dataNew = { ...data };
8917
9025
  for (const [fieldName, fieldValue] of Object.entries(data)) {
8918
9026
  const fieldSchema = schema[fieldName.replace(/__i18n__.*/, "")];
8919
- const fieldType = fieldSchema && typeof fieldSchema === "object" && "type" in fieldSchema && fieldSchema.type;
8920
- const fieldRefMapper = typeof fieldType === "string" && fieldRefMappers2[fieldType];
9027
+ const fieldType = fieldSchema && typeof fieldSchema === "object" && "type" in fieldSchema ? fieldSchema.type : void 0;
9028
+ const fieldRefMapper = typeof fieldType === "string" ? fieldRefMappers2[fieldType] : void 0;
8921
9029
  if (fieldRefMapper) {
8922
9030
  dataNew[fieldName] = fieldRefMapper(fieldValue, {
8923
9031
  schema: fieldSchema,
@@ -8982,6 +9090,9 @@ const richtextFieldRefMapper = (data, { schemas, maps, fieldRefMappers: fieldRef
8982
9090
  fieldRefMappers: fieldRefMappers2
8983
9091
  });
8984
9092
  const multilinkFieldRefMapper = (data, { maps }) => {
9093
+ if (!data || typeof data !== "object") {
9094
+ return data;
9095
+ }
8985
9096
  if (data.linktype !== "story") {
8986
9097
  return data;
8987
9098
  }
@@ -9001,6 +9112,9 @@ const bloksFieldRefMapper = (data, { schemas, maps, fieldRefMappers: fieldRefMap
9001
9112
  }));
9002
9113
  };
9003
9114
  const assetFieldRefMapper = (data, { maps }) => {
9115
+ if (!data || typeof data !== "object") {
9116
+ return data;
9117
+ }
9004
9118
  const mappedAsset = typeof data.id === "number" ? maps.assets?.get(data.id) : void 0;
9005
9119
  if (!mappedAsset) {
9006
9120
  return data;
@@ -9018,7 +9132,7 @@ const multiassetFieldRefMapper = (data, options) => {
9018
9132
  return data.map((d) => assetFieldRefMapper(d, options));
9019
9133
  };
9020
9134
  const optionsFieldRefMapper = (data, { schema, maps }) => {
9021
- if (schema.source !== "internal_stories" || !Array.isArray(data)) {
9135
+ if (!schema || !("source" in schema) || schema.source !== "internal_stories" || !Array.isArray(data)) {
9022
9136
  return data;
9023
9137
  }
9024
9138
  return data.map((d) => maps.stories?.get(d) || d);
@@ -9911,11 +10025,7 @@ pushCmd$1.action(async (assetInput, options, command) => {
9911
10025
  const sourceBasename = isRemoteSource(assetBinaryPath) ? basename(new URL(assetBinaryPath).pathname) : basename(assetBinaryPath);
9912
10026
  const shortFilename = options.shortFilename || assetDataPartial.short_filename || sourceBasename;
9913
10027
  const folderId = options.folder ? Number(options.folder) : void 0;
9914
- assetData = {
9915
- ...assetDataPartial,
9916
- short_filename: shortFilename,
9917
- asset_folder_id: folderId
9918
- };
10028
+ assetData = { ...toAssetUpload(assetDataPartial, shortFilename), asset_folder_id: folderId };
9919
10029
  }
9920
10030
  try {
9921
10031
  for (const scope of scopes) {
@@ -10123,8 +10233,8 @@ transferCmd.action(async (assetIds, options, command) => {
10123
10233
  process.exitCode = summary.failed > 0 ? 1 : 0;
10124
10234
  });
10125
10235
 
10126
- const program$1 = getProgram();
10127
- const storiesCommand = program$1.command(commands.STORIES).description(`Manage your space's stories`);
10236
+ const program$2 = getProgram();
10237
+ const storiesCommand = program$2.command(commands.STORIES).description(`Manage your space's stories`);
10128
10238
 
10129
10239
  const pullCmd = storiesCommand.command("pull").option("-s, --space <space>", "space ID").option("-d, --dry-run", "Preview changes without applying them to Storyblok").option("-q, --query <query>", 'Filter stories by content attributes using Storyblok filter query syntax. Example: --query="[highlighted][in]=true"').option("--starts-with <path>", 'Filter stories by path. Example: --starts-with="/en/blog/"').description(`Download your space's stories as separate json files.`);
10130
10240
  pullCmd.action(async (options, command) => {
@@ -10160,7 +10270,7 @@ pullCmd.action(async (options, command) => {
10160
10270
  fetchStoriesStream({
10161
10271
  spaceId: space,
10162
10272
  params: {
10163
- filter_query: options.query,
10273
+ filter_query: options.query ? parseFilterQuery(options.query) : void 0,
10164
10274
  starts_with: options.startsWith
10165
10275
  },
10166
10276
  setTotalPages: (totalPages) => {
@@ -10629,6 +10739,2337 @@ pushCmd.action(async (options, command) => {
10629
10739
  }
10630
10740
  });
10631
10741
 
10742
+ const program$1 = getProgram();
10743
+ const schemaCommand = program$1.command(commands.SCHEMA).description(`Manage your space's schema from code`);
10744
+
10745
+ function isRecord(value) {
10746
+ return typeof value === "object" && value !== null && !Array.isArray(value);
10747
+ }
10748
+ const COMPONENT_STRIP_KEYS = /* @__PURE__ */ new Set([
10749
+ "id",
10750
+ "created_at",
10751
+ "updated_at",
10752
+ "real_name",
10753
+ // API-computed display/technical name, read-only
10754
+ "preset_id",
10755
+ // Instance-level preset selection, not part of schema definition
10756
+ "all_presets",
10757
+ // Computed list of presets, managed via /presets API
10758
+ "internal_tags_list",
10759
+ // Read-only expanded form of internal_tag_ids ({id, name} objects)
10760
+ "content_type_asset_preview",
10761
+ // Read-only, not in ComponentCreate/ComponentUpdate
10762
+ "image",
10763
+ // Read-only preview image URL
10764
+ "preview_tmpl",
10765
+ // Read-only preview template
10766
+ "metadata",
10767
+ // Not in current API types, stripped defensively
10768
+ "component_group_uuid"
10769
+ // UI grouping; stripped by default — kept for diffing only when a block opts into the group escape hatch
10770
+ ]);
10771
+ const DATASOURCE_STRIP_KEYS = /* @__PURE__ */ new Set(["id", "created_at", "updated_at"]);
10772
+ const DATASOURCE_DIMENSION_STRIP_KEYS = /* @__PURE__ */ new Set(["id", "datasource_id", "created_at", "updated_at"]);
10773
+ const DATASOURCE_DEFAULTS = {
10774
+ dimensions: []
10775
+ };
10776
+ const COMPONENT_DEFAULTS = {
10777
+ display_name: "",
10778
+ description: "",
10779
+ color: "",
10780
+ icon: "",
10781
+ preview_field: "",
10782
+ internal_tag_ids: []
10783
+ };
10784
+ function applyDefaults(entity, defaults) {
10785
+ const result = { ...entity };
10786
+ for (const [key, defaultValue] of Object.entries(defaults)) {
10787
+ if (result[key] === void 0 || result[key] === null) {
10788
+ Object.assign(result, { [key]: defaultValue });
10789
+ }
10790
+ }
10791
+ return result;
10792
+ }
10793
+ const INDENT = " ";
10794
+ class RawCode {
10795
+ constructor(code) {
10796
+ this.code = code;
10797
+ }
10798
+ }
10799
+ function quoteString(value) {
10800
+ const escaped = JSON.stringify(value).slice(1, -1).replace(/\\"/g, '"').replace(/'/g, "\\'");
10801
+ return `'${escaped}'`;
10802
+ }
10803
+ function formatValue(value, depth) {
10804
+ const indent = INDENT.repeat(depth);
10805
+ const innerIndent = INDENT.repeat(depth + 1);
10806
+ if (value === null || value === void 0) {
10807
+ return String(value);
10808
+ }
10809
+ if (value instanceof RawCode) {
10810
+ return value.code;
10811
+ }
10812
+ if (typeof value === "string") {
10813
+ return quoteString(value);
10814
+ }
10815
+ if (typeof value === "number" || typeof value === "boolean") {
10816
+ return String(value);
10817
+ }
10818
+ if (Array.isArray(value)) {
10819
+ if (value.length === 0) {
10820
+ return "[]";
10821
+ }
10822
+ const items = value.map((item) => `${innerIndent}${formatValue(item, depth + 1)},`);
10823
+ return `[
10824
+ ${items.join("\n")}
10825
+ ${indent}]`;
10826
+ }
10827
+ if (typeof value === "object") {
10828
+ const entries = Object.entries(value).filter(([, v]) => v !== void 0 && v !== null).sort(([a], [b]) => a.localeCompare(b));
10829
+ if (entries.length === 0) {
10830
+ return "{}";
10831
+ }
10832
+ const props = entries.map(
10833
+ ([key, val]) => `${innerIndent}${key}: ${formatValue(val, depth + 1)},`
10834
+ );
10835
+ return `{
10836
+ ${props.join("\n")}
10837
+ ${indent}}`;
10838
+ }
10839
+ return String(value);
10840
+ }
10841
+ function fileTimestamp(iso) {
10842
+ return iso.replace(/\D/g, "").slice(0, 14);
10843
+ }
10844
+ function displayPath(filePath, userPath) {
10845
+ return userPath && isAbsolute(userPath) ? filePath : relative(process.cwd(), filePath);
10846
+ }
10847
+ function stripKeys(obj, keysToStrip) {
10848
+ const result = {};
10849
+ for (const [key, value] of Object.entries(obj)) {
10850
+ if (!keysToStrip.has(key) && value !== void 0 && value !== null) {
10851
+ result[key] = value;
10852
+ }
10853
+ }
10854
+ return result;
10855
+ }
10856
+
10857
+ function buildGroupPathByUuid(folders) {
10858
+ const byUuid = new Map(folders.map((folder) => [folder.uuid, folder]));
10859
+ const cache = /* @__PURE__ */ new Map();
10860
+ function pathFor(uuid, visited) {
10861
+ if (!uuid) {
10862
+ return [];
10863
+ }
10864
+ const cached = cache.get(uuid);
10865
+ if (cached) {
10866
+ return cached;
10867
+ }
10868
+ const folder = byUuid.get(uuid);
10869
+ if (!folder) {
10870
+ return [];
10871
+ }
10872
+ if (visited.has(uuid)) {
10873
+ return [];
10874
+ }
10875
+ visited.add(uuid);
10876
+ const path = [...pathFor(folder.parent_uuid, visited), slugify(folder.name)];
10877
+ cache.set(uuid, path);
10878
+ return path;
10879
+ }
10880
+ for (const folder of folders) {
10881
+ pathFor(folder.uuid, /* @__PURE__ */ new Set());
10882
+ }
10883
+ return cache;
10884
+ }
10885
+ function slugifyPath(displayPath) {
10886
+ return displayPath.split("/").map((segment) => slugify(segment)).filter(Boolean).join("/");
10887
+ }
10888
+ function expandFolderPath(displayPath) {
10889
+ const segments = displayPath.split("/").map((segment) => ({ name: segment, slug: slugify(segment) })).filter((segment) => segment.slug !== "");
10890
+ const result = [];
10891
+ let parentPath = null;
10892
+ for (const { name, slug } of segments) {
10893
+ const path = parentPath ? `${parentPath}/${slug}` : slug;
10894
+ result.push({ name, path, parentPath });
10895
+ parentPath = path;
10896
+ }
10897
+ return result;
10898
+ }
10899
+
10900
+ function mapFieldToWire(field) {
10901
+ const { name, allow, datasource, ...rest } = field;
10902
+ const value = { ...rest };
10903
+ if (allow !== void 0 && Array.isArray(allow)) {
10904
+ const folderPaths = allow.filter((entry) => isRecord(entry) && typeof entry.folder === "string").map((entry) => slugifyPath(entry.folder));
10905
+ const blockNames = allow.filter((entry) => typeof entry === "string");
10906
+ if (folderPaths.length > 0) {
10907
+ value.component_group_whitelist = folderPaths;
10908
+ if (rest.type === "bloks" || rest.type === "richtext") {
10909
+ value.restrict_components = true;
10910
+ value.restrict_type = "groups";
10911
+ }
10912
+ } else {
10913
+ value.component_whitelist = blockNames;
10914
+ if (rest.type === "bloks") {
10915
+ value.restrict_components = true;
10916
+ value.restrict_type = "";
10917
+ }
10918
+ }
10919
+ } else if (allow !== void 0) {
10920
+ value.component_whitelist = allow;
10921
+ if (rest.type === "bloks") {
10922
+ value.restrict_components = true;
10923
+ value.restrict_type = "";
10924
+ }
10925
+ }
10926
+ if (datasource !== void 0) {
10927
+ value.datasource_slug = datasource;
10928
+ }
10929
+ return { name: typeof name === "string" ? name : "", value };
10930
+ }
10931
+ function mapBlockToWire(block) {
10932
+ const { fields, folder, ...rest } = block;
10933
+ const schema = {};
10934
+ if (Array.isArray(fields)) {
10935
+ for (const field of fields) {
10936
+ if (!isRecord(field)) {
10937
+ continue;
10938
+ }
10939
+ const { name, value } = mapFieldToWire(field);
10940
+ if (name) {
10941
+ schema[name] = value;
10942
+ }
10943
+ }
10944
+ }
10945
+ return {
10946
+ ...rest,
10947
+ ...folder !== void 0 && { folder: typeof folder === "string" ? slugifyPath(folder) : null },
10948
+ schema
10949
+ };
10950
+ }
10951
+ function mapDatasourceToWire(datasource) {
10952
+ return datasource;
10953
+ }
10954
+
10955
+ function isComponent(value) {
10956
+ return isRecord(value) && typeof value.name === "string" && Array.isArray(value.fields);
10957
+ }
10958
+ function isDatasource(value) {
10959
+ return isRecord(value) && typeof value.name === "string" && typeof value.slug === "string" && !Array.isArray(value.fields);
10960
+ }
10961
+ function isFolder(value) {
10962
+ return isRecord(value) && typeof value.name === "string" && typeof value.path === "string" && !Array.isArray(value.fields) && !("slug" in value);
10963
+ }
10964
+ function buildLocalFolders(registered, derived) {
10965
+ const byPath = /* @__PURE__ */ new Map();
10966
+ const add = (displayPath, registeredLeaf) => {
10967
+ const expanded = expandFolderPath(displayPath);
10968
+ expanded.forEach((entry, i) => {
10969
+ const isLeaf = i === expanded.length - 1;
10970
+ const isRegisteredEntry = registeredLeaf && isLeaf;
10971
+ const existing = byPath.get(entry.path);
10972
+ if (!existing) {
10973
+ byPath.set(entry.path, { folder: entry, isRegistered: isRegisteredEntry });
10974
+ return;
10975
+ }
10976
+ if (isRegisteredEntry && existing.isRegistered && existing.folder.name !== entry.name) {
10977
+ throw new CommandError(`Conflicting folder names for path "${entry.path}": "${existing.folder.name}" vs "${entry.name}"`);
10978
+ }
10979
+ if (isRegisteredEntry && !existing.isRegistered) {
10980
+ byPath.set(entry.path, { folder: entry, isRegistered: true });
10981
+ }
10982
+ });
10983
+ };
10984
+ for (const path of registered) {
10985
+ add(path, true);
10986
+ }
10987
+ for (const path of derived) {
10988
+ add(path, false);
10989
+ }
10990
+ const folders = [...byPath.values()].map((v) => v.folder).sort((a, b) => a.path.split("/").length - b.path.split("/").length || a.path.localeCompare(b.path));
10991
+ const leafToPath = /* @__PURE__ */ new Map();
10992
+ for (const folder of folders) {
10993
+ const leaf = folder.path.split("/").pop() ?? folder.path;
10994
+ const existing = leafToPath.get(leaf);
10995
+ if (existing !== void 0 && existing !== folder.path) {
10996
+ throw new CommandError(
10997
+ `Duplicate folder name "${leaf}" (folders "${existing}" and "${folder.path}"): Storyblok group names must be unique per space, even under different parents.`
10998
+ );
10999
+ }
11000
+ leafToPath.set(leaf, folder.path);
11001
+ }
11002
+ return folders;
11003
+ }
11004
+ function isSchemaObject(value) {
11005
+ return isRecord(value) && ("blocks" in value || "datasources" in value || "folders" in value);
11006
+ }
11007
+ function emptySchemaData() {
11008
+ return { components: [], folders: [], datasources: [] };
11009
+ }
11010
+ function classifyExports(moduleExports) {
11011
+ const data = emptySchemaData();
11012
+ const seenComponents = /* @__PURE__ */ new Set();
11013
+ const seenDatasources = /* @__PURE__ */ new Set();
11014
+ const registered = [];
11015
+ const derived = [];
11016
+ function harvestComponentPaths(value) {
11017
+ if (typeof value.folder === "string") {
11018
+ derived.push(value.folder);
11019
+ }
11020
+ if (Array.isArray(value.fields)) {
11021
+ for (const field of value.fields) {
11022
+ if (!isRecord(field) || !Array.isArray(field.allow)) {
11023
+ continue;
11024
+ }
11025
+ for (const entry of field.allow) {
11026
+ if (isRecord(entry) && typeof entry.folder === "string") {
11027
+ derived.push(entry.folder);
11028
+ }
11029
+ }
11030
+ }
11031
+ }
11032
+ }
11033
+ function collect(value) {
11034
+ if (isComponent(value)) {
11035
+ harvestComponentPaths(value);
11036
+ if (seenComponents.has(value.name)) {
11037
+ return;
11038
+ }
11039
+ seenComponents.add(value.name);
11040
+ data.components.push(mapBlockToWire(value));
11041
+ } else if (isFolder(value)) {
11042
+ registered.push(value.path);
11043
+ } else if (isDatasource(value)) {
11044
+ if (seenDatasources.has(value.name)) {
11045
+ return;
11046
+ }
11047
+ seenDatasources.add(value.name);
11048
+ data.datasources.push(mapDatasourceToWire(value));
11049
+ }
11050
+ }
11051
+ for (const value of Object.values(moduleExports)) {
11052
+ if (isSchemaObject(value)) {
11053
+ for (const group of Object.values(value)) {
11054
+ if (isRecord(group)) {
11055
+ for (const entity of Object.values(group)) {
11056
+ collect(entity);
11057
+ }
11058
+ }
11059
+ }
11060
+ } else {
11061
+ collect(value);
11062
+ }
11063
+ }
11064
+ data.folders = buildLocalFolders(registered, derived);
11065
+ return data;
11066
+ }
11067
+ async function loadSchema(entryPath) {
11068
+ const { createJiti } = await import('jiti');
11069
+ const jiti = createJiti(import.meta.url, { interopDefault: true });
11070
+ const { resolve } = await import('pathe');
11071
+ const entryAbs = resolve(entryPath);
11072
+ const entryMod = await jiti.import(entryAbs);
11073
+ return classifyExports(entryMod);
11074
+ }
11075
+
11076
+ function sortSchemaByPos$1(schema) {
11077
+ const entries = Object.entries(schema).filter(([key]) => key !== "_uid" && key !== "component").sort(([, a], [, b]) => {
11078
+ const posA = typeof a.pos === "number" ? a.pos : Infinity;
11079
+ const posB = typeof b.pos === "number" ? b.pos : Infinity;
11080
+ return posA - posB;
11081
+ });
11082
+ return Object.fromEntries(
11083
+ entries.map(([key, field]) => {
11084
+ const { id, ...rest } = field;
11085
+ if (rest.restrict_type === "") {
11086
+ delete rest.restrict_type;
11087
+ }
11088
+ return [key, rest];
11089
+ })
11090
+ );
11091
+ }
11092
+ const FOLDER_UNGROUPED = "__FOLDER_UNGROUPED__";
11093
+ function serializeComponent(component, options = {}) {
11094
+ const stripSet = options.includeGroupUuid ? new Set([...COMPONENT_STRIP_KEYS].filter((key) => key !== "component_group_uuid")) : COMPONENT_STRIP_KEYS;
11095
+ const clean = stripKeys(component, stripSet);
11096
+ if (clean.schema && typeof clean.schema === "object") {
11097
+ clean.schema = sortSchemaByPos$1(clean.schema);
11098
+ }
11099
+ const ordered = {};
11100
+ if (clean.name !== void 0) {
11101
+ ordered.name = clean.name;
11102
+ }
11103
+ if (clean.display_name !== void 0) {
11104
+ ordered.display_name = clean.display_name;
11105
+ }
11106
+ if (clean.is_root !== void 0) {
11107
+ ordered.is_root = clean.is_root;
11108
+ }
11109
+ if (clean.is_nestable !== void 0) {
11110
+ ordered.is_nestable = clean.is_nestable;
11111
+ }
11112
+ if ("folder" in component) {
11113
+ ordered.folder = component.folder === null ? FOLDER_UNGROUPED : component.folder;
11114
+ }
11115
+ const handled = /* @__PURE__ */ new Set(["name", "display_name", "is_root", "is_nestable", "folder", "schema"]);
11116
+ for (const [key, value] of Object.entries(clean).sort(([a], [b]) => a.localeCompare(b))) {
11117
+ if (!handled.has(key)) {
11118
+ ordered[key] = value;
11119
+ }
11120
+ }
11121
+ if (clean.schema !== void 0) {
11122
+ ordered.schema = clean.schema;
11123
+ }
11124
+ return `defineBlock(${formatValue(ordered, 0)})`.replace(`folder: '${FOLDER_UNGROUPED}'`, "folder: null");
11125
+ }
11126
+ function serializeDatasource(datasource) {
11127
+ const clean = stripKeys(datasource, DATASOURCE_STRIP_KEYS);
11128
+ if (Array.isArray(clean.dimensions)) {
11129
+ clean.dimensions = clean.dimensions.map(
11130
+ (dim) => typeof dim === "object" && dim !== null ? stripKeys(dim, DATASOURCE_DIMENSION_STRIP_KEYS) : dim
11131
+ );
11132
+ }
11133
+ const ordered = {};
11134
+ if (clean.name !== void 0) {
11135
+ ordered.name = clean.name;
11136
+ }
11137
+ if (clean.slug !== void 0) {
11138
+ ordered.slug = clean.slug;
11139
+ }
11140
+ const handled = /* @__PURE__ */ new Set(["name", "slug"]);
11141
+ for (const [key, value] of Object.entries(clean).sort(([a], [b]) => a.localeCompare(b))) {
11142
+ if (!handled.has(key)) {
11143
+ ordered[key] = value;
11144
+ }
11145
+ }
11146
+ return `defineDatasource(${formatValue(ordered, 0)})`;
11147
+ }
11148
+
11149
+ function translateGroupWhitelist(schema, uuidToPath) {
11150
+ if (!isRecord(schema)) {
11151
+ return schema;
11152
+ }
11153
+ const result = {};
11154
+ for (const [fieldName, field] of Object.entries(schema)) {
11155
+ if (isRecord(field) && Array.isArray(field.component_group_whitelist)) {
11156
+ result[fieldName] = {
11157
+ ...field,
11158
+ component_group_whitelist: field.component_group_whitelist.map((entry) => typeof entry === "string" ? uuidToPath.get(entry) ?? entry : entry)
11159
+ };
11160
+ } else {
11161
+ result[fieldName] = field;
11162
+ }
11163
+ }
11164
+ return result;
11165
+ }
11166
+ function diffEntity(type, name, localSerialized, remoteSerialized) {
11167
+ if (!remoteSerialized && localSerialized) {
11168
+ return { type, name, action: "create", diff: null, local: null, remote: null };
11169
+ }
11170
+ if (remoteSerialized && !localSerialized) {
11171
+ return { type, name, action: "stale", diff: null, local: null, remote: null };
11172
+ }
11173
+ if (localSerialized === remoteSerialized) {
11174
+ return { type, name, action: "unchanged", diff: null, local: null, remote: null };
11175
+ }
11176
+ const patch = createTwoFilesPatch(
11177
+ `remote/${name}`,
11178
+ `local/${name}`,
11179
+ remoteSerialized,
11180
+ localSerialized,
11181
+ "remote",
11182
+ "local"
11183
+ );
11184
+ return { type, name, action: "update", diff: patch, local: null, remote: null };
11185
+ }
11186
+ function diffSchema(local, remote) {
11187
+ const diffs = [];
11188
+ const groupPathByUuid = buildGroupPathByUuid([...remote.componentFolders.values()]);
11189
+ const uuidToPath = /* @__PURE__ */ new Map();
11190
+ for (const [uuid, segments] of groupPathByUuid) {
11191
+ uuidToPath.set(uuid, segments.join("/"));
11192
+ }
11193
+ const remoteFolderPaths = new Set(uuidToPath.values());
11194
+ const localFolderPaths = new Set(local.folders.map((f) => f.path));
11195
+ for (const folder of local.folders) {
11196
+ const action = remoteFolderPaths.has(folder.path) ? "unchanged" : "create";
11197
+ diffs.push({ type: "folder", name: folder.path, action, diff: null, local: null, remote: null });
11198
+ }
11199
+ for (const path of remoteFolderPaths) {
11200
+ if (!localFolderPaths.has(path)) {
11201
+ diffs.push({ type: "folder", name: path, action: "stale", diff: null, local: null, remote: null });
11202
+ }
11203
+ }
11204
+ const processedComponentNames = /* @__PURE__ */ new Set();
11205
+ for (const comp of local.components) {
11206
+ processedComponentNames.add(comp.name);
11207
+ const remoteComp = remote.components.get(comp.name);
11208
+ const includeGroupUuid = typeof comp.component_group_uuid === "string";
11209
+ const localForDiff = { ...comp };
11210
+ const remoteForDiff = remoteComp ? { ...remoteComp } : void 0;
11211
+ if ("folder" in comp) {
11212
+ if (remoteForDiff) {
11213
+ const uuid = remoteForDiff.component_group_uuid;
11214
+ remoteForDiff.folder = typeof uuid === "string" && uuid ? uuidToPath.get(uuid) ?? null : null;
11215
+ }
11216
+ } else {
11217
+ delete localForDiff.folder;
11218
+ if (remoteForDiff) {
11219
+ delete remoteForDiff.folder;
11220
+ }
11221
+ }
11222
+ localForDiff.schema = translateGroupWhitelist(localForDiff.schema, uuidToPath);
11223
+ if (remoteForDiff) {
11224
+ remoteForDiff.schema = translateGroupWhitelist(remoteForDiff.schema, uuidToPath);
11225
+ }
11226
+ const localSerialized = serializeComponent(applyDefaults(localForDiff, COMPONENT_DEFAULTS), { includeGroupUuid });
11227
+ const remoteSerialized = remoteForDiff ? serializeComponent(applyDefaults(remoteForDiff, COMPONENT_DEFAULTS), { includeGroupUuid }) : null;
11228
+ diffs.push(diffEntity("component", comp.name, localSerialized, remoteSerialized));
11229
+ }
11230
+ for (const [name] of remote.components) {
11231
+ if (!processedComponentNames.has(name)) {
11232
+ diffs.push(diffEntity("component", name, null, "stale"));
11233
+ }
11234
+ }
11235
+ const processedDatasourceNames = /* @__PURE__ */ new Set();
11236
+ for (const ds of local.datasources) {
11237
+ processedDatasourceNames.add(ds.name);
11238
+ const remoteDs = remote.datasources.get(ds.name);
11239
+ const localSerialized = serializeDatasource(applyDefaults(ds, DATASOURCE_DEFAULTS));
11240
+ const remoteSerialized = remoteDs ? serializeDatasource(applyDefaults(remoteDs, DATASOURCE_DEFAULTS)) : null;
11241
+ diffs.push(diffEntity("datasource", ds.name, localSerialized, remoteSerialized));
11242
+ }
11243
+ for (const [name] of remote.datasources) {
11244
+ if (!processedDatasourceNames.has(name)) {
11245
+ diffs.push(diffEntity("datasource", name, null, "stale"));
11246
+ }
11247
+ }
11248
+ return {
11249
+ diffs,
11250
+ creates: diffs.filter((d) => d.action === "create").length,
11251
+ updates: diffs.filter((d) => d.action === "update").length,
11252
+ unchanged: diffs.filter((d) => d.action === "unchanged").length,
11253
+ stale: diffs.filter((d) => d.action === "stale").length
11254
+ };
11255
+ }
11256
+
11257
+ async function fetchRemoteSchema(spaceId) {
11258
+ const client = getMapiClient();
11259
+ const spaceIdNum = Number(spaceId);
11260
+ const [componentsRes, foldersRes, rawDatasources] = await Promise.all([
11261
+ client.components.list({ path: { space_id: spaceIdNum }, throwOnError: true }),
11262
+ client.componentFolders.list({ path: { space_id: spaceIdNum }, throwOnError: true }),
11263
+ fetchAllPages(
11264
+ (page) => client.datasources.list({ path: { space_id: spaceIdNum }, query: { page }, throwOnError: true }),
11265
+ (data) => data?.datasources ?? []
11266
+ )
11267
+ ]);
11268
+ const rawComponents = componentsRes.data?.components ?? [];
11269
+ const rawComponentFolders = foldersRes.data?.component_groups ?? [];
11270
+ const remote = {
11271
+ components: new Map(rawComponents.map((c) => [c.name, c])),
11272
+ componentFolders: new Map(rawComponentFolders.map((f) => [f.name, f])),
11273
+ datasources: new Map(rawDatasources.map((d) => [d.name, d]))
11274
+ };
11275
+ return { remote, rawComponents, rawComponentFolders, rawDatasources };
11276
+ }
11277
+
11278
+ function isSchemaField(value) {
11279
+ return isRecord(value) && "type" in value;
11280
+ }
11281
+ function toSchemaRecord(schema) {
11282
+ const result = {};
11283
+ for (const [key, value] of Object.entries(schema)) {
11284
+ if (key === "_uid" || key === "component" || !isSchemaField(value)) {
11285
+ continue;
11286
+ }
11287
+ result[key] = value;
11288
+ }
11289
+ return result;
11290
+ }
11291
+ function buildComponentPayload(input) {
11292
+ if (!isRecord(input)) {
11293
+ return { name: "" };
11294
+ }
11295
+ return {
11296
+ name: typeof input.name === "string" ? input.name : "",
11297
+ // Fields in COMPONENT_DEFAULTS are always sent with their reset value so that
11298
+ // removing a field from the local schema actually clears it on the API.
11299
+ // (Root-level fields are additive on MAPI update — omitting preserves the old value.)
11300
+ display_name: typeof input.display_name === "string" ? input.display_name : "",
11301
+ description: typeof input.description === "string" ? input.description : "",
11302
+ color: typeof input.color === "string" ? input.color : "",
11303
+ icon: typeof input.icon === "string" ? input.icon : "",
11304
+ preview_field: typeof input.preview_field === "string" ? input.preview_field : "",
11305
+ internal_tag_ids: Array.isArray(input.internal_tag_ids) ? input.internal_tag_ids : [],
11306
+ // Conditionally sent: only included when explicitly set in local schema
11307
+ ...isRecord(input.schema) && { schema: toSchemaRecord(input.schema) },
11308
+ ...typeof input.is_root === "boolean" && { is_root: input.is_root },
11309
+ ...typeof input.is_nestable === "boolean" && { is_nestable: input.is_nestable },
11310
+ // Forward the group membership only when the key is present on input: a
11311
+ // string sets the group, an explicit `null` clears it. Unmanaged components
11312
+ // (key absent) omit it so their remote group is left untouched.
11313
+ ..."component_group_uuid" in input && (typeof input.component_group_uuid === "string" || input.component_group_uuid === null) && { component_group_uuid: input.component_group_uuid }
11314
+ };
11315
+ }
11316
+ function toComponentCreate(input) {
11317
+ return buildComponentPayload(input);
11318
+ }
11319
+ function toComponentUpdate(input) {
11320
+ return buildComponentPayload(input);
11321
+ }
11322
+ function toDatasourceCreate(input) {
11323
+ if (!isRecord(input)) {
11324
+ return { name: "", slug: "" };
11325
+ }
11326
+ const result = {
11327
+ name: typeof input.name === "string" ? input.name : "",
11328
+ slug: typeof input.slug === "string" ? input.slug : ""
11329
+ };
11330
+ if (Array.isArray(input.dimensions)) {
11331
+ result.dimensions_attributes = input.dimensions.filter((d) => isRecord(d) && typeof d.name === "string" && typeof d.entry_value === "string").map((d) => ({
11332
+ name: d.name,
11333
+ entry_value: d.entry_value
11334
+ }));
11335
+ }
11336
+ return result;
11337
+ }
11338
+ function toDatasourceUpdate(input, remote) {
11339
+ const base = toDatasourceCreate(input);
11340
+ const localDims = base.dimensions_attributes ?? [];
11341
+ const remoteDims = remote.dimensions ?? [];
11342
+ if (remoteDims.length === 0) {
11343
+ return base;
11344
+ }
11345
+ const localKeys = new Set(localDims.map((d) => `${d.name}::${d.entry_value}`));
11346
+ const destroyEntries = remoteDims.filter((rd) => rd.id != null && !localKeys.has(`${rd.name}::${rd.entry_value}`)).map((rd) => ({ id: rd.id, _destroy: true }));
11347
+ if (destroyEntries.length > 0) {
11348
+ return {
11349
+ ...base,
11350
+ dimensions_attributes: [...localDims, ...destroyEntries]
11351
+ };
11352
+ }
11353
+ return base;
11354
+ }
11355
+
11356
+ function resolveGroupRefs(comp, groupByPath) {
11357
+ const { folder, ...rest } = comp;
11358
+ const resolved = { ...rest };
11359
+ if (typeof folder === "string") {
11360
+ const group = groupByPath.get(folder);
11361
+ if (!group) {
11362
+ throw new CommandError(
11363
+ `Unknown folder path "${folder}" for component "${comp.name}": no matching component group exists. Folder creation runs before component push, so this indicates an internal inconsistency.`
11364
+ );
11365
+ }
11366
+ resolved.component_group_uuid = group.uuid;
11367
+ } else if (folder === null) {
11368
+ resolved.component_group_uuid = null;
11369
+ }
11370
+ if (isRecord(resolved.schema)) {
11371
+ const schema = {};
11372
+ for (const [key, field] of Object.entries(resolved.schema)) {
11373
+ if (isRecord(field) && Array.isArray(field.component_group_whitelist)) {
11374
+ schema[key] = {
11375
+ ...field,
11376
+ component_group_whitelist: field.component_group_whitelist.map((path) => typeof path === "string" ? groupByPath.get(path)?.uuid ?? path : path)
11377
+ };
11378
+ } else {
11379
+ schema[key] = field;
11380
+ }
11381
+ }
11382
+ resolved.schema = schema;
11383
+ }
11384
+ return resolved;
11385
+ }
11386
+ function buildGroupByPath(remote) {
11387
+ const remoteFolders = [...remote.componentFolders.values()];
11388
+ const groupPathByUuid = buildGroupPathByUuid(remoteFolders);
11389
+ const groupByPath = /* @__PURE__ */ new Map();
11390
+ for (const folder of remoteFolders) {
11391
+ const segments = groupPathByUuid.get(folder.uuid);
11392
+ if (segments?.length) {
11393
+ groupByPath.set(segments.join("/"), { id: folder.id, uuid: folder.uuid });
11394
+ }
11395
+ }
11396
+ return groupByPath;
11397
+ }
11398
+ function formatDiffOutput(result, options) {
11399
+ const lines = [];
11400
+ const byType = {
11401
+ component: [],
11402
+ datasource: [],
11403
+ folder: []
11404
+ };
11405
+ for (const diff of result.diffs) {
11406
+ byType[diff.type].push(diff);
11407
+ }
11408
+ const willDelete = options?.delete ?? false;
11409
+ const icons = {
11410
+ create: chalk.green("+"),
11411
+ update: chalk.yellow("~"),
11412
+ unchanged: chalk.dim("="),
11413
+ stale: chalk.red("-")
11414
+ };
11415
+ const sections = [
11416
+ ["Folders", byType.folder],
11417
+ ["Components", byType.component],
11418
+ ["Datasources", byType.datasource]
11419
+ ];
11420
+ for (const [label, diffs] of sections) {
11421
+ if (diffs.length === 0) {
11422
+ continue;
11423
+ }
11424
+ lines.push(chalk.bold(label));
11425
+ for (const diff of diffs) {
11426
+ const icon = icons[diff.action] ?? " ";
11427
+ const name = diff.action === "stale" ? chalk.red(diff.name) : diff.name;
11428
+ const actionLabel = diff.action === "stale" && willDelete ? "delete" : diff.action;
11429
+ lines.push(` ${icon} ${name} ${chalk.dim(`(${actionLabel})`)}`);
11430
+ if (diff.diff) {
11431
+ for (const line of diff.diff.split("\n")) {
11432
+ if (line.startsWith("+") && !line.startsWith("+++")) {
11433
+ lines.push(` ${chalk.green(line)}`);
11434
+ } else if (line.startsWith("-") && !line.startsWith("---")) {
11435
+ lines.push(` ${chalk.red(line)}`);
11436
+ }
11437
+ }
11438
+ }
11439
+ }
11440
+ lines.push("");
11441
+ }
11442
+ const summary = [
11443
+ result.creates > 0 ? chalk.green(`${result.creates} to create`) : null,
11444
+ result.updates > 0 ? chalk.yellow(`${result.updates} to update`) : null,
11445
+ result.unchanged > 0 ? chalk.dim(`${result.unchanged} unchanged`) : null,
11446
+ result.stale > 0 ? chalk.red(`${result.stale} ${willDelete ? "to delete" : "stale"}`) : null
11447
+ ].filter(Boolean).join(", ");
11448
+ lines.push(`Summary: ${summary}`);
11449
+ return lines.join("\n");
11450
+ }
11451
+ async function executePush(spaceId, local, remote, diffResult, options) {
11452
+ const client = getMapiClient();
11453
+ const spaceIdNum = Number(spaceId);
11454
+ let created = 0;
11455
+ let updated = 0;
11456
+ let deleted = 0;
11457
+ const groupByPath = buildGroupByPath(remote);
11458
+ const foldersByPath = new Map(local.folders.map((f) => [f.path, f]));
11459
+ const folderCreates = diffResult.diffs.filter((d) => d.type === "folder" && d.action === "create").sort((a, b) => a.name.split("/").length - b.name.split("/").length);
11460
+ for (const diff of folderCreates) {
11461
+ const localFolder = foldersByPath.get(diff.name);
11462
+ if (!localFolder) {
11463
+ continue;
11464
+ }
11465
+ const parent = localFolder.parentPath ? groupByPath.get(localFolder.parentPath) : void 0;
11466
+ let createdGroup;
11467
+ try {
11468
+ const res = await client.componentFolders.create({
11469
+ path: { space_id: spaceIdNum },
11470
+ body: { component_group: { name: localFolder.name, parent_id: parent?.id ?? null } },
11471
+ throwOnError: true
11472
+ });
11473
+ createdGroup = res.data?.component_group;
11474
+ } catch (error) {
11475
+ handleAPIError("push_component_folder", error, `Failed to create folder ${localFolder.name}`);
11476
+ }
11477
+ if (createdGroup?.id == null || !createdGroup.uuid) {
11478
+ throw new CommandError(
11479
+ `Folder "${localFolder.name}" was created but the Management API response did not include an id and uuid; blocks that reference this folder cannot be resolved.`
11480
+ );
11481
+ }
11482
+ groupByPath.set(localFolder.path, { id: createdGroup.id, uuid: createdGroup.uuid });
11483
+ created++;
11484
+ }
11485
+ const componentDiffs = diffResult.diffs.filter((d) => d.type === "component");
11486
+ const resolvedComponents = /* @__PURE__ */ new Map();
11487
+ for (const diff of componentDiffs) {
11488
+ if (diff.action !== "create" && diff.action !== "update") {
11489
+ continue;
11490
+ }
11491
+ const localComp = local.components.find((c) => c.name === diff.name);
11492
+ if (localComp) {
11493
+ resolvedComponents.set(diff.name, resolveGroupRefs(localComp, groupByPath));
11494
+ }
11495
+ }
11496
+ const componentResults = await Promise.allSettled(
11497
+ componentDiffs.map(async (diff) => {
11498
+ const resolvedComp = resolvedComponents.get(diff.name);
11499
+ if (diff.action === "create" && resolvedComp) {
11500
+ await client.components.create({
11501
+ path: { space_id: spaceIdNum },
11502
+ body: { component: toComponentCreate(resolvedComp) },
11503
+ throwOnError: true
11504
+ });
11505
+ return "created";
11506
+ }
11507
+ if (diff.action === "update" && resolvedComp) {
11508
+ const existing = remote.components.get(diff.name);
11509
+ if (existing?.id) {
11510
+ await client.components.update(existing.id, {
11511
+ path: { space_id: spaceIdNum },
11512
+ body: { component: toComponentUpdate(resolvedComp) },
11513
+ throwOnError: true
11514
+ });
11515
+ return "updated";
11516
+ }
11517
+ }
11518
+ })
11519
+ );
11520
+ for (let i = 0; i < componentResults.length; i++) {
11521
+ const result = componentResults[i];
11522
+ const diff = componentDiffs[i];
11523
+ if (result.status === "fulfilled") {
11524
+ if (result.value === "created") {
11525
+ created++;
11526
+ } else if (result.value === "updated") {
11527
+ updated++;
11528
+ }
11529
+ } else {
11530
+ const eventId = diff.action === "create" ? "push_component" : "update_component";
11531
+ handleAPIError(eventId, result.reason, `Failed to ${diff.action} component ${diff.name}`);
11532
+ }
11533
+ }
11534
+ const datasourceDiffs = diffResult.diffs.filter((d) => d.type === "datasource");
11535
+ const datasourceResults = await Promise.allSettled(
11536
+ datasourceDiffs.map(async (diff) => {
11537
+ const localDs = local.datasources.find((d) => d.name === diff.name);
11538
+ if (diff.action === "create" && localDs) {
11539
+ await client.datasources.create({
11540
+ path: { space_id: spaceIdNum },
11541
+ body: { datasource: toDatasourceCreate(localDs) },
11542
+ throwOnError: true
11543
+ });
11544
+ return "created";
11545
+ }
11546
+ if (diff.action === "update" && localDs) {
11547
+ const existing = remote.datasources.get(diff.name);
11548
+ if (existing?.id) {
11549
+ await client.datasources.update(existing.id, {
11550
+ path: { space_id: spaceIdNum },
11551
+ body: { datasource: toDatasourceUpdate(localDs, existing) },
11552
+ throwOnError: true
11553
+ });
11554
+ return "updated";
11555
+ }
11556
+ }
11557
+ })
11558
+ );
11559
+ for (let i = 0; i < datasourceResults.length; i++) {
11560
+ const result = datasourceResults[i];
11561
+ const diff = datasourceDiffs[i];
11562
+ if (result.status === "fulfilled") {
11563
+ if (result.value === "created") {
11564
+ created++;
11565
+ } else if (result.value === "updated") {
11566
+ updated++;
11567
+ }
11568
+ } else {
11569
+ const eventId = diff.action === "create" ? "push_datasource" : "update_datasource";
11570
+ handleAPIError(eventId, result.reason, `Failed to ${diff.action} datasource ${diff.name}`);
11571
+ }
11572
+ }
11573
+ if (options.delete) {
11574
+ const deleteErrors = [];
11575
+ const staleComponents = diffResult.diffs.filter((d) => d.type === "component" && d.action === "stale");
11576
+ const deleteComponentResults = await Promise.allSettled(
11577
+ staleComponents.map(async (diff) => {
11578
+ const existing = remote.components.get(diff.name);
11579
+ if (existing?.id) {
11580
+ await client.components.delete(existing.id, {
11581
+ path: { space_id: spaceIdNum },
11582
+ throwOnError: true
11583
+ });
11584
+ return true;
11585
+ }
11586
+ })
11587
+ );
11588
+ for (let i = 0; i < deleteComponentResults.length; i++) {
11589
+ const result = deleteComponentResults[i];
11590
+ if (result.status === "fulfilled") {
11591
+ if (result.value) {
11592
+ deleted++;
11593
+ }
11594
+ } else {
11595
+ deleteErrors.push({ action: "delete_component", reason: result.reason, message: `Failed to delete component ${staleComponents[i].name}` });
11596
+ }
11597
+ }
11598
+ const staleDatasources = diffResult.diffs.filter((d) => d.type === "datasource" && d.action === "stale");
11599
+ const deleteDatasourceResults = await Promise.allSettled(
11600
+ staleDatasources.map(async (diff) => {
11601
+ const existing = remote.datasources.get(diff.name);
11602
+ if (existing?.id) {
11603
+ await client.datasources.delete(existing.id, {
11604
+ path: { space_id: spaceIdNum },
11605
+ throwOnError: true
11606
+ });
11607
+ return true;
11608
+ }
11609
+ })
11610
+ );
11611
+ for (let i = 0; i < deleteDatasourceResults.length; i++) {
11612
+ const result = deleteDatasourceResults[i];
11613
+ if (result.status === "fulfilled") {
11614
+ if (result.value) {
11615
+ deleted++;
11616
+ }
11617
+ } else {
11618
+ deleteErrors.push({ action: "delete_datasource", reason: result.reason, message: `Failed to delete datasource ${staleDatasources[i].name}` });
11619
+ }
11620
+ }
11621
+ const staleFolders = diffResult.diffs.filter((d) => d.type === "folder" && d.action === "stale").sort((a, b) => b.name.split("/").length - a.name.split("/").length);
11622
+ for (const diff of staleFolders) {
11623
+ const group = groupByPath.get(diff.name);
11624
+ if (!group) {
11625
+ deleteErrors.push({
11626
+ action: "delete_component_folder",
11627
+ reason: new Error(`No component group resolved for stale folder path "${diff.name}".`),
11628
+ message: `Failed to delete folder ${diff.name}`
11629
+ });
11630
+ continue;
11631
+ }
11632
+ try {
11633
+ await client.componentFolders.delete(group.id, {
11634
+ path: { space_id: spaceIdNum },
11635
+ throwOnError: true
11636
+ });
11637
+ deleted++;
11638
+ } catch (error) {
11639
+ deleteErrors.push({ action: "delete_component_folder", reason: error, message: `Failed to delete folder ${diff.name}` });
11640
+ }
11641
+ }
11642
+ if (deleteErrors.length > 0) {
11643
+ const first = deleteErrors[0];
11644
+ const suffix = deleteErrors.length > 1 ? ` (and ${deleteErrors.length - 1} more delete error(s))` : "";
11645
+ handleAPIError(first.action, first.reason, `${first.message}${suffix}`);
11646
+ }
11647
+ }
11648
+ return { created, updated, deleted };
11649
+ }
11650
+ function buildChangesetEntries(diffResult, local, remote, options) {
11651
+ const changes = [];
11652
+ const remoteFolders = [...remote.componentFolders.values()];
11653
+ const folderPathByUuid = buildGroupPathByUuid(remoteFolders);
11654
+ const remoteFolderByPath = /* @__PURE__ */ new Map();
11655
+ for (const folder of remoteFolders) {
11656
+ const segments = folderPathByUuid.get(folder.uuid);
11657
+ if (segments?.length) {
11658
+ remoteFolderByPath.set(segments.join("/"), folder);
11659
+ }
11660
+ }
11661
+ for (const diff of diffResult.diffs) {
11662
+ if (diff.action === "unchanged") {
11663
+ continue;
11664
+ }
11665
+ if (diff.action === "stale" && !options.delete) {
11666
+ continue;
11667
+ }
11668
+ const action = diff.action === "stale" ? "delete" : diff.action;
11669
+ let remoteSrc;
11670
+ let localSrc;
11671
+ if (diff.type === "component") {
11672
+ remoteSrc = remote.components.get(diff.name);
11673
+ localSrc = local.components.find((c) => c.name === diff.name);
11674
+ } else if (diff.type === "datasource") {
11675
+ remoteSrc = remote.datasources.get(diff.name);
11676
+ localSrc = local.datasources.find((d) => d.name === diff.name);
11677
+ } else if (diff.type === "folder") {
11678
+ remoteSrc = remoteFolderByPath.get(diff.name);
11679
+ localSrc = local.folders.find((f) => f.path === diff.name);
11680
+ }
11681
+ changes.push({
11682
+ type: diff.type,
11683
+ name: diff.name,
11684
+ action,
11685
+ ...remoteSrc && { before: { ...remoteSrc } },
11686
+ ...localSrc && { after: { ...localSrc } }
11687
+ });
11688
+ }
11689
+ return changes;
11690
+ }
11691
+
11692
+ async function ensureDir(dir) {
11693
+ await mkdir(dir, { recursive: true });
11694
+ }
11695
+ async function saveChangeset(basePath, data) {
11696
+ const dir = join(basePath, "schema", "changesets");
11697
+ await ensureDir(dir);
11698
+ const fileName = `${fileTimestamp(data.timestamp)}.json`;
11699
+ const filePath = join(dir, fileName);
11700
+ await writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
11701
+ return filePath;
11702
+ }
11703
+
11704
+ const SENTINEL_FIELDS = /* @__PURE__ */ new Set(["_uid", "component"]);
11705
+ function classifyFieldChanges(remoteSchema, localSchema) {
11706
+ const removed = [];
11707
+ const added = [];
11708
+ const typeChanged = [];
11709
+ const requiredAdded = [];
11710
+ const requiredChanged = [];
11711
+ for (const [field, remoteField] of Object.entries(remoteSchema)) {
11712
+ if (SENTINEL_FIELDS.has(field)) {
11713
+ continue;
11714
+ }
11715
+ if (typeof remoteField.type !== "string") {
11716
+ continue;
11717
+ }
11718
+ if (!(field in localSchema)) {
11719
+ removed.push({ field, type: remoteField.type });
11720
+ }
11721
+ }
11722
+ for (const [field, localField] of Object.entries(localSchema)) {
11723
+ if (SENTINEL_FIELDS.has(field)) {
11724
+ continue;
11725
+ }
11726
+ if (typeof localField.type !== "string") {
11727
+ continue;
11728
+ }
11729
+ if (!(field in remoteSchema)) {
11730
+ if (localField.required) {
11731
+ requiredAdded.push({ field, type: localField.type });
11732
+ } else {
11733
+ added.push({ field, type: localField.type, required: false });
11734
+ }
11735
+ } else {
11736
+ const remoteField = remoteSchema[field];
11737
+ if (typeof remoteField?.type !== "string") {
11738
+ continue;
11739
+ }
11740
+ if (remoteField.type !== localField.type) {
11741
+ typeChanged.push({ field, oldType: remoteField.type, newType: localField.type });
11742
+ }
11743
+ if (localField.required && !remoteField.required) {
11744
+ requiredChanged.push({ field, type: localField.type });
11745
+ }
11746
+ }
11747
+ }
11748
+ return { removed, added, typeChanged, requiredAdded, requiredChanged };
11749
+ }
11750
+ function longestCommonSubstring(a, b) {
11751
+ let maxLen = 0;
11752
+ for (let i = 0; i < a.length; i++) {
11753
+ for (let j = 0; j < b.length; j++) {
11754
+ let len = 0;
11755
+ while (i + len < a.length && j + len < b.length && a[i + len] === b[j + len]) {
11756
+ len++;
11757
+ }
11758
+ if (len > maxLen) {
11759
+ maxLen = len;
11760
+ }
11761
+ }
11762
+ }
11763
+ return maxLen;
11764
+ }
11765
+ function nameSimilarity(a, b) {
11766
+ const longer = Math.max(a.length, b.length);
11767
+ if (longer === 0) {
11768
+ return 1;
11769
+ }
11770
+ return longestCommonSubstring(a, b) / longer;
11771
+ }
11772
+ function detectRenames(removed, added) {
11773
+ const renames = [];
11774
+ const usedRemoved = /* @__PURE__ */ new Set();
11775
+ const usedAdded = /* @__PURE__ */ new Set();
11776
+ const addedByType = /* @__PURE__ */ new Map();
11777
+ for (const addedField of added) {
11778
+ if (!addedByType.has(addedField.type)) {
11779
+ addedByType.set(addedField.type, []);
11780
+ }
11781
+ addedByType.get(addedField.type).push(addedField);
11782
+ }
11783
+ const isSinglePair = removed.length === 1 && added.length === 1;
11784
+ for (const removedField of removed) {
11785
+ const candidates = addedByType.get(removedField.type) ?? [];
11786
+ const availableCandidates = candidates.filter((c) => !usedAdded.has(c.field));
11787
+ if (availableCandidates.length === 0) {
11788
+ continue;
11789
+ }
11790
+ let bestCandidate = availableCandidates[0];
11791
+ let bestScore = nameSimilarity(removedField.field, bestCandidate.field);
11792
+ for (let i = 1; i < availableCandidates.length; i++) {
11793
+ const score = nameSimilarity(removedField.field, availableCandidates[i].field);
11794
+ if (score > bestScore) {
11795
+ bestScore = score;
11796
+ bestCandidate = availableCandidates[i];
11797
+ }
11798
+ }
11799
+ if (!isSinglePair && bestScore < 0.3) {
11800
+ continue;
11801
+ }
11802
+ renames.push({ oldField: removedField.field, newField: bestCandidate.field, fieldType: removedField.type });
11803
+ usedRemoved.add(removedField.field);
11804
+ usedAdded.add(bestCandidate.field);
11805
+ }
11806
+ const unmatchedRemoved = removed.filter((r) => !usedRemoved.has(r.field));
11807
+ const unmatchedAdded = added.filter((a) => !usedAdded.has(a.field));
11808
+ return { renames, unmatchedRemoved, unmatchedAdded };
11809
+ }
11810
+ function analyzeBreakingChanges(diffResult, local, remote) {
11811
+ const results = [];
11812
+ const updatedComponents = diffResult.diffs.filter(
11813
+ (d) => d.type === "component" && d.action === "update"
11814
+ );
11815
+ for (const diff of updatedComponents) {
11816
+ const localComp = local.components.find((c) => c.name === diff.name);
11817
+ const remoteComp = remote.components.get(diff.name);
11818
+ if (!localComp?.schema || !remoteComp?.schema) {
11819
+ continue;
11820
+ }
11821
+ const classification = classifyFieldChanges(
11822
+ remoteComp.schema,
11823
+ localComp.schema
11824
+ );
11825
+ const changes = [];
11826
+ const { renames, unmatchedRemoved } = detectRenames(classification.removed, classification.added);
11827
+ for (const rename of renames) {
11828
+ changes.push({ kind: "rename", field: rename.newField, oldField: rename.oldField });
11829
+ }
11830
+ for (const removed of unmatchedRemoved) {
11831
+ changes.push({ kind: "removed", field: removed.field });
11832
+ }
11833
+ for (const tc of classification.typeChanged) {
11834
+ changes.push({ kind: "type_changed", field: tc.field, oldType: tc.oldType, newType: tc.newType });
11835
+ }
11836
+ for (const ra of classification.requiredAdded) {
11837
+ changes.push({ kind: "required_added", field: ra.field, fieldType: ra.type });
11838
+ }
11839
+ for (const rc of classification.requiredChanged) {
11840
+ changes.push({ kind: "required_changed", field: rc.field, fieldType: rc.type });
11841
+ }
11842
+ if (changes.length > 0) {
11843
+ results.push({ componentName: diff.name, changes });
11844
+ }
11845
+ }
11846
+ return results;
11847
+ }
11848
+
11849
+ const COMPATIBLE_TYPES = /* @__PURE__ */ new Set(["text:textarea", "textarea:text"]);
11850
+ function defaultForType(fieldType) {
11851
+ switch (fieldType) {
11852
+ case "text":
11853
+ case "textarea":
11854
+ case "markdown":
11855
+ return `''`;
11856
+ case "number":
11857
+ return "0";
11858
+ case "boolean":
11859
+ return "false";
11860
+ default:
11861
+ return null;
11862
+ }
11863
+ }
11864
+ function typeConversion(field, oldType, newType) {
11865
+ const key = `${oldType}:${newType}`;
11866
+ if (COMPATIBLE_TYPES.has(key)) {
11867
+ return null;
11868
+ }
11869
+ const accessor = `block.${field}`;
11870
+ switch (key) {
11871
+ case "text:number":
11872
+ return `${accessor} = Number(${accessor}) || 0;`;
11873
+ case "number:text":
11874
+ return `${accessor} = String(${accessor});`;
11875
+ case "text:boolean":
11876
+ return `${accessor} = !!${accessor};`;
11877
+ case "boolean:text":
11878
+ return `${accessor} = String(${accessor});`;
11879
+ default:
11880
+ return `${accessor}; // TODO: convert from ${oldType} to ${newType}`;
11881
+ }
11882
+ }
11883
+ function renderMigrationCode(changes) {
11884
+ const lines = [];
11885
+ lines.push(" // Review this migration before running it against your space.");
11886
+ lines.push(" // Generated migrations are scaffolds and may need manual adjustments.");
11887
+ lines.push(" // Example rename migration:");
11888
+ lines.push(" // block.new_field = block.old_field;");
11889
+ lines.push(" // delete block.old_field;");
11890
+ lines.push("");
11891
+ for (const change of changes) {
11892
+ switch (change.kind) {
11893
+ case "rename":
11894
+ lines.push(` // Rename: ${change.oldField} \u2192 ${change.field}`);
11895
+ lines.push(` if ('${change.oldField}' in block) {`);
11896
+ lines.push(` block.${change.field} = block.${change.oldField};`);
11897
+ lines.push(` delete block.${change.oldField};`);
11898
+ lines.push(` }`);
11899
+ break;
11900
+ case "removed":
11901
+ if (change.renameHint) {
11902
+ lines.push(` // If '${change.field}' was renamed to '${change.renameHint.newField}', uncomment:`);
11903
+ lines.push(` // block.${change.renameHint.newField} = block.${change.field};`);
11904
+ } else {
11905
+ lines.push(` // Removed field: ${change.field}`);
11906
+ }
11907
+ lines.push(` delete block.${change.field};`);
11908
+ break;
11909
+ case "type_changed": {
11910
+ const conversion = typeConversion(change.field, change.oldType, change.newType);
11911
+ if (conversion) {
11912
+ lines.push(` // Type change: ${change.field} (${change.oldType} \u2192 ${change.newType})`);
11913
+ lines.push(` ${conversion}`);
11914
+ }
11915
+ break;
11916
+ }
11917
+ case "required_added": {
11918
+ const defaultValue = defaultForType(change.fieldType);
11919
+ lines.push(` // New required field: ${change.field} (${change.fieldType})`);
11920
+ if (defaultValue !== null) {
11921
+ lines.push(` // TODO: provide a meaningful default value`);
11922
+ lines.push(` block.${change.field} = block.${change.field} ?? ${defaultValue};`);
11923
+ } else {
11924
+ lines.push(` // TODO: provide a default value appropriate for the '${change.fieldType}' type`);
11925
+ lines.push(` // block.${change.field} = block.${change.field} ?? <default>;`);
11926
+ }
11927
+ break;
11928
+ }
11929
+ case "required_changed": {
11930
+ const defaultValue = defaultForType(change.fieldType);
11931
+ lines.push(` // Field is now required: ${change.field} (${change.fieldType})`);
11932
+ lines.push(` // Existing stories may have null/undefined values \u2014 provide a default for those.`);
11933
+ if (defaultValue !== null) {
11934
+ lines.push(` // TODO: provide a meaningful default value`);
11935
+ lines.push(` block.${change.field} = block.${change.field} ?? ${defaultValue};`);
11936
+ } else {
11937
+ lines.push(` // TODO: provide a default value appropriate for the '${change.fieldType}' type`);
11938
+ lines.push(` // block.${change.field} = block.${change.field} ?? <default>;`);
11939
+ }
11940
+ break;
11941
+ }
11942
+ }
11943
+ lines.push("");
11944
+ }
11945
+ const body = lines.length > 0 ? `
11946
+ ${lines.join("\n")}` : "\n";
11947
+ return `export default function (block) {${body} return block;
11948
+ }
11949
+ `;
11950
+ }
11951
+ async function writeMigrationFile(options) {
11952
+ const { spaceId, componentName, code, timestamp, basePath } = options;
11953
+ const dir = resolvePath(basePath, `migrations/${spaceId}`);
11954
+ await mkdir(dir, { recursive: true });
11955
+ const fileName = `${componentName}.${fileTimestamp(timestamp)}.js`;
11956
+ const filePath = join(dir, fileName);
11957
+ await writeFile(filePath, code, "utf-8");
11958
+ return filePath;
11959
+ }
11960
+
11961
+ const DEFAULT_GROUPS_FILENAME = "groups.json";
11962
+ const CONSOLIDATED_COMPONENTS_FILENAME = "components.json";
11963
+ function sanitizeForLocalWrite(component) {
11964
+ const { folder, ...rest } = component;
11965
+ if (isRecord(rest.schema)) {
11966
+ const schema = {};
11967
+ for (const [key, field] of Object.entries(rest.schema)) {
11968
+ if (isRecord(field) && "component_group_whitelist" in field) {
11969
+ const { component_group_whitelist, restrict_components, ...fieldRest } = field;
11970
+ if (fieldRest.restrict_type === "groups") {
11971
+ delete fieldRest.restrict_type;
11972
+ }
11973
+ schema[key] = fieldRest;
11974
+ } else {
11975
+ schema[key] = field;
11976
+ }
11977
+ }
11978
+ rest.schema = schema;
11979
+ }
11980
+ return rest;
11981
+ }
11982
+ async function writeLocalComponents({
11983
+ space,
11984
+ basePath,
11985
+ resolved,
11986
+ diffResult,
11987
+ deleteRemoved,
11988
+ ui,
11989
+ logger
11990
+ }) {
11991
+ const componentsDir = resolveCommandPath(directories.components, space, basePath);
11992
+ const consolidatedPath = join(componentsDir, CONSOLIDATED_COMPONENTS_FILENAME);
11993
+ if (await fileExists(consolidatedPath)) {
11994
+ ui.warn(
11995
+ `A consolidated ${CONSOLIDATED_COMPONENTS_FILENAME} exists at ${displayPath(componentsDir, basePath)}. Per-component files will still be written, but the consolidated file may shadow them when stories push validates schemas. Delete it or run \`storyblok components pull --separate-files\` to regenerate.`
11996
+ );
11997
+ }
11998
+ for (const component of resolved.components) {
11999
+ const filePath = join(componentsDir, `${sanitizeFilename(component.name || "")}.json`);
12000
+ await saveToFile(filePath, JSON.stringify(sanitizeForLocalWrite(component), null, 2));
12001
+ }
12002
+ const groupsPath = join(componentsDir, DEFAULT_GROUPS_FILENAME);
12003
+ if (await fileExists(groupsPath)) {
12004
+ try {
12005
+ await unlink(groupsPath);
12006
+ logger.info("Removed stale local groups file", { path: displayPath(groupsPath, basePath) });
12007
+ } catch (error) {
12008
+ if (error.code !== "ENOENT") {
12009
+ throw error;
12010
+ }
12011
+ }
12012
+ }
12013
+ if (deleteRemoved) {
12014
+ const staleComponents = diffResult.diffs.filter(
12015
+ (d) => d.type === "component" && d.action === "stale"
12016
+ );
12017
+ for (const stale of staleComponents) {
12018
+ const filePath = join(componentsDir, `${sanitizeFilename(stale.name)}.json`);
12019
+ try {
12020
+ await unlink(filePath);
12021
+ logger.info("Removed stale local component file", { path: displayPath(filePath, basePath) });
12022
+ } catch (error) {
12023
+ if (error.code !== "ENOENT") {
12024
+ throw error;
12025
+ }
12026
+ }
12027
+ }
12028
+ }
12029
+ logger.info("Wrote local component files", {
12030
+ space,
12031
+ componentsWritten: resolved.components.length
12032
+ });
12033
+ }
12034
+
12035
+ schemaCommand.command("push <entry-file>").description("Push local TypeScript schema and datasource definitions to a Storyblok space").option("-s, --space <space>", "space ID").option("-p, --path <path>", "path for file storage").option("--dry-run", "Show diffs without applying changes", false).option("--delete", "Delete remote entities not present in local schema", false).option("--migrations", "Generate scaffold migration files for breaking changes", true).addOption(new Option("--no-migrations", "Skip migration generation for breaking changes")).option("--write-components", "Write component schemas as local JSON files after push (also removes local files for components deleted via --delete)", true).addOption(new Option("--no-write-components", "Skip writing local component files")).action(async (entryFile, options, command) => {
12036
+ const ui = getUI();
12037
+ const logger = getLogger();
12038
+ const reporter = getReporter();
12039
+ const { space, path: basePath, verbose } = command.optsWithGlobals();
12040
+ const { state } = session();
12041
+ ui.title(commands.SCHEMA, colorPalette.SCHEMA, "Pushing schema...");
12042
+ logger.info("Schema push started", { entryFile, space });
12043
+ if (!requireAuthentication(state, verbose)) {
12044
+ return;
12045
+ }
12046
+ if (!space) {
12047
+ handleError(new CommandError("Please provide the space as argument --space SPACE_ID."), verbose);
12048
+ return;
12049
+ }
12050
+ const summary = { total: 0, succeeded: 0, failed: 0 };
12051
+ try {
12052
+ const loadSpinner = ui.createSpinner("Resolving schema...");
12053
+ let local;
12054
+ try {
12055
+ local = await loadSchema(entryFile);
12056
+ } catch (maybeError) {
12057
+ loadSpinner.failed("Failed to resolve schema");
12058
+ handleError(toError(maybeError), verbose);
12059
+ return;
12060
+ }
12061
+ loadSpinner.succeed(`Found: ${local.components.length} components, ${local.folders.length} folders, ${local.datasources.length} datasources`);
12062
+ const totalLocal = local.components.length + local.datasources.length + local.folders.length;
12063
+ if (totalLocal === 0) {
12064
+ ui.warn("No components, folders, or datasources found in the entry file. Verify the file exports schema definitions.");
12065
+ return;
12066
+ }
12067
+ const remoteSpinner = ui.createSpinner(`Fetching remote state from space ${space}...`);
12068
+ let remoteResult;
12069
+ try {
12070
+ remoteResult = await fetchRemoteSchema(space);
12071
+ } catch (maybeError) {
12072
+ remoteSpinner.failed("Failed to fetch remote schema");
12073
+ handleError(toError(maybeError), verbose);
12074
+ return;
12075
+ }
12076
+ const { remote, rawComponents, rawComponentFolders, rawDatasources } = remoteResult;
12077
+ remoteSpinner.succeed(`Remote: ${remote.components.size} components, ${remote.datasources.size} datasources`);
12078
+ const diffResult = diffSchema(local, remote);
12079
+ ui.br();
12080
+ ui.log(formatDiffOutput(diffResult, { delete: options.delete }));
12081
+ if (options.migrations) {
12082
+ const breakingChanges = analyzeBreakingChanges(diffResult, local, remote);
12083
+ if (breakingChanges.length > 0) {
12084
+ const totalChanges = breakingChanges.reduce((sum, c) => sum + c.changes.length, 0);
12085
+ ui.br();
12086
+ ui.warn(`${totalChanges} breaking change(s) detected in ${breakingChanges.length} component(s).`);
12087
+ ui.info("Generated migrations are scaffolds. Review and adjust them before running `storyblok migrations run`.");
12088
+ if (!options.dryRun) {
12089
+ const explicitMigrations = command.getOptionValueSource("migrations") === "cli";
12090
+ const shouldGenerate = explicitMigrations || await confirm({
12091
+ message: "Generate migration files for breaking changes?",
12092
+ default: true
12093
+ });
12094
+ if (shouldGenerate) {
12095
+ const migrationTimestamp = (/* @__PURE__ */ new Date()).toISOString();
12096
+ const resolvedBase = resolvePath(basePath, "");
12097
+ for (const comp of breakingChanges) {
12098
+ const renames = comp.changes.filter((c) => c.kind === "rename");
12099
+ if (renames.length > 0 && explicitMigrations) {
12100
+ for (const r of renames) {
12101
+ if (r.kind === "rename") {
12102
+ ui.log(` Assumed rename in '${comp.componentName}': ${r.oldField} \u2192 ${r.field}`);
12103
+ }
12104
+ }
12105
+ }
12106
+ if (renames.length > 0 && !explicitMigrations) {
12107
+ ui.br();
12108
+ ui.log(`Detected renames in '${comp.componentName}':`);
12109
+ for (const r of renames) {
12110
+ if (r.kind === "rename") {
12111
+ ui.log(` ${r.oldField} \u2192 ${r.field}`);
12112
+ }
12113
+ }
12114
+ const renameConfirmed = await confirm({
12115
+ message: "Are these renames correct?",
12116
+ default: true
12117
+ });
12118
+ if (!renameConfirmed) {
12119
+ comp.changes = comp.changes.map((c) => {
12120
+ if (c.kind === "rename") {
12121
+ return { kind: "removed", field: c.oldField, renameHint: { newField: c.field } };
12122
+ }
12123
+ return c;
12124
+ });
12125
+ }
12126
+ }
12127
+ const code = renderMigrationCode(comp.changes);
12128
+ const path = await writeMigrationFile({
12129
+ spaceId: space,
12130
+ componentName: comp.componentName,
12131
+ code,
12132
+ timestamp: migrationTimestamp,
12133
+ basePath: resolvedBase
12134
+ });
12135
+ const migrationPath = displayPath(path, basePath);
12136
+ logger.info("Migration generated", { component: comp.componentName, path: migrationPath });
12137
+ ui.log(` Generated: ${migrationPath}`);
12138
+ }
12139
+ ui.br();
12140
+ ui.info(`Run migrations when ready: storyblok migrations run --space ${space}`);
12141
+ }
12142
+ }
12143
+ }
12144
+ }
12145
+ if (options.delete) {
12146
+ const deletedComponents = diffResult.diffs.filter((d) => d.type === "component" && d.action === "stale");
12147
+ for (const comp of deletedComponents) {
12148
+ ui.warn(`Component '${comp.name}' will be deleted. Stories using it will have out-of-schema content.`);
12149
+ }
12150
+ const staleFolders = diffResult.diffs.filter((d) => d.type === "folder" && d.action === "stale");
12151
+ if (staleFolders.length > 0) {
12152
+ const uuidByPath = /* @__PURE__ */ new Map();
12153
+ for (const [uuid, segments] of buildGroupPathByUuid([...remote.componentFolders.values()])) {
12154
+ uuidByPath.set(segments.join("/"), uuid);
12155
+ }
12156
+ const staleComponentNames = new Set(
12157
+ diffResult.diffs.filter((d) => d.type === "component" && d.action === "stale").map((d) => d.name)
12158
+ );
12159
+ for (const folder of staleFolders) {
12160
+ const uuid = uuidByPath.get(folder.name);
12161
+ if (!uuid) {
12162
+ continue;
12163
+ }
12164
+ const count = [...remote.components.values()].filter((c) => c.component_group_uuid === uuid && !staleComponentNames.has(c.name)).length;
12165
+ if (count > 0) {
12166
+ ui.warn(`Folder '${folder.name}' will be deleted; ${count} component(s) inside will be ungrouped.`);
12167
+ }
12168
+ }
12169
+ }
12170
+ }
12171
+ if (diffResult.stale > 0 && !options.delete) {
12172
+ ui.warn(`${diffResult.stale} stale entity(s) exist remotely but not in schema. Use --delete to remove.`);
12173
+ }
12174
+ if (options.dryRun) {
12175
+ ui.info("Dry run \u2014 no changes applied.");
12176
+ logger.info("Dry run completed", { creates: diffResult.creates, updates: diffResult.updates });
12177
+ return;
12178
+ }
12179
+ const resolvedPath = resolvePath(basePath, "");
12180
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
12181
+ const changesetPath = await saveChangeset(resolvedPath, {
12182
+ timestamp,
12183
+ spaceId: Number(space),
12184
+ remote: { components: rawComponents, componentFolders: rawComponentFolders, datasources: rawDatasources },
12185
+ changes: buildChangesetEntries(diffResult, local, remote, { delete: options.delete })
12186
+ });
12187
+ logger.info("Changeset saved", { path: displayPath(changesetPath, basePath) });
12188
+ const nothingToPush = diffResult.creates === 0 && diffResult.updates === 0 && (!options.delete || diffResult.stale === 0);
12189
+ if (nothingToPush) {
12190
+ ui.ok("Everything up to date \u2014 nothing to push.");
12191
+ } else {
12192
+ const pushSpinner = ui.createSpinner("Pushing schema...");
12193
+ let result;
12194
+ try {
12195
+ result = await executePush(space, local, remote, diffResult, { delete: options.delete });
12196
+ } catch (error) {
12197
+ pushSpinner.failed("Failed to push schema");
12198
+ throw error;
12199
+ }
12200
+ summary.total = result.created + result.updated + result.deleted;
12201
+ summary.succeeded = summary.total;
12202
+ pushSpinner.succeed(`Pushed ${result.created} creations, ${result.updated} updates${result.deleted > 0 ? `, ${result.deleted} deletions` : ""}.`);
12203
+ }
12204
+ if (options.writeComponents) {
12205
+ try {
12206
+ await writeLocalComponents({
12207
+ space,
12208
+ basePath,
12209
+ resolved: local,
12210
+ diffResult,
12211
+ deleteRemoved: options.delete,
12212
+ ui,
12213
+ logger
12214
+ });
12215
+ } catch (writeError) {
12216
+ ui.warn(`Failed to write local component files: ${toError(writeError).message}`);
12217
+ logger.warn("Failed to write local component files", { error: toError(writeError).message });
12218
+ }
12219
+ }
12220
+ } catch (maybeError) {
12221
+ summary.failed += 1;
12222
+ handleError(toError(maybeError), verbose);
12223
+ } finally {
12224
+ logger.info("Schema push finished", { summary });
12225
+ reporter.addSummary("schemaPushResults", summary);
12226
+ reporter.finalize();
12227
+ }
12228
+ });
12229
+
12230
+ const FIELD_STRIP_KEYS = /* @__PURE__ */ new Set(["id", "pos"]);
12231
+ function toCamelCaseIdentifier(str) {
12232
+ const camel = slugify(str).replace(/^[_-]+/, "").replace(/[_-]+(.)/g, (_, char) => char.toUpperCase());
12233
+ if (!camel) {
12234
+ return "_";
12235
+ }
12236
+ return /^\d/.test(camel) ? `_${camel}` : camel;
12237
+ }
12238
+ function toKebabCase(str) {
12239
+ return str.replace(/[\s_]+/g, "-").replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "");
12240
+ }
12241
+ function componentVarName(name) {
12242
+ return `${toCamelCaseIdentifier(name)}Block`;
12243
+ }
12244
+ function datasourceVarName(name) {
12245
+ return `${toCamelCaseIdentifier(name)}Datasource`;
12246
+ }
12247
+ function folderVarName(name) {
12248
+ return `${toCamelCaseIdentifier(name)}Folder`;
12249
+ }
12250
+ function resolveVarNames(rawNames, baseVarName) {
12251
+ const used = /* @__PURE__ */ new Set();
12252
+ return rawNames.map((raw) => {
12253
+ const base = baseVarName(raw);
12254
+ let candidate = base;
12255
+ let n = 2;
12256
+ while (used.has(candidate)) {
12257
+ candidate = `${base}${n++}`;
12258
+ }
12259
+ used.add(candidate);
12260
+ return candidate;
12261
+ });
12262
+ }
12263
+ function resolveFileNames(baseNames, dirKeys) {
12264
+ const usedByDir = /* @__PURE__ */ new Map();
12265
+ return baseNames.map((base, i) => {
12266
+ const dir = dirKeys?.[i] ?? "";
12267
+ let used = usedByDir.get(dir);
12268
+ if (!used) {
12269
+ used = /* @__PURE__ */ new Set();
12270
+ usedByDir.set(dir, used);
12271
+ }
12272
+ let candidate = base;
12273
+ let n = 2;
12274
+ while (used.has(candidate)) {
12275
+ candidate = `${base}-${n++}`;
12276
+ }
12277
+ used.add(candidate);
12278
+ return candidate;
12279
+ });
12280
+ }
12281
+ function componentFileName(name) {
12282
+ return toKebabCase(name);
12283
+ }
12284
+ function datasourceFileName(datasource) {
12285
+ return toKebabCase(datasource.slug || datasource.name);
12286
+ }
12287
+ function resolveComponents(components, segmentsByIndex) {
12288
+ const varNames = resolveVarNames(components.map((c) => c.name), componentVarName);
12289
+ const fileNames = resolveFileNames(
12290
+ components.map((c) => componentFileName(c.name)),
12291
+ segmentsByIndex.map((segments) => segments.join("/"))
12292
+ );
12293
+ return components.map((component, i) => ({
12294
+ component,
12295
+ varName: varNames[i],
12296
+ fileName: fileNames[i],
12297
+ segments: segmentsByIndex[i]
12298
+ }));
12299
+ }
12300
+ function resolveDatasources(datasources) {
12301
+ const varNames = resolveVarNames(datasources.map((d) => d.name), datasourceVarName);
12302
+ const fileNames = resolveFileNames(datasources.map((d) => datasourceFileName(d)));
12303
+ return datasources.map((datasource, i) => ({
12304
+ datasource,
12305
+ varName: varNames[i],
12306
+ fileName: fileNames[i]
12307
+ }));
12308
+ }
12309
+ function resolveFolders(folders) {
12310
+ const pathByUuid = buildGroupPathByUuid(folders);
12311
+ const ordered = [...folders].sort((a, b) => (pathByUuid.get(a.uuid)?.length ?? 0) - (pathByUuid.get(b.uuid)?.length ?? 0));
12312
+ const varNames = resolveVarNames(ordered.map((f) => f.name), folderVarName);
12313
+ return ordered.map((folder, i) => ({
12314
+ folder,
12315
+ varName: varNames[i],
12316
+ segments: pathByUuid.get(folder.uuid) ?? []
12317
+ }));
12318
+ }
12319
+ function resolveGroupWhitelistRefs(whitelist, folderVarByUuid) {
12320
+ if (!folderVarByUuid || !Array.isArray(whitelist) || whitelist.length === 0) {
12321
+ return void 0;
12322
+ }
12323
+ const vars = whitelist.map((uuid) => typeof uuid === "string" ? folderVarByUuid.get(uuid) : void 0);
12324
+ if (!vars.every((v) => typeof v === "string")) {
12325
+ return void 0;
12326
+ }
12327
+ return vars.map((v) => new RawCode(v));
12328
+ }
12329
+ function toDslField(field, folderVarByUuid) {
12330
+ const { component_whitelist, component_group_whitelist, datasource_slug, restrict_components, restrict_type, ...rest } = field;
12331
+ const out = { ...rest };
12332
+ const groupRefs = resolveGroupWhitelistRefs(component_group_whitelist, folderVarByUuid);
12333
+ const hasBlockNames = Array.isArray(component_whitelist) && component_whitelist.length > 0;
12334
+ if (hasBlockNames) {
12335
+ out.allow = component_whitelist;
12336
+ } else if (groupRefs) {
12337
+ out.allow = groupRefs;
12338
+ } else if (component_group_whitelist !== void 0) {
12339
+ out.component_group_whitelist = component_group_whitelist;
12340
+ if (restrict_components !== void 0) {
12341
+ out.restrict_components = restrict_components;
12342
+ }
12343
+ if (restrict_type !== void 0) {
12344
+ out.restrict_type = restrict_type;
12345
+ }
12346
+ }
12347
+ if (datasource_slug !== void 0) {
12348
+ out.datasource = datasource_slug;
12349
+ }
12350
+ return out;
12351
+ }
12352
+ function omitEmptyArrays(obj) {
12353
+ const out = {};
12354
+ for (const [key, value] of Object.entries(obj)) {
12355
+ if (Array.isArray(value) && value.length === 0) {
12356
+ continue;
12357
+ }
12358
+ out[key] = value;
12359
+ }
12360
+ return out;
12361
+ }
12362
+ function generateFieldCode(fieldName, fieldData, depth, folderVarByUuid) {
12363
+ const clean = omitEmptyArrays(toDslField(stripKeys(fieldData, FIELD_STRIP_KEYS), folderVarByUuid));
12364
+ return `defineField(${quoteString(fieldName)}, ${formatValue(clean, depth)})`;
12365
+ }
12366
+ function collectWhitelistFolderVars(schema, folderVarByUuid) {
12367
+ const vars = /* @__PURE__ */ new Set();
12368
+ for (const field of Object.values(schema)) {
12369
+ if (!isRecord(field)) {
12370
+ continue;
12371
+ }
12372
+ const refs = resolveGroupWhitelistRefs(field.component_group_whitelist, folderVarByUuid);
12373
+ if (refs) {
12374
+ refs.forEach((ref) => vars.add(ref.code));
12375
+ }
12376
+ }
12377
+ return [...vars].sort();
12378
+ }
12379
+ function sortSchemaByPos(schema) {
12380
+ return Object.entries(schema).filter(([key]) => key !== "_uid" && key !== "component").sort(([, a], [, b]) => {
12381
+ const posA = typeof a.pos === "number" ? a.pos : Infinity;
12382
+ const posB = typeof b.pos === "number" ? b.pos : Infinity;
12383
+ return posA - posB;
12384
+ });
12385
+ }
12386
+ function generateFoldersFile(resolved) {
12387
+ const varByUuid = new Map(resolved.map((r) => [r.folder.uuid, r.varName]));
12388
+ const lines = ["import { defineFolder } from '@storyblok/schema';", ""];
12389
+ for (const { folder, varName } of resolved) {
12390
+ const parentVar = folder.parent_uuid ? varByUuid.get(folder.parent_uuid) : void 0;
12391
+ lines.push(`export const ${varName} = defineFolder({`);
12392
+ lines.push(`${INDENT}name: ${quoteString(folder.name)},`);
12393
+ if (parentVar) {
12394
+ lines.push(`${INDENT}parent: ${parentVar},`);
12395
+ }
12396
+ lines.push("});");
12397
+ lines.push("");
12398
+ }
12399
+ return lines.join("\n");
12400
+ }
12401
+ function generateComponentFile(component, varName, folderRef, folderVarByUuid) {
12402
+ const lines = [];
12403
+ lines.push("import {");
12404
+ lines.push(" defineBlock,");
12405
+ lines.push(" defineField,");
12406
+ lines.push("} from '@storyblok/schema';");
12407
+ lines.push("");
12408
+ const blockDepth = folderRef?.segments.length ?? 0;
12409
+ const schema = isRecord(component.schema) ? component.schema : {};
12410
+ const folderVars = [.../* @__PURE__ */ new Set([
12411
+ ...folderRef ? [folderRef.varName] : [],
12412
+ ...collectWhitelistFolderVars(schema, folderVarByUuid)
12413
+ ])].sort();
12414
+ if (folderVars.length > 0) {
12415
+ const rel = "../".repeat(blockDepth + 1);
12416
+ lines.push(`import { ${folderVars.join(", ")} } from '${rel}folders';`);
12417
+ lines.push("");
12418
+ }
12419
+ const resolvedVarName = varName ?? componentVarName(component.name);
12420
+ lines.push(`export const ${resolvedVarName} = defineBlock({`);
12421
+ const clean = omitEmptyArrays(stripKeys(component, COMPONENT_STRIP_KEYS));
12422
+ delete clean.component_group_uuid;
12423
+ const orderedKeys = [];
12424
+ if (clean.name !== void 0) {
12425
+ orderedKeys.push("name");
12426
+ }
12427
+ if (clean.display_name !== void 0) {
12428
+ orderedKeys.push("display_name");
12429
+ }
12430
+ if (clean.is_root !== void 0) {
12431
+ orderedKeys.push("is_root");
12432
+ }
12433
+ if (clean.is_nestable !== void 0) {
12434
+ orderedKeys.push("is_nestable");
12435
+ }
12436
+ const prefixKeyCount = orderedKeys.length;
12437
+ const handled = /* @__PURE__ */ new Set(["name", "display_name", "is_root", "is_nestable", "schema"]);
12438
+ for (const key of Object.keys(clean).sort()) {
12439
+ if (!handled.has(key)) {
12440
+ orderedKeys.push(key);
12441
+ }
12442
+ }
12443
+ orderedKeys.forEach((key, i) => {
12444
+ lines.push(`${INDENT}${key}: ${formatValue(clean[key], 1)},`);
12445
+ if (folderRef && i === prefixKeyCount - 1) {
12446
+ lines.push(`${INDENT}folder: ${folderRef.varName},`);
12447
+ }
12448
+ });
12449
+ if (folderRef && prefixKeyCount === 0) {
12450
+ lines.push(`${INDENT}folder: ${folderRef.varName},`);
12451
+ }
12452
+ if (clean.schema && typeof clean.schema === "object") {
12453
+ const schema2 = clean.schema;
12454
+ const sortedFields = sortSchemaByPos(schema2);
12455
+ if (sortedFields.length > 0) {
12456
+ lines.push(`${INDENT}fields: [`);
12457
+ for (const [fieldName, fieldData] of sortedFields) {
12458
+ const fieldCode = generateFieldCode(fieldName, fieldData, 2, folderVarByUuid);
12459
+ lines.push(`${INDENT}${INDENT}${fieldCode},`);
12460
+ }
12461
+ lines.push(`${INDENT}],`);
12462
+ } else {
12463
+ lines.push(`${INDENT}fields: [],`);
12464
+ }
12465
+ }
12466
+ lines.push("});");
12467
+ lines.push("");
12468
+ return lines.join("\n");
12469
+ }
12470
+ function generateDatasourceFile(datasource, varName) {
12471
+ const lines = [];
12472
+ lines.push("import { defineDatasource } from '@storyblok/schema';");
12473
+ lines.push("");
12474
+ const resolvedVarName = varName ?? datasourceVarName(datasource.name);
12475
+ lines.push(`export const ${resolvedVarName} = defineDatasource({`);
12476
+ const clean = stripKeys(datasource, DATASOURCE_STRIP_KEYS);
12477
+ if (clean.name !== void 0) {
12478
+ lines.push(`${INDENT}name: ${formatValue(clean.name, 1)},`);
12479
+ }
12480
+ if (clean.slug !== void 0) {
12481
+ lines.push(`${INDENT}slug: ${formatValue(clean.slug, 1)},`);
12482
+ }
12483
+ const handled = /* @__PURE__ */ new Set(["name", "slug"]);
12484
+ for (const [key, value] of Object.entries(clean).sort(([a], [b]) => a.localeCompare(b))) {
12485
+ if (!handled.has(key)) {
12486
+ lines.push(`${INDENT}${key}: ${formatValue(value, 1)},`);
12487
+ }
12488
+ }
12489
+ lines.push("});");
12490
+ lines.push("");
12491
+ return lines.join("\n");
12492
+ }
12493
+ function generateSchemaFile(components, datasources, folders = []) {
12494
+ const lines = [];
12495
+ lines.push("import { defineSchema } from '@storyblok/schema';");
12496
+ lines.push("import type { Schema as InferSchema, Story as InferStory } from '@storyblok/schema';");
12497
+ lines.push("import type { MapiStory as InferStoryMapi } from '@storyblok/schema';");
12498
+ lines.push("");
12499
+ for (const { varName, fileName, segments } of components) {
12500
+ const subPath = segments.length > 0 ? `${segments.join("/")}/` : "";
12501
+ lines.push(`import { ${varName} } from './blocks/${subPath}${fileName}';`);
12502
+ }
12503
+ for (const { varName, fileName } of datasources) {
12504
+ lines.push(`import { ${varName} } from './datasources/${fileName}';`);
12505
+ }
12506
+ if (folders.length > 0) {
12507
+ lines.push(`import { ${folders.map((f) => f.varName).join(", ")} } from './folders';`);
12508
+ }
12509
+ lines.push("");
12510
+ lines.push("export const schema = defineSchema({");
12511
+ if (components.length > 0) {
12512
+ lines.push(" blocks: {");
12513
+ for (const { varName } of components) {
12514
+ lines.push(` ${varName},`);
12515
+ }
12516
+ lines.push(" },");
12517
+ }
12518
+ if (datasources.length > 0) {
12519
+ lines.push(" datasources: {");
12520
+ for (const { varName } of datasources) {
12521
+ lines.push(` ${varName},`);
12522
+ }
12523
+ lines.push(" },");
12524
+ }
12525
+ if (folders.length > 0) {
12526
+ lines.push(" folders: {");
12527
+ for (const { varName } of folders) {
12528
+ lines.push(` ${varName},`);
12529
+ }
12530
+ lines.push(" },");
12531
+ }
12532
+ lines.push("});");
12533
+ lines.push("");
12534
+ lines.push("export type Schema = InferSchema<typeof schema>;");
12535
+ lines.push("export type Blocks = Schema['blocks'];");
12536
+ lines.push("export type Story = InferStory<Blocks>;");
12537
+ lines.push("export type StoryMapi = InferStoryMapi<Blocks>;");
12538
+ lines.push("");
12539
+ return lines.join("\n");
12540
+ }
12541
+
12542
+ async function writeFileWithDirs(filePath, content) {
12543
+ const dir = dirname(filePath);
12544
+ await mkdir(dir, { recursive: true });
12545
+ await writeFile(filePath, content, "utf-8");
12546
+ }
12547
+ async function writeSchemaFiles(targetPath, components, componentFolders, datasources) {
12548
+ const writtenFiles = [];
12549
+ const groupPathByUuid = buildGroupPathByUuid(componentFolders);
12550
+ const componentSegments = components.map(
12551
+ (comp) => comp.component_group_uuid ? groupPathByUuid.get(comp.component_group_uuid) ?? [] : []
12552
+ );
12553
+ const resolvedComponents = resolveComponents(components, componentSegments);
12554
+ const resolvedDatasources = resolveDatasources(datasources);
12555
+ const resolvedFolders = resolveFolders(componentFolders);
12556
+ const folderByUuid = new Map(resolvedFolders.map((r) => [r.folder.uuid, r]));
12557
+ const folderVarByUuid = new Map([...folderByUuid].map(([uuid, r]) => [uuid, r.varName]));
12558
+ for (const { component, varName, fileName, segments } of resolvedComponents) {
12559
+ const filePath = join(targetPath, "blocks", ...segments, `${fileName}.ts`);
12560
+ const folder = component.component_group_uuid ? folderByUuid.get(component.component_group_uuid) : void 0;
12561
+ const folderRef = folder && { varName: folder.varName, segments };
12562
+ await writeFileWithDirs(filePath, generateComponentFile(component, varName, folderRef, folderVarByUuid));
12563
+ writtenFiles.push(filePath);
12564
+ }
12565
+ for (const { datasource, varName, fileName } of resolvedDatasources) {
12566
+ const filePath = join(targetPath, "datasources", `${fileName}.ts`);
12567
+ await writeFileWithDirs(filePath, generateDatasourceFile(datasource, varName));
12568
+ writtenFiles.push(filePath);
12569
+ }
12570
+ if (resolvedFolders.length > 0) {
12571
+ const foldersPath = join(targetPath, "folders.ts");
12572
+ await writeFileWithDirs(foldersPath, generateFoldersFile(resolvedFolders));
12573
+ writtenFiles.push(foldersPath);
12574
+ }
12575
+ const schemaPath = join(targetPath, "schema.ts");
12576
+ await writeFileWithDirs(schemaPath, generateSchemaFile(resolvedComponents, resolvedDatasources, resolvedFolders));
12577
+ writtenFiles.push(schemaPath);
12578
+ return writtenFiles;
12579
+ }
12580
+
12581
+ async function isTargetEmpty(targetPath) {
12582
+ try {
12583
+ const entries = await readdir(targetPath);
12584
+ return entries.every((entry) => entry.startsWith("."));
12585
+ } catch (maybeError) {
12586
+ const error = maybeError;
12587
+ if (error?.code === "ENOENT") {
12588
+ return true;
12589
+ }
12590
+ throw error;
12591
+ }
12592
+ }
12593
+ schemaCommand.command("init").description("Initialize a local code-driven schema workspace from an existing Storyblok space (one-time bootstrap)").option("-s, --space <space>", "space ID").option("--out-dir <dir>", "Output directory for generated bootstrap files", ".storyblok/schema").action(async (options, command) => {
12594
+ const ui = getUI();
12595
+ const logger = getLogger();
12596
+ const reporter = getReporter();
12597
+ const { space, verbose } = command.optsWithGlobals();
12598
+ const { state } = session();
12599
+ ui.title(commands.SCHEMA, colorPalette.SCHEMA, "Initializing schema...");
12600
+ logger.info("Schema init started", { space });
12601
+ if (!requireAuthentication(state, verbose)) {
12602
+ return;
12603
+ }
12604
+ if (!space) {
12605
+ handleError(new CommandError("Please provide the space as argument --space SPACE_ID."), verbose);
12606
+ return;
12607
+ }
12608
+ const targetPath = resolve(options.outDir);
12609
+ const targetDisplayPath = displayPath(targetPath, options.outDir);
12610
+ if (!await isTargetEmpty(targetPath)) {
12611
+ handleError(
12612
+ new CommandError(`Target directory ${targetDisplayPath} is not empty. \`schema init\` is a one-time bootstrap and refuses to overwrite existing files. Use \`schema push\` for ongoing changes, or remove the directory to re-bootstrap.`),
12613
+ verbose
12614
+ );
12615
+ return;
12616
+ }
12617
+ const summary = { total: 0, succeeded: 0, failed: 0 };
12618
+ try {
12619
+ const fetchSpinner = ui.createSpinner(`Fetching schema from space ${space}...`);
12620
+ let fetchResult;
12621
+ try {
12622
+ fetchResult = await fetchRemoteSchema(space);
12623
+ } catch (maybeError) {
12624
+ fetchSpinner.failed("Failed to fetch remote schema");
12625
+ handleError(toError(maybeError), verbose);
12626
+ return;
12627
+ }
12628
+ const { rawComponents, rawComponentFolders, rawDatasources } = fetchResult;
12629
+ fetchSpinner.succeed(`Found: ${rawComponents.length} components, ${rawComponentFolders.length} component folders, ${rawDatasources.length} datasources`);
12630
+ const writeSpinner = ui.createSpinner(`Generating TypeScript files to ${targetDisplayPath}...`);
12631
+ const writtenFiles = await writeSchemaFiles(targetPath, rawComponents, rawComponentFolders, rawDatasources);
12632
+ summary.total = writtenFiles.length;
12633
+ summary.succeeded = writtenFiles.length;
12634
+ writeSpinner.succeed(`Generated ${writtenFiles.length} files`);
12635
+ ui.list(writtenFiles.map((file) => displayPath(file, options.outDir)));
12636
+ ui.warn("`schema init` is a one-time bootstrap step for adopting an existing space. Review generated files before continuing.");
12637
+ ui.info("After bootstrapping, keep your local schema as the source of truth and use `schema push` for ongoing changes.");
12638
+ ui.info("Make sure `@storyblok/schema` is installed in the project that imports these files (e.g. `pnpm add @storyblok/schema`).");
12639
+ } catch (maybeError) {
12640
+ summary.failed += 1;
12641
+ handleError(toError(maybeError), verbose);
12642
+ } finally {
12643
+ logger.info("Schema init finished", { summary });
12644
+ reporter.addSummary("schemaInitResults", summary);
12645
+ reporter.finalize();
12646
+ }
12647
+ });
12648
+
12649
+ const API_ASSIGNED_FIELDS = [
12650
+ "id",
12651
+ "created_at",
12652
+ "updated_at",
12653
+ "real_name",
12654
+ "all_presets",
12655
+ "image",
12656
+ "uuid"
12657
+ ];
12658
+ function stripApiFields(payload) {
12659
+ const result = { ...payload };
12660
+ for (const field of API_ASSIGNED_FIELDS) {
12661
+ delete result[field];
12662
+ }
12663
+ return result;
12664
+ }
12665
+ async function listChangesets(basePath) {
12666
+ const dir = join(basePath, "schema", "changesets");
12667
+ if (!await fileExists(dir)) {
12668
+ return [];
12669
+ }
12670
+ const files = await readDirectory(dir);
12671
+ return files.filter((f) => f.endsWith(".json")).sort().reverse().map((f) => join(dir, f));
12672
+ }
12673
+ async function loadChangeset(filePath) {
12674
+ const content = await readFile$1(filePath, "utf-8");
12675
+ return JSON.parse(content);
12676
+ }
12677
+ function buildRollbackOps(changeset) {
12678
+ if (changeset.changes.length === 0) {
12679
+ return [];
12680
+ }
12681
+ return changeset.changes.map((entry) => {
12682
+ switch (entry.action) {
12683
+ case "create":
12684
+ return { type: entry.type, name: entry.name, action: "delete", payload: {} };
12685
+ case "update":
12686
+ return { type: entry.type, name: entry.name, action: "update", payload: entry.before ?? {} };
12687
+ case "delete":
12688
+ return { type: entry.type, name: entry.name, action: "create", payload: entry.before ?? {} };
12689
+ default:
12690
+ return { type: entry.type, name: entry.name, action: entry.action, payload: {} };
12691
+ }
12692
+ });
12693
+ }
12694
+ function rollbackAction(original) {
12695
+ switch (original) {
12696
+ case "create":
12697
+ return "delete";
12698
+ case "update":
12699
+ return "update";
12700
+ case "delete":
12701
+ return "create";
12702
+ }
12703
+ }
12704
+ function formatRollbackOutput(changes) {
12705
+ const byType = {
12706
+ folder: [],
12707
+ component: [],
12708
+ datasource: []
12709
+ };
12710
+ for (const entry of changes) {
12711
+ byType[entry.type]?.push(entry);
12712
+ }
12713
+ const icons = {
12714
+ create: chalk.green("+"),
12715
+ update: chalk.yellow("~"),
12716
+ delete: chalk.red("-")
12717
+ };
12718
+ const lines = [];
12719
+ const sections = [
12720
+ ["Folders", byType.folder],
12721
+ ["Components", byType.component],
12722
+ ["Datasources", byType.datasource]
12723
+ ];
12724
+ for (const [label, entries] of sections) {
12725
+ if (entries.length === 0) {
12726
+ continue;
12727
+ }
12728
+ lines.push(chalk.bold(label));
12729
+ for (const entry of entries) {
12730
+ const action = rollbackAction(entry.action);
12731
+ const icon = icons[action] ?? " ";
12732
+ const name = action === "delete" ? chalk.red(entry.name) : entry.name;
12733
+ lines.push(` ${icon} ${name} ${chalk.dim(`(${action})`)}`);
12734
+ if (entry.action === "update" && entry.before && entry.after) {
12735
+ let fromStr;
12736
+ let toStr;
12737
+ if (entry.type === "component") {
12738
+ fromStr = serializeComponent(applyDefaults(entry.after, COMPONENT_DEFAULTS));
12739
+ toStr = serializeComponent(applyDefaults(entry.before, COMPONENT_DEFAULTS));
12740
+ } else if (entry.type === "datasource") {
12741
+ fromStr = serializeDatasource(entry.after);
12742
+ toStr = serializeDatasource(entry.before);
12743
+ } else {
12744
+ continue;
12745
+ }
12746
+ if (fromStr !== toStr) {
12747
+ const patch = createTwoFilesPatch(
12748
+ `current/${entry.name}`,
12749
+ `restore/${entry.name}`,
12750
+ fromStr,
12751
+ toStr,
12752
+ "current",
12753
+ "restore"
12754
+ );
12755
+ for (const line of patch.split("\n")) {
12756
+ if (line.startsWith("+") && !line.startsWith("+++")) {
12757
+ lines.push(` ${chalk.green(line)}`);
12758
+ } else if (line.startsWith("-") && !line.startsWith("---")) {
12759
+ lines.push(` ${chalk.red(line)}`);
12760
+ }
12761
+ }
12762
+ }
12763
+ }
12764
+ }
12765
+ lines.push("");
12766
+ }
12767
+ return lines.join("\n").trimEnd();
12768
+ }
12769
+ function buildGroupRefByPath(remote) {
12770
+ const folders = [...remote.componentFolders.values()];
12771
+ const pathByUuid = buildGroupPathByUuid(folders);
12772
+ const byPath = /* @__PURE__ */ new Map();
12773
+ for (const folder of folders) {
12774
+ const segments = pathByUuid.get(folder.uuid);
12775
+ if (segments?.length) {
12776
+ byPath.set(segments.join("/"), { id: folder.id, uuid: folder.uuid });
12777
+ }
12778
+ }
12779
+ return byPath;
12780
+ }
12781
+ function parentPathOf(path) {
12782
+ const segments = path.split("/");
12783
+ return segments.length > 1 ? segments.slice(0, -1).join("/") : null;
12784
+ }
12785
+ function remapGroupUuids(payload, uuidMap) {
12786
+ if (uuidMap.size === 0) {
12787
+ return payload;
12788
+ }
12789
+ const result = { ...payload };
12790
+ if (typeof result.component_group_uuid === "string") {
12791
+ result.component_group_uuid = uuidMap.get(result.component_group_uuid) ?? result.component_group_uuid;
12792
+ }
12793
+ if (isRecord(result.schema)) {
12794
+ const schema = {};
12795
+ for (const [key, field] of Object.entries(result.schema)) {
12796
+ if (isRecord(field) && Array.isArray(field.component_group_whitelist)) {
12797
+ schema[key] = {
12798
+ ...field,
12799
+ component_group_whitelist: field.component_group_whitelist.map((uuid) => typeof uuid === "string" ? uuidMap.get(uuid) ?? uuid : uuid)
12800
+ };
12801
+ } else {
12802
+ schema[key] = field;
12803
+ }
12804
+ }
12805
+ result.schema = schema;
12806
+ }
12807
+ return result;
12808
+ }
12809
+ async function executeRollback(spaceId, ops, remote) {
12810
+ const client = getMapiClient();
12811
+ const spaceIdNum = Number(spaceId);
12812
+ let created = 0;
12813
+ let updated = 0;
12814
+ let deleted = 0;
12815
+ const componentOps = ops.filter((op) => op.type === "component");
12816
+ const datasourceOps = ops.filter((op) => op.type === "datasource");
12817
+ const folderOps = ops.filter((op) => op.type === "folder");
12818
+ const groupRefByPath = buildGroupRefByPath(remote);
12819
+ const groupUuidMap = /* @__PURE__ */ new Map();
12820
+ const folderCreates = folderOps.filter((o) => o.action === "create").sort((a, b) => a.name.split("/").length - b.name.split("/").length);
12821
+ for (const op of folderCreates) {
12822
+ const existing = groupRefByPath.get(op.name);
12823
+ const oldUuid = typeof op.payload.uuid === "string" ? op.payload.uuid : void 0;
12824
+ if (existing) {
12825
+ if (oldUuid) {
12826
+ groupUuidMap.set(oldUuid, existing.uuid);
12827
+ }
12828
+ continue;
12829
+ }
12830
+ const parentPath = parentPathOf(op.name);
12831
+ const parentId = parentPath ? groupRefByPath.get(parentPath)?.id ?? null : null;
12832
+ const name = typeof op.payload.name === "string" ? op.payload.name : op.name.split("/").pop() ?? op.name;
12833
+ try {
12834
+ const res = await client.componentFolders.create({
12835
+ path: { space_id: spaceIdNum },
12836
+ body: { component_group: { name, parent_id: parentId } },
12837
+ throwOnError: true
12838
+ });
12839
+ const group = res.data?.component_group;
12840
+ if (group?.id != null && group.uuid) {
12841
+ groupRefByPath.set(op.name, { id: group.id, uuid: group.uuid });
12842
+ if (oldUuid) {
12843
+ groupUuidMap.set(oldUuid, group.uuid);
12844
+ }
12845
+ created++;
12846
+ }
12847
+ } catch (error) {
12848
+ handleAPIError("push_component_folder", error, `Failed to recreate folder ${op.name}`);
12849
+ }
12850
+ }
12851
+ for (const op of componentOps.filter((o) => o.action !== "delete")) {
12852
+ if (op.action === "create") {
12853
+ const payload = toComponentCreate(remapGroupUuids(stripApiFields(op.payload), groupUuidMap));
12854
+ try {
12855
+ await client.components.create({
12856
+ path: { space_id: spaceIdNum },
12857
+ body: { component: payload },
12858
+ throwOnError: true
12859
+ });
12860
+ created++;
12861
+ } catch (error) {
12862
+ handleAPIError("push_component", error, `Failed to create component ${op.name}`);
12863
+ }
12864
+ } else if (op.action === "update") {
12865
+ const existing = remote.components.get(op.name);
12866
+ if (existing?.id) {
12867
+ const payload = toComponentUpdate(remapGroupUuids(stripApiFields(op.payload), groupUuidMap));
12868
+ try {
12869
+ await client.components.update(existing.id, {
12870
+ path: { space_id: spaceIdNum },
12871
+ body: { component: payload },
12872
+ throwOnError: true
12873
+ });
12874
+ updated++;
12875
+ } catch (error) {
12876
+ handleAPIError("update_component", error, `Failed to update component ${op.name}`);
12877
+ }
12878
+ }
12879
+ }
12880
+ }
12881
+ for (const op of datasourceOps.filter((o) => o.action !== "delete")) {
12882
+ if (op.action === "create") {
12883
+ const payload = toDatasourceCreate(stripApiFields(op.payload));
12884
+ try {
12885
+ await client.datasources.create({
12886
+ path: { space_id: spaceIdNum },
12887
+ body: { datasource: payload },
12888
+ throwOnError: true
12889
+ });
12890
+ created++;
12891
+ } catch (error) {
12892
+ handleAPIError("push_datasource", error, `Failed to create datasource ${op.name}`);
12893
+ }
12894
+ } else if (op.action === "update") {
12895
+ const existing = remote.datasources.get(op.name);
12896
+ if (existing?.id) {
12897
+ const payload = toDatasourceUpdate(stripApiFields(op.payload), existing);
12898
+ try {
12899
+ await client.datasources.update(existing.id, {
12900
+ path: { space_id: spaceIdNum },
12901
+ body: { datasource: payload },
12902
+ throwOnError: true
12903
+ });
12904
+ updated++;
12905
+ } catch (error) {
12906
+ handleAPIError("update_datasource", error, `Failed to update datasource ${op.name}`);
12907
+ }
12908
+ }
12909
+ }
12910
+ }
12911
+ for (const op of datasourceOps.filter((o) => o.action === "delete")) {
12912
+ const existing = remote.datasources.get(op.name);
12913
+ if (existing?.id) {
12914
+ try {
12915
+ await client.datasources.delete(existing.id, {
12916
+ path: { space_id: spaceIdNum },
12917
+ throwOnError: true
12918
+ });
12919
+ deleted++;
12920
+ } catch (error) {
12921
+ handleAPIError("delete_datasource", error, `Failed to delete datasource ${op.name}`);
12922
+ }
12923
+ }
12924
+ }
12925
+ for (const op of componentOps.filter((o) => o.action === "delete")) {
12926
+ const existing = remote.components.get(op.name);
12927
+ if (existing?.id) {
12928
+ try {
12929
+ await client.components.delete(existing.id, {
12930
+ path: { space_id: spaceIdNum },
12931
+ throwOnError: true
12932
+ });
12933
+ deleted++;
12934
+ } catch (error) {
12935
+ handleAPIError("delete_component", error, `Failed to delete component ${op.name}`);
12936
+ }
12937
+ }
12938
+ }
12939
+ const folderDeletes = folderOps.filter((o) => o.action === "delete").sort((a, b) => b.name.split("/").length - a.name.split("/").length);
12940
+ for (const op of folderDeletes) {
12941
+ const ref = groupRefByPath.get(op.name);
12942
+ if (!ref) {
12943
+ continue;
12944
+ }
12945
+ try {
12946
+ await client.componentFolders.delete(ref.id, {
12947
+ path: { space_id: spaceIdNum },
12948
+ throwOnError: true
12949
+ });
12950
+ deleted++;
12951
+ } catch (error) {
12952
+ handleAPIError("delete_component_folder", error, `Failed to delete folder ${op.name}`);
12953
+ }
12954
+ }
12955
+ return { created, updated, deleted };
12956
+ }
12957
+
12958
+ schemaCommand.command("rollback [changeset-file]").description("Roll back a Storyblok space to the state captured in a changeset").option("-s, --space <space>", "space ID").option("-p, --path <path>", "path for file storage").option("--dry-run", "Show what would be undone without applying changes", false).option("--yes", "Skip confirmation prompt", false).option("--latest", "Automatically select the most recent changeset", false).action(async (changesetFile, options, command) => {
12959
+ const ui = getUI();
12960
+ const logger = getLogger();
12961
+ const reporter = getReporter();
12962
+ const { space, path: basePath, verbose } = command.optsWithGlobals();
12963
+ const { state } = session();
12964
+ ui.title(commands.SCHEMA, colorPalette.SCHEMA, "Rolling back schema...");
12965
+ logger.info("Schema rollback started", { changesetFile, space });
12966
+ if (!requireAuthentication(state, verbose)) {
12967
+ return;
12968
+ }
12969
+ if (!space) {
12970
+ handleError(new CommandError("Please provide the space as argument --space SPACE_ID."), verbose);
12971
+ return;
12972
+ }
12973
+ const summary = { total: 0, succeeded: 0, failed: 0 };
12974
+ try {
12975
+ const resolvedBase = resolvePath(basePath, "");
12976
+ let resolvedFile;
12977
+ if (changesetFile) {
12978
+ resolvedFile = changesetFile;
12979
+ } else if (options.latest) {
12980
+ const available = await listChangesets(resolvedBase);
12981
+ if (available.length === 0) {
12982
+ ui.warn("No changesets found. Run `schema push` first to create one.");
12983
+ return;
12984
+ }
12985
+ resolvedFile = available[0];
12986
+ ui.info(`Using latest changeset: ${basename(resolvedFile)}`);
12987
+ } else {
12988
+ const available = await listChangesets(resolvedBase);
12989
+ if (available.length === 0) {
12990
+ ui.warn("No changesets found. Run `schema push` first to create one.");
12991
+ return;
12992
+ }
12993
+ resolvedFile = await select({
12994
+ message: "Select a changeset to roll back:",
12995
+ choices: available.map((f) => ({ name: basename(f), value: f }))
12996
+ });
12997
+ }
12998
+ let changeset;
12999
+ try {
13000
+ changeset = await loadChangeset(resolvedFile);
13001
+ } catch (maybeError) {
13002
+ handleError(toError(maybeError), verbose);
13003
+ return;
13004
+ }
13005
+ logger.info("Changeset loaded", { file: resolvedFile, changes: changeset.changes.length });
13006
+ const ops = buildRollbackOps(changeset);
13007
+ if (ops.length === 0) {
13008
+ ui.ok("Changeset has no changes \u2014 nothing to roll back.");
13009
+ return;
13010
+ }
13011
+ ui.br();
13012
+ ui.log(formatRollbackOutput(changeset.changes));
13013
+ if (options.dryRun) {
13014
+ ui.info("Dry run \u2014 no changes applied.");
13015
+ logger.info("Dry run completed", { ops: ops.length });
13016
+ return;
13017
+ }
13018
+ if (!options.yes) {
13019
+ const confirmed = await confirm({
13020
+ message: `Apply rollback of ${ops.length} change(s) from ${basename(resolvedFile)}?`,
13021
+ default: false
13022
+ });
13023
+ if (!confirmed) {
13024
+ ui.info("Rollback cancelled.");
13025
+ return;
13026
+ }
13027
+ }
13028
+ const remoteSpinner = ui.createSpinner(`Fetching current remote state from space ${space}...`);
13029
+ let remoteResult;
13030
+ try {
13031
+ remoteResult = await fetchRemoteSchema(space);
13032
+ } catch (maybeError) {
13033
+ remoteSpinner.failed("Failed to fetch remote schema");
13034
+ handleError(toError(maybeError), verbose);
13035
+ return;
13036
+ }
13037
+ const { remote } = remoteResult;
13038
+ remoteSpinner.succeed(`Remote: ${remote.components.size} components, ${remote.componentFolders.size} component folders, ${remote.datasources.size} datasources`);
13039
+ const rollbackSpinner = ui.createSpinner("Applying rollback...");
13040
+ let result;
13041
+ try {
13042
+ result = await executeRollback(space, ops, remote);
13043
+ } catch (error) {
13044
+ rollbackSpinner.failed("Failed to apply rollback");
13045
+ throw error;
13046
+ }
13047
+ summary.total = result.created + result.updated + result.deleted;
13048
+ summary.succeeded = summary.total;
13049
+ rollbackSpinner.succeed(`Rolled back: ${result.created} created, ${result.updated} updated, ${result.deleted} deleted.`);
13050
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
13051
+ const rollbackChangesetPath = await saveChangeset(resolvedBase, {
13052
+ timestamp,
13053
+ spaceId: Number(space),
13054
+ remote: { components: remoteResult.rawComponents, componentFolders: remoteResult.rawComponentFolders, datasources: remoteResult.rawDatasources },
13055
+ changes: ops.map((op) => ({
13056
+ type: op.type,
13057
+ name: op.name,
13058
+ action: op.action,
13059
+ ...Object.keys(op.payload).length > 0 && { after: op.payload }
13060
+ }))
13061
+ });
13062
+ logger.info("Rollback changeset saved", { path: displayPath(rollbackChangesetPath, basePath) });
13063
+ } catch (maybeError) {
13064
+ summary.failed += 1;
13065
+ handleError(toError(maybeError), verbose);
13066
+ } finally {
13067
+ logger.info("Schema rollback finished", { summary });
13068
+ reporter.addSummary("schemaRollbackResults", summary);
13069
+ reporter.finalize();
13070
+ }
13071
+ });
13072
+
10632
13073
  const program = getProgram();
10633
13074
  konsola.br();
10634
13075
  konsola.br();