storyblok 4.18.2 → 4.19.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -7,7 +7,7 @@ import { homedir } from 'node:os';
7
7
  import { loadConfig as loadConfig$1, SUPPORTED_EXTENSIONS } from 'c12';
8
8
  import chalk from 'chalk';
9
9
  import { readPackageUp } from 'read-package-up';
10
- import { Command } from 'commander';
10
+ import { Command, Option } from 'commander';
11
11
  import { MultiBar, Presets } from 'cli-progress';
12
12
  import { Spinner } from '@topcli/spinner';
13
13
  import fs, { mkdir, writeFile, readdir, readFile as readFile$1, appendFile, access, constants, unlink } from 'node:fs/promises';
@@ -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",
@@ -1048,11 +1051,11 @@ function requireAuthentication(state, verbose = false) {
1048
1051
  return true;
1049
1052
  }
1050
1053
 
1051
- const toCamelCase = (str) => {
1054
+ const toCamelCase$1 = (str) => {
1052
1055
  return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()).replace(/_/g, "").replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()).replace(/[^a-z0-9]([a-z])/gi, (_, letter) => letter.toUpperCase()).replace(/[^a-z0-9]/gi, "");
1053
1056
  };
1054
1057
  const toPascalCase = (str) => {
1055
- const camelCase = toCamelCase(str);
1058
+ const camelCase = toCamelCase$1(str);
1056
1059
  return camelCase ? camelCase[0].toUpperCase() + camelCase.slice(1) : camelCase;
1057
1060
  };
1058
1061
  const capitalize = (str) => {
@@ -1150,6 +1153,25 @@ function getPackageJson() {
1150
1153
  return packageJson$1;
1151
1154
  }
1152
1155
 
1156
+ async function fetchAllPages(fetchFunction, extractDataFunction) {
1157
+ const items = [];
1158
+ let page = 1;
1159
+ while (true) {
1160
+ const { data, response } = await fetchFunction(page);
1161
+ const totalHeader = response.headers.get("total");
1162
+ const fetchedItems = extractDataFunction(data);
1163
+ items.push(...fetchedItems);
1164
+ if (!totalHeader) {
1165
+ return items;
1166
+ }
1167
+ const total = Number(totalHeader);
1168
+ if (Number.isNaN(total) || items.length >= total || fetchedItems.length === 0) {
1169
+ return items;
1170
+ }
1171
+ page++;
1172
+ }
1173
+ }
1174
+
1153
1175
  const __filename$1 = fileURLToPath(import.meta.url);
1154
1176
  const __dirname$1 = dirname(__filename$1);
1155
1177
  function isRegion(value) {
@@ -1238,6 +1260,10 @@ class UI {
1238
1260
  this.br();
1239
1261
  }
1240
1262
  }
1263
+ /** Plain console.log passthrough — use for preformatted or multi-line text. */
1264
+ log(message) {
1265
+ this.console?.log(message);
1266
+ }
1241
1267
  list(items) {
1242
1268
  for (const item of items) {
1243
1269
  this.console?.log(` ${item}`);
@@ -2147,14 +2173,14 @@ async function performInteractiveLogin(options) {
2147
2173
  }
2148
2174
  }
2149
2175
 
2150
- const program$e = getProgram();
2176
+ const program$f = getProgram();
2151
2177
  const allRegionsText = Object.values(regions).join(",");
2152
- 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(
2178
+ 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(
2153
2179
  "-r, --region <region>",
2154
2180
  `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}.`
2155
2181
  ).action(async (options) => {
2156
2182
  konsola.title(`${commands.LOGIN}`, colorPalette.LOGIN);
2157
- const verbose = program$e.opts().verbose;
2183
+ const verbose = program$f.opts().verbose;
2158
2184
  const { token, region } = options;
2159
2185
  const { state, updateSession, persistCredentials } = session();
2160
2186
  if (state.isLoggedIn && !state.envLogin) {
@@ -2212,10 +2238,10 @@ program$e.command(commands.LOGIN).description("Login to the Storyblok CLI").opti
2212
2238
  konsola.br();
2213
2239
  });
2214
2240
 
2215
- const program$d = getProgram();
2216
- program$d.command(commands.LOGOUT).description("Logout from the Storyblok CLI").action(async () => {
2241
+ const program$e = getProgram();
2242
+ program$e.command(commands.LOGOUT).description("Logout from the Storyblok CLI").action(async () => {
2217
2243
  konsola.title(`${commands.LOGOUT}`, colorPalette.LOGOUT);
2218
- const verbose = program$d.opts().verbose;
2244
+ const verbose = program$e.opts().verbose;
2219
2245
  try {
2220
2246
  const { state } = session();
2221
2247
  if (!state.isLoggedIn || !state.password || !state.region) {
@@ -2262,10 +2288,10 @@ async function openSignupInBrowser(url) {
2262
2288
  }
2263
2289
  }
2264
2290
 
2265
- const program$c = getProgram();
2266
- program$c.command(commands.SIGNUP).description("Sign up for Storyblok").action(async () => {
2291
+ const program$d = getProgram();
2292
+ program$d.command(commands.SIGNUP).description("Sign up for Storyblok").action(async () => {
2267
2293
  konsola.title(`${commands.SIGNUP}`, colorPalette.SIGNUP);
2268
- const verbose = program$c.opts().verbose;
2294
+ const verbose = program$d.opts().verbose;
2269
2295
  const { state } = session();
2270
2296
  if (state.isLoggedIn && !state.envLogin) {
2271
2297
  konsola.ok(`You are already logged in. If you want to signup with a different account, please logout first.`);
@@ -2286,10 +2312,10 @@ program$c.command(commands.SIGNUP).description("Sign up for Storyblok").action(a
2286
2312
  konsola.br();
2287
2313
  });
2288
2314
 
2289
- const program$b = getProgram();
2290
- program$b.command(commands.USER).description("Get the current user").action(async () => {
2315
+ const program$c = getProgram();
2316
+ program$c.command(commands.USER).description("Get the current user").action(async () => {
2291
2317
  konsola.title(`${commands.USER}`, colorPalette.USER);
2292
- const verbose = program$b.opts().verbose;
2318
+ const verbose = program$c.opts().verbose;
2293
2319
  const { state } = session();
2294
2320
  if (!requireAuthentication(state)) {
2295
2321
  return;
@@ -2317,10 +2343,10 @@ program$b.command(commands.USER).description("Get the current user").action(asyn
2317
2343
  konsola.br();
2318
2344
  });
2319
2345
 
2320
- const program$a = getProgram();
2321
- const componentsCommand = program$a.command(commands.COMPONENTS).alias("comp").description(`Manage your space's block schema`);
2346
+ const program$b = getProgram();
2347
+ const componentsCommand = program$b.command(commands.COMPONENTS).alias("comp").description(`Manage your space's block schema`);
2322
2348
 
2323
- function isComponent(item) {
2349
+ function isComponent$1(item) {
2324
2350
  return "schema" in item;
2325
2351
  }
2326
2352
  function isPreset(item) {
@@ -2354,7 +2380,7 @@ async function loadComponents(directoryPath, options) {
2354
2380
  throw new Error('Internal tag is missing "id"!');
2355
2381
  }
2356
2382
  tagMap.set(item.id, item);
2357
- } else if (isComponent(item)) {
2383
+ } else if (isComponent$1(item)) {
2358
2384
  const existing = componentMap.get(item.name);
2359
2385
  if (existing) {
2360
2386
  duplicates.push(`Component "${item.name}" found in both "${existing.file}" and "${file}"`);
@@ -2390,44 +2416,20 @@ To fix this, either:
2390
2416
  }
2391
2417
 
2392
2418
  const DEFAULT_COMPONENTS_FILENAME = "components";
2393
- const DEFAULT_GROUPS_FILENAME = "groups";
2419
+ const DEFAULT_GROUPS_FILENAME$1 = "groups";
2394
2420
  const DEFAULT_PRESETS_FILENAME = "presets";
2395
2421
  const DEFAULT_TAGS_FILENAME = "tags";
2396
2422
 
2397
- async function fetchAllPages(fetchFunction, extractDataFunction) {
2398
- const items = [];
2399
- let page = 1;
2400
- while (true) {
2401
- const { data, response } = await fetchFunction(page);
2402
- const totalHeader = response.headers.get("total");
2403
- const fetchedItems = extractDataFunction(data);
2404
- items.push(...fetchedItems);
2405
- if (!totalHeader) {
2406
- return items;
2407
- }
2408
- const total = Number(totalHeader);
2409
- if (Number.isNaN(total) || items.length >= total || fetchedItems.length === 0) {
2410
- return items;
2411
- }
2412
- page++;
2413
- }
2414
- }
2415
-
2416
2423
  const fetchComponents = async (spaceId) => {
2417
2424
  try {
2418
2425
  const client = getMapiClient();
2419
- return await fetchAllPages(
2420
- (page) => client.components.list({
2421
- path: {
2422
- space_id: Number(spaceId)
2423
- },
2424
- query: {
2425
- page
2426
- },
2427
- throwOnError: true
2428
- }),
2429
- (data) => data?.components ?? []
2430
- );
2426
+ const { data } = await client.components.list({
2427
+ path: {
2428
+ space_id: Number(spaceId)
2429
+ },
2430
+ throwOnError: true
2431
+ });
2432
+ return data?.components ?? [];
2431
2433
  } catch (error) {
2432
2434
  handleAPIError("pull_components", error);
2433
2435
  }
@@ -2435,19 +2437,16 @@ const fetchComponents = async (spaceId) => {
2435
2437
  const fetchComponent = async (spaceId, componentName) => {
2436
2438
  try {
2437
2439
  const client = getMapiClient();
2438
- const matches = await fetchAllPages(
2439
- (page) => client.components.list({
2440
- path: {
2441
- space_id: Number(spaceId)
2442
- },
2443
- query: {
2444
- page,
2445
- search: componentName
2446
- },
2447
- throwOnError: true
2448
- }),
2449
- (data) => data?.components ?? []
2450
- );
2440
+ const { data } = await client.components.list({
2441
+ path: {
2442
+ space_id: Number(spaceId)
2443
+ },
2444
+ query: {
2445
+ search: componentName
2446
+ },
2447
+ throwOnError: true
2448
+ });
2449
+ const matches = data?.components ?? [];
2451
2450
  return matches.find((c) => c.name === componentName);
2452
2451
  } catch (error) {
2453
2452
  handleAPIError("pull_components", error, `Failed to fetch component ${componentName}`);
@@ -2514,7 +2513,7 @@ const saveComponentsToFiles = async (space, spaceData, options) => {
2514
2513
  const presetsFilePath = join(resolvedPath, suffix ? `${sanitizedName}.${DEFAULT_PRESETS_FILENAME}.${suffix}.json` : `${sanitizedName}.${DEFAULT_PRESETS_FILENAME}.json`);
2515
2514
  await saveToFile(presetsFilePath, JSON.stringify(componentPresets, null, 2));
2516
2515
  }
2517
- const groupsFilePath = join(resolvedPath, suffix ? `${DEFAULT_GROUPS_FILENAME}.${suffix}.json` : `${DEFAULT_GROUPS_FILENAME}.json`);
2516
+ const groupsFilePath = join(resolvedPath, suffix ? `${DEFAULT_GROUPS_FILENAME$1}.${suffix}.json` : `${DEFAULT_GROUPS_FILENAME$1}.json`);
2518
2517
  await saveToFile(groupsFilePath, JSON.stringify(groups, null, 2));
2519
2518
  const internalTagsFilePath = join(resolvedPath, suffix ? `${DEFAULT_TAGS_FILENAME}.${suffix}.json` : `${DEFAULT_TAGS_FILENAME}.json`);
2520
2519
  await saveToFile(internalTagsFilePath, JSON.stringify(internalTags, null, 2));
@@ -2524,7 +2523,7 @@ const saveComponentsToFiles = async (space, spaceData, options) => {
2524
2523
  const componentsFilePath = join(resolvedPath, suffix ? `${filename}.${suffix}.json` : `${filename}.json`);
2525
2524
  await saveToFile(componentsFilePath, JSON.stringify(components, null, 2));
2526
2525
  if (groups.length > 0) {
2527
- const groupsFilePath = join(resolvedPath, suffix ? `${DEFAULT_GROUPS_FILENAME}.${suffix}.json` : `${DEFAULT_GROUPS_FILENAME}.json`);
2526
+ const groupsFilePath = join(resolvedPath, suffix ? `${DEFAULT_GROUPS_FILENAME$1}.${suffix}.json` : `${DEFAULT_GROUPS_FILENAME$1}.json`);
2528
2527
  await saveToFile(groupsFilePath, JSON.stringify(groups, null, 2));
2529
2528
  }
2530
2529
  if (presets.length > 0) {
@@ -2540,6 +2539,27 @@ const saveComponentsToFiles = async (space, spaceData, options) => {
2540
2539
  }
2541
2540
  };
2542
2541
 
2542
+ function isSchemaField$1(value) {
2543
+ return typeof value === "object" && value !== null && "type" in value;
2544
+ }
2545
+ function toWritableSchema(schema) {
2546
+ if (!schema) {
2547
+ return void 0;
2548
+ }
2549
+ const result = {};
2550
+ for (const [key, value] of Object.entries(schema)) {
2551
+ if (isSchemaField$1(value)) {
2552
+ result[key] = value;
2553
+ }
2554
+ }
2555
+ return result;
2556
+ }
2557
+ function toRequestTagIds(tagIds) {
2558
+ if (!tagIds) {
2559
+ return void 0;
2560
+ }
2561
+ return tagIds.map((id) => Number(id));
2562
+ }
2543
2563
  const pushComponent = async (space, component) => {
2544
2564
  try {
2545
2565
  const client = getMapiClient();
@@ -2574,10 +2594,23 @@ const updateComponent = async (space, componentId, component) => {
2574
2594
  }
2575
2595
  };
2576
2596
  const upsertComponent = async (space, component, existingId) => {
2597
+ const { name, display_name, schema, is_root, is_nestable, component_group_uuid, color, icon, preview_field, internal_tag_ids } = component;
2598
+ const payload = {
2599
+ name,
2600
+ display_name: display_name ?? void 0,
2601
+ schema: toWritableSchema(schema),
2602
+ is_root,
2603
+ is_nestable,
2604
+ component_group_uuid: component_group_uuid ?? void 0,
2605
+ color: color ?? void 0,
2606
+ icon: icon ?? void 0,
2607
+ preview_field: preview_field ?? void 0,
2608
+ internal_tag_ids: toRequestTagIds(internal_tag_ids)
2609
+ };
2577
2610
  if (existingId) {
2578
- return await updateComponent(space, existingId, component);
2611
+ return await updateComponent(space, existingId, payload);
2579
2612
  } else {
2580
- return await pushComponent(space, component);
2613
+ return await pushComponent(space, payload);
2581
2614
  }
2582
2615
  };
2583
2616
  const pushComponentGroup = async (space, componentGroup) => {
@@ -2629,7 +2662,14 @@ const pushComponentPreset = async (space, preset) => {
2629
2662
  space_id: Number(space)
2630
2663
  },
2631
2664
  body: {
2632
- preset
2665
+ preset: {
2666
+ ...preset,
2667
+ preset: preset.preset ?? void 0,
2668
+ image: preset.image ?? void 0,
2669
+ color: preset.color ?? void 0,
2670
+ icon: preset.icon ?? void 0,
2671
+ description: preset.description ?? void 0
2672
+ }
2633
2673
  },
2634
2674
  throwOnError: true
2635
2675
  });
@@ -2646,7 +2686,14 @@ const updateComponentPreset = async (space, presetId, preset) => {
2646
2686
  space_id: Number(space)
2647
2687
  },
2648
2688
  body: {
2649
- preset
2689
+ preset: {
2690
+ ...preset,
2691
+ preset: preset.preset ?? void 0,
2692
+ image: preset.image ?? void 0,
2693
+ color: preset.color ?? void 0,
2694
+ icon: preset.icon ?? void 0,
2695
+ description: preset.description ?? void 0
2696
+ }
2650
2697
  },
2651
2698
  throwOnError: true
2652
2699
  });
@@ -2680,7 +2727,7 @@ const pushComponentInternalTag = async (space, componentInternalTag) => {
2680
2727
  path: {
2681
2728
  space_id: Number(space)
2682
2729
  },
2683
- body: componentInternalTag,
2730
+ body: { internal_tag: componentInternalTag },
2684
2731
  throwOnError: true
2685
2732
  });
2686
2733
  return data.internal_tag;
@@ -2695,7 +2742,7 @@ const updateComponentInternalTag = async (space, tagId, componentInternalTag) =>
2695
2742
  path: {
2696
2743
  space_id: Number(space)
2697
2744
  },
2698
- body: componentInternalTag,
2745
+ body: { internal_tag: componentInternalTag },
2699
2746
  throwOnError: true
2700
2747
  });
2701
2748
  return data.internal_tag;
@@ -3951,16 +3998,14 @@ const fetchSpace = async (spaceId) => {
3951
3998
  handleAPIError("pull_spaces", error, `Failed to fetch space ${spaceId}`);
3952
3999
  }
3953
4000
  };
3954
- const createSpace = async (space) => {
4001
+ const createSpace = async (space, query) => {
3955
4002
  try {
3956
- const { in_org, assign_partner, ...spaceData } = space;
3957
4003
  const client = getMapiClient();
3958
4004
  const { data } = await client.spaces.create({
3959
4005
  body: {
3960
- space: spaceData,
3961
- ...in_org && { in_org },
3962
- ...assign_partner && { assign_partner }
3963
- }
4006
+ space
4007
+ },
4008
+ query
3964
4009
  });
3965
4010
  return data?.space;
3966
4011
  } catch (error) {
@@ -3971,10 +4016,10 @@ const createSpace = async (space) => {
3971
4016
  const fetchLanguages = async (spaceId) => {
3972
4017
  try {
3973
4018
  const space = await fetchSpace(spaceId);
3974
- if (space?.default_lang_name !== void 0 && space?.languages?.length) {
4019
+ if (space?.default_lang_name && space?.languages?.length) {
3975
4020
  return {
3976
- default_lang_name: space?.default_lang_name,
3977
- languages: space?.languages
4021
+ default_lang_name: space.default_lang_name,
4022
+ languages: space.languages
3978
4023
  };
3979
4024
  }
3980
4025
  } catch (error) {
@@ -3994,8 +4039,8 @@ const saveLanguagesToFile = async (space, internationalizationOptions, options)
3994
4039
  }
3995
4040
  };
3996
4041
 
3997
- const program$9 = getProgram();
3998
- const languagesCommand = program$9.command(commands.LANGUAGES).alias("lang").description(`Manage your space's languages`);
4042
+ const program$a = getProgram();
4043
+ const languagesCommand = program$a.command(commands.LANGUAGES).alias("lang").description(`Manage your space's languages`);
3999
4044
  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");
4000
4045
  pullCmd$3.action(async (options, command) => {
4001
4046
  konsola.title(`${commands.LANGUAGES}`, colorPalette.LANGUAGES);
@@ -4041,8 +4086,8 @@ pullCmd$3.action(async (options, command) => {
4041
4086
  konsola.br();
4042
4087
  });
4043
4088
 
4044
- const program$8 = getProgram();
4045
- const migrationsCommand = program$8.command(commands.MIGRATIONS).alias("mig").description(`Manage your space's migrations`);
4089
+ const program$9 = getProgram();
4090
+ const migrationsCommand = program$9.command(commands.MIGRATIONS).alias("mig").description(`Manage your space's migrations`);
4046
4091
 
4047
4092
  const getMigrationTemplate = () => {
4048
4093
  return `export default function (block) {
@@ -4146,7 +4191,7 @@ const fetchStories = async (spaceId, params) => {
4146
4191
  const fetchStory = async (spaceId, storyId) => {
4147
4192
  try {
4148
4193
  const client = getMapiClient();
4149
- const { data } = await client.stories.get(storyId, {
4194
+ const { data } = await client.stories.get(Number(storyId), {
4150
4195
  path: {
4151
4196
  space_id: Number(spaceId)
4152
4197
  },
@@ -4191,9 +4236,11 @@ const updateStory = async (spaceId, storyId, payload) => {
4191
4236
  ...payload.story,
4192
4237
  // StoryUpdate2 expects `parent_id?: number`; normalize null → undefined.
4193
4238
  parent_id: payload.story.parent_id ?? void 0
4194
- },
4195
- force_update: payload.force_update === "1" ? "1" : "0",
4196
- ...payload.publish ? { publish: payload.publish } : {}
4239
+ }
4240
+ },
4241
+ query: {
4242
+ force_update: payload.force_update === "1",
4243
+ ...payload.publish ? { publish: Boolean(payload.publish) } : {}
4197
4244
  },
4198
4245
  throwOnError: true
4199
4246
  });
@@ -4290,6 +4337,35 @@ const prefetchTargetStoriesByKeys = async (spaceId, keys, options) => {
4290
4337
  return result;
4291
4338
  };
4292
4339
 
4340
+ function parseFilterQuery(input) {
4341
+ const trimmed = input.trim();
4342
+ if (!trimmed) {
4343
+ return {};
4344
+ }
4345
+ if (trimmed.startsWith("{")) {
4346
+ return JSON.parse(trimmed);
4347
+ }
4348
+ const result = {};
4349
+ for (const clause of trimmed.split("&")) {
4350
+ if (!clause) {
4351
+ continue;
4352
+ }
4353
+ const eq = clause.indexOf("=");
4354
+ if (eq === -1) {
4355
+ continue;
4356
+ }
4357
+ const path = clause.slice(0, eq);
4358
+ const value = clause.slice(eq + 1);
4359
+ const keys = [...path.matchAll(/\[([^\]]+)\]/g)].map((match) => match[1]);
4360
+ if (keys.length < 2) {
4361
+ continue;
4362
+ }
4363
+ const [field, operation] = keys;
4364
+ result[field] = { ...result[field], [operation]: value };
4365
+ }
4366
+ return result;
4367
+ }
4368
+
4293
4369
  const PIPELINE_BACKPRESSURE_MULTIPLIER = 2;
4294
4370
  const DEFAULT_PIPELINE_BACKPRESSURE = 12;
4295
4371
  function createPipelineBackpressureLock(limit) {
@@ -4313,16 +4389,13 @@ const ERROR_CODES = {
4313
4389
  async function* storiesIterator(spaceId, params, onTotal) {
4314
4390
  try {
4315
4391
  let perPage = 500;
4316
- const transformedParams = {
4317
- ...params
4318
- };
4319
- if (params?.componentName && typeof params.componentName === "string") {
4320
- transformedParams.contain_component = params.componentName;
4321
- delete transformedParams.componentName;
4392
+ const { componentName, query, ...rest } = params ?? {};
4393
+ const transformedParams = { ...rest };
4394
+ if (componentName) {
4395
+ transformedParams.contain_component = componentName;
4322
4396
  }
4323
- if (params?.query && typeof params.query === "string") {
4324
- transformedParams.filter_query = params.query.startsWith("filter_query") ? params.query : `filter_query${params.query}`;
4325
- delete transformedParams.query;
4397
+ if (query) {
4398
+ transformedParams.filter_query = parseFilterQuery(query);
4326
4399
  }
4327
4400
  const result = await fetchStories(spaceId, {
4328
4401
  ...transformedParams,
@@ -4647,8 +4720,8 @@ class MigrationStream extends Transform {
4647
4720
  id: story.id,
4648
4721
  name: story.name || "",
4649
4722
  content: story.content,
4650
- published: story.published,
4651
- unpublished_changes: story.unpublished_changes
4723
+ published: story.published ?? void 0,
4724
+ unpublished_changes: story.unpublished_changes ?? void 0
4652
4725
  },
4653
4726
  migrationTimestamp: this.timestamp,
4654
4727
  migrationNames
@@ -4664,8 +4737,8 @@ class MigrationStream extends Transform {
4664
4737
  storyId: story.id,
4665
4738
  name: story.name,
4666
4739
  content: storyContent,
4667
- published: story.published,
4668
- unpublished_changes: story.unpublished_changes
4740
+ published: story.published ?? void 0,
4741
+ unpublished_changes: story.unpublished_changes ?? void 0
4669
4742
  };
4670
4743
  } else if (processed && !contentChanged) {
4671
4744
  this.results.skipped.push({
@@ -4807,7 +4880,6 @@ class UpdateStream extends Writable {
4807
4880
  const payload = {
4808
4881
  story: {
4809
4882
  content,
4810
- id: storyId,
4811
4883
  name: storyName
4812
4884
  },
4813
4885
  force_update: "1"
@@ -5056,7 +5128,6 @@ rollbackCmd.action(async (migrationFile, _options, command) => {
5056
5128
  const payload = {
5057
5129
  story: {
5058
5130
  content: story.content,
5059
- id: story.storyId,
5060
5131
  name: story.name
5061
5132
  },
5062
5133
  force_update: "1"
@@ -5096,8 +5167,8 @@ rollbackCmd.action(async (migrationFile, _options, command) => {
5096
5167
  }
5097
5168
  });
5098
5169
 
5099
- const program$7 = getProgram();
5100
- const typesCommand = program$7.command(commands.TYPES).alias("ts").description(`Generate types d.ts for your component schemas`);
5170
+ const program$8 = getProgram();
5171
+ const typesCommand = program$8.command(commands.TYPES).alias("ts").description(`Generate types d.ts for your component schemas`);
5101
5172
 
5102
5173
  const getAssetJSONSchema = (title) => ({
5103
5174
  $id: "#/asset",
@@ -5708,7 +5779,7 @@ const getPropertyTypeAnnotation = (property, prefix, suffix) => {
5708
5779
  }
5709
5780
  };
5710
5781
  function getStoryType(property, prefix, suffix) {
5711
- return `${STORY_TYPE}<${prefix ?? ""}${capitalize(toCamelCase(property))}${suffix ?? ""}>`;
5782
+ return `${STORY_TYPE}<${prefix ?? ""}${capitalize(toCamelCase$1(property))}${suffix ?? ""}>`;
5712
5783
  }
5713
5784
  const getComponentType = (componentName, options) => {
5714
5785
  const prefix = options.typePrefix ?? "";
@@ -6099,7 +6170,7 @@ const deleteDatasourceEntry = async (spaceId, entryId) => {
6099
6170
  handleAPIError("delete_datasource_entry", error, `Failed to delete datasource entry ${entryId}`);
6100
6171
  }
6101
6172
  };
6102
- function isDatasource(item) {
6173
+ function isDatasource$1(item) {
6103
6174
  return typeof item === "object" && item !== null && "slug" in item && typeof item.slug === "string";
6104
6175
  }
6105
6176
  const readDatasourcesFiles = async (options) => {
@@ -6132,7 +6203,7 @@ const readDatasourcesFiles = async (options) => {
6132
6203
  continue;
6133
6204
  }
6134
6205
  for (const item of data) {
6135
- if (isDatasource(item)) {
6206
+ if (isDatasource$1(item)) {
6136
6207
  const existing = datasourceMap.get(item.slug);
6137
6208
  if (existing) {
6138
6209
  duplicates.push(`Datasource "${item.slug}" found in both "${existing.file}" and "${file}"`);
@@ -6232,8 +6303,8 @@ generateCmd.action(async (options, command) => {
6232
6303
  }
6233
6304
  });
6234
6305
 
6235
- const program$6 = getProgram();
6236
- const datasourcesCommand = program$6.command(commands.DATASOURCES).alias("ds").description(`Manage your space's datasources`);
6306
+ const program$7 = getProgram();
6307
+ const datasourcesCommand = program$7.command(commands.DATASOURCES).alias("ds").description(`Manage your space's datasources`);
6237
6308
 
6238
6309
  const DEFAULT_DATASOURCES_FILENAME = "datasources";
6239
6310
 
@@ -6844,13 +6915,13 @@ async function promptForLogin(verbose) {
6844
6915
  return null;
6845
6916
  }
6846
6917
  }
6847
- const program$5 = getProgram();
6848
- 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(
6918
+ const program$6 = getProgram();
6919
+ 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(
6849
6920
  "-r, --region <region>",
6850
6921
  `The region to apply to the generated project template (does not affect space creation).`
6851
6922
  ).action(async (projectPath, options) => {
6852
6923
  ui.title(`${commands.CREATE}`, colorPalette.CREATE);
6853
- const verbose = program$5.opts().verbose;
6924
+ const verbose = program$6.opts().verbose;
6854
6925
  const { template, blueprint, token } = options;
6855
6926
  if (options.region && !isRegion(options.region)) {
6856
6927
  handleError(new CommandError(`The provided region: ${options.region} is not valid. Please use one of the following values: ${Object.values(regions).join(" | ")}`));
@@ -7023,13 +7094,13 @@ program$5.command(`${commands.CREATE} [project-path]`).alias("c").description(`S
7023
7094
  name: toHumanReadable(projectName),
7024
7095
  domain: blueprintDomain
7025
7096
  };
7097
+ const createSpaceQuery = {};
7026
7098
  if (whereToCreateSpace === "org") {
7027
- spaceToCreate.org = userData.org;
7028
- spaceToCreate.in_org = true;
7099
+ createSpaceQuery.in_org = true;
7029
7100
  } else if (whereToCreateSpace === "partner") {
7030
- spaceToCreate.assign_partner = true;
7101
+ createSpaceQuery.assign_partner = true;
7031
7102
  }
7032
- createdSpace = await createSpace(spaceToCreate);
7103
+ createdSpace = await createSpace(spaceToCreate, createSpaceQuery);
7033
7104
  spinnerSpace.succeed(`Space "${chalk.hex(colorPalette.PRIMARY)(toHumanReadable(projectName))}" created successfully`);
7034
7105
  if (createdSpace?.first_token) {
7035
7106
  await handleEnvFileCreation(resolvedPath, createdSpace.first_token, region);
@@ -7069,8 +7140,8 @@ program$5.command(`${commands.CREATE} [project-path]`).alias("c").description(`S
7069
7140
  ui.br();
7070
7141
  });
7071
7142
 
7072
- const program$4 = getProgram();
7073
- const logsCommand = program$4.command(commands.LOGS).alias("lg").description(`Inspect and manage logs.`);
7143
+ const program$5 = getProgram();
7144
+ const logsCommand = program$5.command(commands.LOGS).alias("lg").description(`Inspect and manage logs.`);
7074
7145
 
7075
7146
  const listCmd$1 = logsCommand.command("list").description("List logs").option("-s, --space <space>", "space ID");
7076
7147
  listCmd$1.action(async (_options, command) => {
@@ -7095,8 +7166,8 @@ pruneCmd$1.action(async (options, command) => {
7095
7166
  ui.info(`Deleted ${deletedFilesCount} log file${deletedFilesCount === 1 ? "" : "s"}`);
7096
7167
  });
7097
7168
 
7098
- const program$3 = getProgram();
7099
- const reportsCommand = program$3.command(commands.REPORTS).alias("rp").description("Inspect and manage reports.");
7169
+ const program$4 = getProgram();
7170
+ const reportsCommand = program$4.command(commands.REPORTS).alias("rp").description("Inspect and manage reports.");
7100
7171
 
7101
7172
  const listCmd = reportsCommand.command("list").description("List reports").option("-s, --space <space>", "space ID");
7102
7173
  listCmd.action(async (_options, command) => {
@@ -7121,8 +7192,8 @@ pruneCmd.action(async (options, command) => {
7121
7192
  ui.info(`Deleted ${deletedFilesCount} report file${deletedFilesCount === 1 ? "" : "s"}`);
7122
7193
  });
7123
7194
 
7124
- const program$2 = getProgram();
7125
- const assetsCommand = program$2.command(commands.ASSETS).description(`Manage your space's assets`);
7195
+ const program$3 = getProgram();
7196
+ const assetsCommand = program$3.command(commands.ASSETS).description(`Manage your space's assets`);
7126
7197
 
7127
7198
  const fetchAssets = async ({ spaceId, params }) => {
7128
7199
  try {
@@ -7170,7 +7241,7 @@ const createAssetInternalTag = async (spaceId, name) => {
7170
7241
  const client = getMapiClient();
7171
7242
  const { data } = await client.internalTags.create({
7172
7243
  path: { space_id: Number(spaceId) },
7173
- body: { name, object_type: "asset" },
7244
+ body: { internal_tag: { name, object_type: "asset" } },
7174
7245
  throwOnError: true
7175
7246
  });
7176
7247
  const tag = data?.internal_tag;
@@ -7294,7 +7365,7 @@ const updateAsset = async (id, asset, { spaceId, fileBuffer }) => {
7294
7365
  const createAsset = async (asset, fileBuffer, { spaceId }) => {
7295
7366
  try {
7296
7367
  const client = getMapiClient();
7297
- const { id: _id, ...assetBody } = asset;
7368
+ const { id: _id, filename: _filename, ...assetBody } = asset;
7298
7369
  return await client.assets.create({
7299
7370
  body: assetBody,
7300
7371
  file: fileBuffer,
@@ -7354,6 +7425,26 @@ const parseAssetData = (raw) => {
7354
7425
  throw new Error(`Invalid --data JSON: ${toError(maybeError).message}`);
7355
7426
  }
7356
7427
  };
7428
+ const toAssetUpload = (partial, shortFilename) => {
7429
+ const nullToUndef = (value) => value ?? void 0;
7430
+ return {
7431
+ id: partial.id,
7432
+ filename: partial.filename,
7433
+ short_filename: shortFilename,
7434
+ asset_folder_id: nullToUndef(partial.asset_folder_id),
7435
+ ext_id: nullToUndef(partial.ext_id),
7436
+ alt: nullToUndef(partial.alt),
7437
+ copyright: nullToUndef(partial.copyright),
7438
+ title: nullToUndef(partial.title),
7439
+ source: nullToUndef(partial.source),
7440
+ expire_at: nullToUndef(partial.expire_at),
7441
+ publish_at: nullToUndef(partial.publish_at),
7442
+ focus: nullToUndef(partial.focus),
7443
+ is_private: nullToUndef(partial.is_private),
7444
+ internal_tag_ids: partial.internal_tag_ids,
7445
+ meta_data: nullToUndef(partial.meta_data)
7446
+ };
7447
+ };
7357
7448
  const getSidecarFilename = (assetBinaryPath) => {
7358
7449
  return join(dirname(assetBinaryPath), `${basename(assetBinaryPath, extname(assetBinaryPath))}.json`);
7359
7450
  };
@@ -7382,7 +7473,7 @@ const isRemoteSource = (assetBinaryPath) => {
7382
7473
  return false;
7383
7474
  }
7384
7475
  };
7385
- const isValidManifestEntry = (entry) => Boolean(typeof entry.old_id === "number" && typeof entry.new_id === "number" && entry.old_filename && entry.new_filename);
7476
+ const isValidManifestEntry = (entry) => Boolean(typeof entry.old_id === "number" && typeof entry.new_id === "number" && entry.new_filename);
7386
7477
  const loadAssetMap = async (manifestFile) => {
7387
7478
  const manifest = await loadManifest(manifestFile);
7388
7479
  const entries = manifest.filter(isValidManifestEntry).map((e) => [
@@ -7762,8 +7853,10 @@ const readLocalAssetsStream = ({
7762
7853
  const sidecar = await loadSidecarAssetData(binaryFilePath);
7763
7854
  const shortFilename = sidecar.short_filename || (sidecar.filename ? basename(sidecar.filename) : void 0) || file;
7764
7855
  const asset = {
7765
- ...sidecar,
7766
- short_filename: shortFilename
7856
+ ...toAssetUpload(sidecar, shortFilename),
7857
+ // Carry the read-only tag detail for source→target tag-name
7858
+ // translation in `processAsset`; it is stripped before the API call.
7859
+ ...sidecar.internal_tags_list ? { internal_tags_list: sidecar.internal_tags_list } : {}
7767
7860
  };
7768
7861
  const fileBuffer = await readFile$1(binaryFilePath);
7769
7862
  const sidecarPath = getSidecarFilename(binaryFilePath);
@@ -7847,7 +7940,7 @@ const mapInternalTagIds = (sourceIds, sourceTags, assetInternalTagsByName, onUnm
7847
7940
  const sourceName = sourceNamesById.get(sourceId);
7848
7941
  const targetId = typeof sourceName === "string" ? assetInternalTagsByName.get(sourceName) : void 0;
7849
7942
  if (typeof targetId === "number") {
7850
- mapped.push(String(targetId));
7943
+ mapped.push(targetId);
7851
7944
  } else {
7852
7945
  onUnmappedTag?.({ sourceId, name: sourceName });
7853
7946
  }
@@ -7899,10 +7992,11 @@ const processAsset = async ({
7899
7992
  const remoteAssetId = hasId(localAsset) ? maps.assets.get(localAsset.id)?.new.id || localAsset.id : void 0;
7900
7993
  const remoteAsset = remoteAssetId ? await transports.getAsset(remoteAssetId) : null;
7901
7994
  const sourceTags = localAsset.internal_tags_list;
7902
- const resolveInternalTagIds = (sourceIds) => maps.assetInternalTagsByName ? mapInternalTagIds(sourceIds, sourceTags, maps.assetInternalTagsByName, onUnmappedTag) : (sourceIds ?? []).map((id) => String(id));
7995
+ const resolveInternalTagIds = (sourceIds) => maps.assetInternalTagsByName ? mapInternalTagIds(sourceIds, sourceTags, maps.assetInternalTagsByName, onUnmappedTag) : (sourceIds ?? []).map((id) => Number(id));
7903
7996
  let newRemoteAsset;
7904
7997
  let status;
7905
7998
  if (remoteAsset) {
7999
+ const nullToUndef = (v) => v ?? void 0;
7906
8000
  const updatePayload = {
7907
8001
  asset_folder_id: remoteFolderId,
7908
8002
  alt: "alt" in localAsset ? localAsset.alt : remoteAsset.alt,
@@ -7911,24 +8005,28 @@ const processAsset = async ({
7911
8005
  source: "source" in localAsset ? localAsset.source : remoteAsset.source,
7912
8006
  is_private: "is_private" in localAsset ? localAsset.is_private : remoteAsset.is_private,
7913
8007
  focus: "focus" in localAsset ? localAsset.focus : remoteAsset.focus,
7914
- expire_at: "expire_at" in localAsset ? localAsset.expire_at : remoteAsset.expire_at,
7915
- publish_at: "publish_at" in localAsset ? localAsset.publish_at : remoteAsset.publish_at,
7916
- internal_tag_ids: "internal_tag_ids" in localAsset ? resolveInternalTagIds(localAsset.internal_tag_ids) : remoteAsset.internal_tag_ids,
7917
- meta_data: "meta_data" in localAsset ? localAsset.meta_data : remoteAsset.meta_data
8008
+ expire_at: nullToUndef("expire_at" in localAsset ? localAsset.expire_at : remoteAsset.expire_at),
8009
+ publish_at: nullToUndef("publish_at" in localAsset ? localAsset.publish_at : remoteAsset.publish_at),
8010
+ internal_tag_ids: localAsset.internal_tag_ids ? resolveInternalTagIds(localAsset.internal_tag_ids) : remoteAsset.internal_tag_ids,
8011
+ meta_data: nullToUndef("meta_data" in localAsset ? localAsset.meta_data : remoteAsset.meta_data)
7918
8012
  };
7919
8013
  await transports.updateAsset(
7920
8014
  remoteAsset.id,
7921
8015
  { ...updatePayload, short_filename: remoteAsset.short_filename },
7922
8016
  fileBuffer
7923
8017
  );
7924
- newRemoteAsset = { ...remoteAsset, ...updatePayload };
8018
+ newRemoteAsset = {
8019
+ ...remoteAsset,
8020
+ ...updatePayload,
8021
+ is_private: updatePayload.is_private ?? remoteAsset.is_private
8022
+ };
7925
8023
  status = "updated";
7926
8024
  } else if (hasShortFilename(localAsset)) {
7927
- const { internal_tags_list: _internalTagsList, internal_tag_ids: _sourceTagIds, ...rest } = localAsset;
7928
- const mappedTagIds = "internal_tag_ids" in localAsset ? resolveInternalTagIds(localAsset.internal_tag_ids) : void 0;
8025
+ const mappedTagIds = localAsset.internal_tag_ids ? resolveInternalTagIds(localAsset.internal_tag_ids) : void 0;
8026
+ const { internal_tag_ids: _sourceTagIds, ...uploadBase } = toAssetUpload(localAsset, localAsset.short_filename);
7929
8027
  const createPayload = {
7930
- ...rest,
7931
- asset_folder_id: remoteFolderId,
8028
+ ...uploadBase,
8029
+ asset_folder_id: remoteFolderId ?? void 0,
7932
8030
  ...mappedTagIds !== void 0 ? { internal_tag_ids: mappedTagIds } : {}
7933
8031
  };
7934
8032
  newRemoteAsset = await transports.createAsset(createPayload, fileBuffer);
@@ -8161,8 +8259,8 @@ const traverseAndMapBySchema = (data, {
8161
8259
  const dataNew = { ...data };
8162
8260
  for (const [fieldName, fieldValue] of Object.entries(data)) {
8163
8261
  const fieldSchema = schema[fieldName.replace(/__i18n__.*/, "")];
8164
- const fieldType = fieldSchema && typeof fieldSchema === "object" && "type" in fieldSchema && fieldSchema.type;
8165
- const fieldRefMapper = typeof fieldType === "string" && fieldRefMappers2[fieldType];
8262
+ const fieldType = fieldSchema && typeof fieldSchema === "object" && "type" in fieldSchema ? fieldSchema.type : void 0;
8263
+ const fieldRefMapper = typeof fieldType === "string" ? fieldRefMappers2[fieldType] : void 0;
8166
8264
  if (fieldRefMapper) {
8167
8265
  dataNew[fieldName] = fieldRefMapper(fieldValue, {
8168
8266
  schema: fieldSchema,
@@ -8227,6 +8325,9 @@ const richtextFieldRefMapper = (data, { schemas, maps, fieldRefMappers: fieldRef
8227
8325
  fieldRefMappers: fieldRefMappers2
8228
8326
  });
8229
8327
  const multilinkFieldRefMapper = (data, { maps }) => {
8328
+ if (!data || typeof data !== "object") {
8329
+ return data;
8330
+ }
8230
8331
  if (data.linktype !== "story") {
8231
8332
  return data;
8232
8333
  }
@@ -8246,6 +8347,9 @@ const bloksFieldRefMapper = (data, { schemas, maps, fieldRefMappers: fieldRefMap
8246
8347
  }));
8247
8348
  };
8248
8349
  const assetFieldRefMapper = (data, { maps }) => {
8350
+ if (!data || typeof data !== "object") {
8351
+ return data;
8352
+ }
8249
8353
  const mappedAsset = typeof data.id === "number" ? maps.assets?.get(data.id) : void 0;
8250
8354
  if (!mappedAsset) {
8251
8355
  return data;
@@ -8263,7 +8367,7 @@ const multiassetFieldRefMapper = (data, options) => {
8263
8367
  return data.map((d) => assetFieldRefMapper(d, options));
8264
8368
  };
8265
8369
  const optionsFieldRefMapper = (data, { schema, maps }) => {
8266
- if (schema.source !== "internal_stories" || !Array.isArray(data)) {
8370
+ if (!schema || !("source" in schema) || schema.source !== "internal_stories" || !Array.isArray(data)) {
8267
8371
  return data;
8268
8372
  }
8269
8373
  return data.map((d) => maps.stories?.get(d) || d);
@@ -9112,11 +9216,7 @@ pushCmd$1.action(async (assetInput, options, command) => {
9112
9216
  const sourceBasename = isRemoteSource(assetBinaryPath) ? basename(new URL(assetBinaryPath).pathname) : basename(assetBinaryPath);
9113
9217
  const shortFilename = options.shortFilename || assetDataPartial.short_filename || sourceBasename;
9114
9218
  const folderId = options.folder ? Number(options.folder) : void 0;
9115
- assetData = {
9116
- ...assetDataPartial,
9117
- short_filename: shortFilename,
9118
- asset_folder_id: folderId
9119
- };
9219
+ assetData = { ...toAssetUpload(assetDataPartial, shortFilename), asset_folder_id: folderId };
9120
9220
  }
9121
9221
  const getAssetTransport = makeGetAssetAPITransport({ spaceId: targetSpace });
9122
9222
  const createAssetTransport = options.dryRun ? async (asset) => asset : makeCreateAssetAPITransport({ spaceId: targetSpace });
@@ -9276,8 +9376,8 @@ transferCmd.action(async (assetIds, options, command) => {
9276
9376
  process.exitCode = summary.failed > 0 ? 1 : 0;
9277
9377
  });
9278
9378
 
9279
- const program$1 = getProgram();
9280
- const storiesCommand = program$1.command(commands.STORIES).description(`Manage your space's stories`);
9379
+ const program$2 = getProgram();
9380
+ const storiesCommand = program$2.command(commands.STORIES).description(`Manage your space's stories`);
9281
9381
 
9282
9382
  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.`);
9283
9383
  pullCmd.action(async (options, command) => {
@@ -9313,7 +9413,7 @@ pullCmd.action(async (options, command) => {
9313
9413
  fetchStoriesStream({
9314
9414
  spaceId: space,
9315
9415
  params: {
9316
- filter_query: options.query,
9416
+ filter_query: options.query ? parseFilterQuery(options.query) : void 0,
9317
9417
  starts_with: options.startsWith
9318
9418
  },
9319
9419
  setTotalPages: (totalPages) => {
@@ -9782,6 +9882,1965 @@ pushCmd.action(async (options, command) => {
9782
9882
  }
9783
9883
  });
9784
9884
 
9885
+ const program$1 = getProgram();
9886
+ const schemaCommand = program$1.command(commands.SCHEMA).description(`Manage your space's schema from code`);
9887
+
9888
+ const COMPONENT_STRIP_KEYS = /* @__PURE__ */ new Set([
9889
+ "id",
9890
+ "created_at",
9891
+ "updated_at",
9892
+ "real_name",
9893
+ // API-computed display/technical name, read-only
9894
+ "preset_id",
9895
+ // Instance-level preset selection, not part of schema definition
9896
+ "all_presets",
9897
+ // Computed list of presets, managed via /presets API
9898
+ "internal_tags_list",
9899
+ // Read-only expanded form of internal_tag_ids ({id, name} objects)
9900
+ "content_type_asset_preview",
9901
+ // Read-only, not in ComponentCreate/ComponentUpdate
9902
+ "image",
9903
+ // Read-only preview image URL
9904
+ "preview_tmpl",
9905
+ // Read-only preview template
9906
+ "metadata"
9907
+ // Not in current API types, stripped defensively
9908
+ ]);
9909
+ const DATASOURCE_STRIP_KEYS = /* @__PURE__ */ new Set(["id", "created_at", "updated_at"]);
9910
+ const DATASOURCE_DIMENSION_STRIP_KEYS = /* @__PURE__ */ new Set(["id", "datasource_id", "created_at", "updated_at"]);
9911
+ const FOLDER_INIT_STRIP_KEYS = /* @__PURE__ */ new Set(["id"]);
9912
+ const FOLDER_STRIP_KEYS = /* @__PURE__ */ new Set(["id", "uuid"]);
9913
+ const DATASOURCE_DEFAULTS = {
9914
+ dimensions: []
9915
+ };
9916
+ const COMPONENT_DEFAULTS = {
9917
+ display_name: "",
9918
+ description: "",
9919
+ color: "",
9920
+ icon: "",
9921
+ preview_field: "",
9922
+ internal_tag_ids: []
9923
+ };
9924
+ function applyDefaults(entity, defaults) {
9925
+ const result = { ...entity };
9926
+ for (const [key, defaultValue] of Object.entries(defaults)) {
9927
+ if (result[key] === void 0 || result[key] === null) {
9928
+ Object.assign(result, { [key]: defaultValue });
9929
+ }
9930
+ }
9931
+ return result;
9932
+ }
9933
+ const INDENT = " ";
9934
+ function formatValue(value, depth) {
9935
+ const indent = INDENT.repeat(depth);
9936
+ const innerIndent = INDENT.repeat(depth + 1);
9937
+ if (value === null || value === void 0) {
9938
+ return String(value);
9939
+ }
9940
+ if (typeof value === "string") {
9941
+ return `'${value.replace(/'/g, "\\'")}'`;
9942
+ }
9943
+ if (typeof value === "number" || typeof value === "boolean") {
9944
+ return String(value);
9945
+ }
9946
+ if (Array.isArray(value)) {
9947
+ if (value.length === 0) {
9948
+ return "[]";
9949
+ }
9950
+ const items = value.map((item) => `${innerIndent}${formatValue(item, depth + 1)},`);
9951
+ return `[
9952
+ ${items.join("\n")}
9953
+ ${indent}]`;
9954
+ }
9955
+ if (typeof value === "object") {
9956
+ const entries = Object.entries(value).filter(([, v]) => v !== void 0 && v !== null).sort(([a], [b]) => a.localeCompare(b));
9957
+ if (entries.length === 0) {
9958
+ return "{}";
9959
+ }
9960
+ const props = entries.map(
9961
+ ([key, val]) => `${innerIndent}${key}: ${formatValue(val, depth + 1)},`
9962
+ );
9963
+ return `{
9964
+ ${props.join("\n")}
9965
+ ${indent}}`;
9966
+ }
9967
+ return String(value);
9968
+ }
9969
+ function fileTimestamp(iso) {
9970
+ return iso.replace(/\D/g, "").slice(0, 14);
9971
+ }
9972
+ function displayPath(filePath, userPath) {
9973
+ return userPath && isAbsolute(userPath) ? filePath : relative(process.cwd(), filePath);
9974
+ }
9975
+ function stripKeys(obj, keysToStrip) {
9976
+ const result = {};
9977
+ for (const [key, value] of Object.entries(obj)) {
9978
+ if (!keysToStrip.has(key) && value !== void 0 && value !== null) {
9979
+ result[key] = value;
9980
+ }
9981
+ }
9982
+ return result;
9983
+ }
9984
+
9985
+ function isObject(value) {
9986
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9987
+ }
9988
+ function isComponent(value) {
9989
+ return isObject(value) && typeof value.name === "string" && "schema" in value && isObject(value.schema);
9990
+ }
9991
+ function isDatasource(value) {
9992
+ return isObject(value) && typeof value.name === "string" && typeof value.slug === "string" && !("schema" in value);
9993
+ }
9994
+ function isComponentFolder(value) {
9995
+ return isObject(value) && typeof value.name === "string" && !("schema" in value) && !("slug" in value) && ("uuid" in value || "parent_id" in value);
9996
+ }
9997
+ function isSchemaObject(value) {
9998
+ return isObject(value) && ("blocks" in value || "blockFolders" in value || "datasources" in value);
9999
+ }
10000
+ function classifyExports(moduleExports) {
10001
+ const components = [];
10002
+ const componentFolders = [];
10003
+ const datasources = [];
10004
+ function collect(value) {
10005
+ if (isComponent(value)) {
10006
+ components.push(value);
10007
+ } else if (isDatasource(value)) {
10008
+ datasources.push(value);
10009
+ } else if (isComponentFolder(value)) {
10010
+ componentFolders.push(value);
10011
+ }
10012
+ }
10013
+ for (const value of Object.values(moduleExports)) {
10014
+ if (isSchemaObject(value)) {
10015
+ for (const group of Object.values(value)) {
10016
+ if (isObject(group)) {
10017
+ for (const entity of Object.values(group)) {
10018
+ collect(entity);
10019
+ }
10020
+ }
10021
+ }
10022
+ } else {
10023
+ collect(value);
10024
+ }
10025
+ }
10026
+ return { components, componentFolders, datasources };
10027
+ }
10028
+ async function loadSchema(entryPath) {
10029
+ const { createJiti } = await import('jiti');
10030
+ const jiti = createJiti(import.meta.url, {
10031
+ interopDefault: true
10032
+ });
10033
+ const absolutePath = (await import('pathe')).resolve(entryPath);
10034
+ const mod = await jiti.import(absolutePath);
10035
+ return classifyExports(mod);
10036
+ }
10037
+
10038
+ function sortSchemaByPos$1(schema) {
10039
+ const entries = Object.entries(schema).filter(([key]) => key !== "_uid" && key !== "component").sort(([, a], [, b]) => {
10040
+ const posA = typeof a.pos === "number" ? a.pos : Infinity;
10041
+ const posB = typeof b.pos === "number" ? b.pos : Infinity;
10042
+ return posA - posB;
10043
+ });
10044
+ return Object.fromEntries(
10045
+ entries.map(([key, field]) => {
10046
+ const { id, ...rest } = field;
10047
+ return [key, rest];
10048
+ })
10049
+ );
10050
+ }
10051
+ function serializeComponent(component) {
10052
+ const clean = stripKeys(component, COMPONENT_STRIP_KEYS);
10053
+ if (clean.schema && typeof clean.schema === "object") {
10054
+ clean.schema = sortSchemaByPos$1(clean.schema);
10055
+ }
10056
+ const ordered = {};
10057
+ if (clean.name !== void 0) {
10058
+ ordered.name = clean.name;
10059
+ }
10060
+ if (clean.display_name !== void 0) {
10061
+ ordered.display_name = clean.display_name;
10062
+ }
10063
+ if (clean.is_root !== void 0) {
10064
+ ordered.is_root = clean.is_root;
10065
+ }
10066
+ if (clean.is_nestable !== void 0) {
10067
+ ordered.is_nestable = clean.is_nestable;
10068
+ }
10069
+ const handled = /* @__PURE__ */ new Set(["name", "display_name", "is_root", "is_nestable", "schema"]);
10070
+ for (const [key, value] of Object.entries(clean).sort(([a], [b]) => a.localeCompare(b))) {
10071
+ if (!handled.has(key)) {
10072
+ ordered[key] = value;
10073
+ }
10074
+ }
10075
+ if (clean.schema !== void 0) {
10076
+ ordered.schema = clean.schema;
10077
+ }
10078
+ return `defineBlock(${formatValue(ordered, 0)})`;
10079
+ }
10080
+ function serializeDatasource(datasource) {
10081
+ const clean = stripKeys(datasource, DATASOURCE_STRIP_KEYS);
10082
+ if (Array.isArray(clean.dimensions)) {
10083
+ clean.dimensions = clean.dimensions.map(
10084
+ (dim) => typeof dim === "object" && dim !== null ? stripKeys(dim, DATASOURCE_DIMENSION_STRIP_KEYS) : dim
10085
+ );
10086
+ }
10087
+ const ordered = {};
10088
+ if (clean.name !== void 0) {
10089
+ ordered.name = clean.name;
10090
+ }
10091
+ if (clean.slug !== void 0) {
10092
+ ordered.slug = clean.slug;
10093
+ }
10094
+ const handled = /* @__PURE__ */ new Set(["name", "slug"]);
10095
+ for (const [key, value] of Object.entries(clean).sort(([a], [b]) => a.localeCompare(b))) {
10096
+ if (!handled.has(key)) {
10097
+ ordered[key] = value;
10098
+ }
10099
+ }
10100
+ return `defineDatasource(${formatValue(ordered, 0)})`;
10101
+ }
10102
+ function serializeComponentFolder(folder) {
10103
+ const clean = stripKeys(folder, FOLDER_STRIP_KEYS);
10104
+ const ordered = {};
10105
+ if (clean.name !== void 0) {
10106
+ ordered.name = clean.name;
10107
+ }
10108
+ const handled = /* @__PURE__ */ new Set(["name"]);
10109
+ for (const [key, value] of Object.entries(clean).sort(([a], [b]) => a.localeCompare(b))) {
10110
+ if (!handled.has(key)) {
10111
+ ordered[key] = value;
10112
+ }
10113
+ }
10114
+ return `defineBlockFolder(${formatValue(ordered, 0)})`;
10115
+ }
10116
+
10117
+ function diffEntity(type, name, localSerialized, remoteSerialized) {
10118
+ if (!remoteSerialized && localSerialized) {
10119
+ return { type, name, action: "create", diff: null, local: null, remote: null };
10120
+ }
10121
+ if (remoteSerialized && !localSerialized) {
10122
+ return { type, name, action: "stale", diff: null, local: null, remote: null };
10123
+ }
10124
+ if (localSerialized === remoteSerialized) {
10125
+ return { type, name, action: "unchanged", diff: null, local: null, remote: null };
10126
+ }
10127
+ const patch = createTwoFilesPatch(
10128
+ `remote/${name}`,
10129
+ `local/${name}`,
10130
+ remoteSerialized,
10131
+ localSerialized,
10132
+ "remote",
10133
+ "local"
10134
+ );
10135
+ return { type, name, action: "update", diff: patch, local: null, remote: null };
10136
+ }
10137
+ function diffSchema(local, remote) {
10138
+ const diffs = [];
10139
+ const processedComponentNames = /* @__PURE__ */ new Set();
10140
+ for (const comp of local.components) {
10141
+ processedComponentNames.add(comp.name);
10142
+ const remoteComp = remote.components.get(comp.name);
10143
+ const localSerialized = serializeComponent(applyDefaults(comp, COMPONENT_DEFAULTS));
10144
+ const remoteSerialized = remoteComp ? serializeComponent(applyDefaults(remoteComp, COMPONENT_DEFAULTS)) : null;
10145
+ diffs.push(diffEntity("component", comp.name, localSerialized, remoteSerialized));
10146
+ }
10147
+ for (const [name] of remote.components) {
10148
+ if (!processedComponentNames.has(name)) {
10149
+ diffs.push(diffEntity("component", name, null, "stale"));
10150
+ }
10151
+ }
10152
+ const processedFolderNames = /* @__PURE__ */ new Set();
10153
+ for (const folder of local.componentFolders) {
10154
+ processedFolderNames.add(folder.name);
10155
+ const remoteFolder = remote.componentFolders.get(folder.name);
10156
+ const localSerialized = serializeComponentFolder(folder);
10157
+ const remoteSerialized = remoteFolder ? serializeComponentFolder(remoteFolder) : null;
10158
+ diffs.push(diffEntity("componentFolder", folder.name, localSerialized, remoteSerialized));
10159
+ }
10160
+ for (const [name] of remote.componentFolders) {
10161
+ if (!processedFolderNames.has(name)) {
10162
+ diffs.push(diffEntity("componentFolder", name, null, "stale"));
10163
+ }
10164
+ }
10165
+ const processedDatasourceNames = /* @__PURE__ */ new Set();
10166
+ for (const ds of local.datasources) {
10167
+ processedDatasourceNames.add(ds.name);
10168
+ const remoteDs = remote.datasources.get(ds.name);
10169
+ const localSerialized = serializeDatasource(applyDefaults(ds, DATASOURCE_DEFAULTS));
10170
+ const remoteSerialized = remoteDs ? serializeDatasource(applyDefaults(remoteDs, DATASOURCE_DEFAULTS)) : null;
10171
+ diffs.push(diffEntity("datasource", ds.name, localSerialized, remoteSerialized));
10172
+ }
10173
+ for (const [name] of remote.datasources) {
10174
+ if (!processedDatasourceNames.has(name)) {
10175
+ diffs.push(diffEntity("datasource", name, null, "stale"));
10176
+ }
10177
+ }
10178
+ return {
10179
+ diffs,
10180
+ creates: diffs.filter((d) => d.action === "create").length,
10181
+ updates: diffs.filter((d) => d.action === "update").length,
10182
+ unchanged: diffs.filter((d) => d.action === "unchanged").length,
10183
+ stale: diffs.filter((d) => d.action === "stale").length
10184
+ };
10185
+ }
10186
+
10187
+ async function fetchRemoteSchema(spaceId) {
10188
+ const client = getMapiClient();
10189
+ const spaceIdNum = Number(spaceId);
10190
+ const [componentsRes, foldersRes, rawDatasources] = await Promise.all([
10191
+ client.components.list({ path: { space_id: spaceIdNum }, throwOnError: true }),
10192
+ client.componentFolders.list({ path: { space_id: spaceIdNum }, throwOnError: true }),
10193
+ fetchAllPages(
10194
+ (page) => client.datasources.list({ path: { space_id: spaceIdNum }, query: { page }, throwOnError: true }),
10195
+ (data) => data?.datasources ?? []
10196
+ )
10197
+ ]);
10198
+ const rawComponents = componentsRes.data?.components ?? [];
10199
+ const rawComponentFolders = foldersRes.data?.component_groups ?? [];
10200
+ const remote = {
10201
+ components: new Map(rawComponents.map((c) => [c.name, c])),
10202
+ componentFolders: new Map(rawComponentFolders.map((f) => [f.name, f])),
10203
+ datasources: new Map(rawDatasources.map((d) => [d.name, d]))
10204
+ };
10205
+ return { remote, rawComponents, rawComponentFolders, rawDatasources };
10206
+ }
10207
+
10208
+ function isRecord(value) {
10209
+ return typeof value === "object" && value !== null && !Array.isArray(value);
10210
+ }
10211
+ function isSchemaField(value) {
10212
+ return isRecord(value) && "type" in value;
10213
+ }
10214
+ function toSchemaRecord(schema) {
10215
+ const result = {};
10216
+ for (const [key, value] of Object.entries(schema)) {
10217
+ if (key === "_uid" || key === "component" || !isSchemaField(value)) {
10218
+ continue;
10219
+ }
10220
+ result[key] = value;
10221
+ }
10222
+ return result;
10223
+ }
10224
+ function buildComponentPayload(input) {
10225
+ if (!isRecord(input)) {
10226
+ return { name: "" };
10227
+ }
10228
+ return {
10229
+ name: typeof input.name === "string" ? input.name : "",
10230
+ // Fields in COMPONENT_DEFAULTS are always sent with their reset value so that
10231
+ // removing a field from the local schema actually clears it on the API.
10232
+ // (Root-level fields are additive on MAPI update — omitting preserves the old value.)
10233
+ display_name: typeof input.display_name === "string" ? input.display_name : "",
10234
+ description: typeof input.description === "string" ? input.description : "",
10235
+ color: typeof input.color === "string" ? input.color : "",
10236
+ icon: typeof input.icon === "string" ? input.icon : "",
10237
+ preview_field: typeof input.preview_field === "string" ? input.preview_field : "",
10238
+ internal_tag_ids: Array.isArray(input.internal_tag_ids) ? input.internal_tag_ids : [],
10239
+ // Conditionally sent: only included when explicitly set in local schema
10240
+ ...isRecord(input.schema) && { schema: toSchemaRecord(input.schema) },
10241
+ ...typeof input.is_root === "boolean" && { is_root: input.is_root },
10242
+ ...typeof input.is_nestable === "boolean" && { is_nestable: input.is_nestable },
10243
+ ...typeof input.component_group_uuid === "string" && { component_group_uuid: input.component_group_uuid }
10244
+ };
10245
+ }
10246
+ function toComponentCreate(input) {
10247
+ return buildComponentPayload(input);
10248
+ }
10249
+ function toComponentUpdate(input) {
10250
+ return buildComponentPayload(input);
10251
+ }
10252
+ function toComponentFolderCreate(input) {
10253
+ if (!isRecord(input)) {
10254
+ return { name: "" };
10255
+ }
10256
+ return {
10257
+ name: typeof input.name === "string" ? input.name : "",
10258
+ ...typeof input.parent_id === "number" && { parent_id: input.parent_id }
10259
+ };
10260
+ }
10261
+ function toDatasourceCreate(input) {
10262
+ if (!isRecord(input)) {
10263
+ return { name: "", slug: "" };
10264
+ }
10265
+ const result = {
10266
+ name: typeof input.name === "string" ? input.name : "",
10267
+ slug: typeof input.slug === "string" ? input.slug : ""
10268
+ };
10269
+ if (Array.isArray(input.dimensions)) {
10270
+ result.dimensions_attributes = input.dimensions.filter((d) => isRecord(d) && typeof d.name === "string" && typeof d.entry_value === "string").map((d) => ({
10271
+ name: d.name,
10272
+ entry_value: d.entry_value
10273
+ }));
10274
+ }
10275
+ return result;
10276
+ }
10277
+ function toDatasourceUpdate(input, remote) {
10278
+ const base = toDatasourceCreate(input);
10279
+ const localDims = base.dimensions_attributes ?? [];
10280
+ const remoteDims = remote.dimensions ?? [];
10281
+ if (remoteDims.length === 0) {
10282
+ return base;
10283
+ }
10284
+ const localKeys = new Set(localDims.map((d) => `${d.name}::${d.entry_value}`));
10285
+ const destroyEntries = remoteDims.filter((rd) => rd.id != null && !localKeys.has(`${rd.name}::${rd.entry_value}`)).map((rd) => ({ id: rd.id, _destroy: true }));
10286
+ if (destroyEntries.length > 0) {
10287
+ return {
10288
+ ...base,
10289
+ dimensions_attributes: [...localDims, ...destroyEntries]
10290
+ };
10291
+ }
10292
+ return base;
10293
+ }
10294
+
10295
+ function formatDiffOutput(result, options) {
10296
+ const lines = [];
10297
+ const byType = {
10298
+ component: [],
10299
+ componentFolder: [],
10300
+ datasource: []
10301
+ };
10302
+ for (const diff of result.diffs) {
10303
+ byType[diff.type].push(diff);
10304
+ }
10305
+ const willDelete = options?.delete ?? false;
10306
+ const icons = {
10307
+ create: chalk.green("+"),
10308
+ update: chalk.yellow("~"),
10309
+ unchanged: chalk.dim("="),
10310
+ stale: chalk.red("-")
10311
+ };
10312
+ const sections = [
10313
+ ["Components", byType.component],
10314
+ ["Component Folders", byType.componentFolder],
10315
+ ["Datasources", byType.datasource]
10316
+ ];
10317
+ for (const [label, diffs] of sections) {
10318
+ if (diffs.length === 0) {
10319
+ continue;
10320
+ }
10321
+ lines.push(chalk.bold(label));
10322
+ for (const diff of diffs) {
10323
+ const icon = icons[diff.action] ?? " ";
10324
+ const name = diff.action === "stale" ? chalk.red(diff.name) : diff.name;
10325
+ const actionLabel = diff.action === "stale" && willDelete ? "delete" : diff.action;
10326
+ lines.push(` ${icon} ${name} ${chalk.dim(`(${actionLabel})`)}`);
10327
+ if (diff.diff) {
10328
+ for (const line of diff.diff.split("\n")) {
10329
+ if (line.startsWith("+") && !line.startsWith("+++")) {
10330
+ lines.push(` ${chalk.green(line)}`);
10331
+ } else if (line.startsWith("-") && !line.startsWith("---")) {
10332
+ lines.push(` ${chalk.red(line)}`);
10333
+ }
10334
+ }
10335
+ }
10336
+ }
10337
+ lines.push("");
10338
+ }
10339
+ const summary = [
10340
+ result.creates > 0 ? chalk.green(`${result.creates} to create`) : null,
10341
+ result.updates > 0 ? chalk.yellow(`${result.updates} to update`) : null,
10342
+ result.unchanged > 0 ? chalk.dim(`${result.unchanged} unchanged`) : null,
10343
+ result.stale > 0 ? chalk.red(`${result.stale} ${willDelete ? "to delete" : "stale"}`) : null
10344
+ ].filter(Boolean).join(", ");
10345
+ lines.push(`Summary: ${summary}`);
10346
+ return lines.join("\n");
10347
+ }
10348
+ async function executePush(spaceId, local, remote, diffResult, options) {
10349
+ const client = getMapiClient();
10350
+ const spaceIdNum = Number(spaceId);
10351
+ let created = 0;
10352
+ let updated = 0;
10353
+ let deleted = 0;
10354
+ const createdFolderUuids = /* @__PURE__ */ new Map();
10355
+ const folderDiffs = diffResult.diffs.filter((d) => d.type === "componentFolder");
10356
+ const folderResults = await Promise.allSettled(
10357
+ folderDiffs.map(async (diff) => {
10358
+ const localFolder = local.componentFolders.find((f) => f.name === diff.name);
10359
+ if (diff.action === "create" && localFolder) {
10360
+ const response = await client.componentFolders.create({
10361
+ path: { space_id: spaceIdNum },
10362
+ body: { component_group: toComponentFolderCreate(localFolder) },
10363
+ throwOnError: true
10364
+ });
10365
+ return { action: "created", uuid: response.data?.component_group?.uuid };
10366
+ }
10367
+ if (diff.action === "update" && localFolder) {
10368
+ const existing = remote.componentFolders.get(diff.name);
10369
+ if (existing?.id) {
10370
+ await client.componentFolders.update(existing.id, {
10371
+ path: { space_id: spaceIdNum },
10372
+ body: { component_group: toComponentFolderCreate(localFolder) },
10373
+ throwOnError: true
10374
+ });
10375
+ return { action: "updated" };
10376
+ }
10377
+ }
10378
+ })
10379
+ );
10380
+ for (let i = 0; i < folderResults.length; i++) {
10381
+ const result = folderResults[i];
10382
+ const diff = folderDiffs[i];
10383
+ if (result.status === "fulfilled") {
10384
+ if (result.value?.action === "created") {
10385
+ if (result.value.uuid) {
10386
+ createdFolderUuids.set(diff.name, result.value.uuid);
10387
+ }
10388
+ created++;
10389
+ } else if (result.value?.action === "updated") {
10390
+ updated++;
10391
+ }
10392
+ } else {
10393
+ const eventId = diff.action === "create" ? "push_component_group" : "update_component_group";
10394
+ handleAPIError(eventId, result.reason, `Failed to ${diff.action} component folder ${diff.name}`);
10395
+ }
10396
+ }
10397
+ const skippedComponents = /* @__PURE__ */ new Set();
10398
+ if (options.pendingFolderAssignments) {
10399
+ const logger = getLogger();
10400
+ for (const [folderName, componentNames] of options.pendingFolderAssignments) {
10401
+ const remoteUuid = createdFolderUuids.get(folderName);
10402
+ if (!remoteUuid) {
10403
+ logger.warn(`Could not resolve folder '${folderName}' \u2014 skipping components: ${componentNames.join(", ")}`);
10404
+ for (const name of componentNames) {
10405
+ skippedComponents.add(name);
10406
+ }
10407
+ continue;
10408
+ }
10409
+ for (const compName of componentNames) {
10410
+ const comp = local.components.find((c) => c.name === compName);
10411
+ if (comp) {
10412
+ comp.component_group_uuid = remoteUuid;
10413
+ }
10414
+ }
10415
+ }
10416
+ }
10417
+ const componentDiffs = diffResult.diffs.filter((d) => d.type === "component" && !skippedComponents.has(d.name));
10418
+ const componentResults = await Promise.allSettled(
10419
+ componentDiffs.map(async (diff) => {
10420
+ const localComp = local.components.find((c) => c.name === diff.name);
10421
+ if (diff.action === "create" && localComp) {
10422
+ await client.components.create({
10423
+ path: { space_id: spaceIdNum },
10424
+ body: { component: toComponentCreate(localComp) },
10425
+ throwOnError: true
10426
+ });
10427
+ return "created";
10428
+ }
10429
+ if (diff.action === "update" && localComp) {
10430
+ const existing = remote.components.get(diff.name);
10431
+ if (existing?.id) {
10432
+ await client.components.update(existing.id, {
10433
+ path: { space_id: spaceIdNum },
10434
+ body: { component: toComponentUpdate(localComp) },
10435
+ throwOnError: true
10436
+ });
10437
+ return "updated";
10438
+ }
10439
+ }
10440
+ })
10441
+ );
10442
+ for (let i = 0; i < componentResults.length; i++) {
10443
+ const result = componentResults[i];
10444
+ const diff = componentDiffs[i];
10445
+ if (result.status === "fulfilled") {
10446
+ if (result.value === "created") {
10447
+ created++;
10448
+ } else if (result.value === "updated") {
10449
+ updated++;
10450
+ }
10451
+ } else {
10452
+ const eventId = diff.action === "create" ? "push_component" : "update_component";
10453
+ handleAPIError(eventId, result.reason, `Failed to ${diff.action} component ${diff.name}`);
10454
+ }
10455
+ }
10456
+ const datasourceDiffs = diffResult.diffs.filter((d) => d.type === "datasource");
10457
+ const datasourceResults = await Promise.allSettled(
10458
+ datasourceDiffs.map(async (diff) => {
10459
+ const localDs = local.datasources.find((d) => d.name === diff.name);
10460
+ if (diff.action === "create" && localDs) {
10461
+ await client.datasources.create({
10462
+ path: { space_id: spaceIdNum },
10463
+ body: { datasource: toDatasourceCreate(localDs) },
10464
+ throwOnError: true
10465
+ });
10466
+ return "created";
10467
+ }
10468
+ if (diff.action === "update" && localDs) {
10469
+ const existing = remote.datasources.get(diff.name);
10470
+ if (existing?.id) {
10471
+ await client.datasources.update(existing.id, {
10472
+ path: { space_id: spaceIdNum },
10473
+ body: { datasource: toDatasourceUpdate(localDs, existing) },
10474
+ throwOnError: true
10475
+ });
10476
+ return "updated";
10477
+ }
10478
+ }
10479
+ })
10480
+ );
10481
+ for (let i = 0; i < datasourceResults.length; i++) {
10482
+ const result = datasourceResults[i];
10483
+ const diff = datasourceDiffs[i];
10484
+ if (result.status === "fulfilled") {
10485
+ if (result.value === "created") {
10486
+ created++;
10487
+ } else if (result.value === "updated") {
10488
+ updated++;
10489
+ }
10490
+ } else {
10491
+ const eventId = diff.action === "create" ? "push_datasource" : "update_datasource";
10492
+ handleAPIError(eventId, result.reason, `Failed to ${diff.action} datasource ${diff.name}`);
10493
+ }
10494
+ }
10495
+ if (options.delete) {
10496
+ const staleComponents = diffResult.diffs.filter((d) => d.type === "component" && d.action === "stale");
10497
+ const deleteComponentResults = await Promise.allSettled(
10498
+ staleComponents.map(async (diff) => {
10499
+ const existing = remote.components.get(diff.name);
10500
+ if (existing?.id) {
10501
+ await client.components.delete(existing.id, {
10502
+ path: { space_id: spaceIdNum },
10503
+ throwOnError: true
10504
+ });
10505
+ return true;
10506
+ }
10507
+ })
10508
+ );
10509
+ for (let i = 0; i < deleteComponentResults.length; i++) {
10510
+ const result = deleteComponentResults[i];
10511
+ if (result.status === "fulfilled") {
10512
+ if (result.value) {
10513
+ deleted++;
10514
+ }
10515
+ } else {
10516
+ handleAPIError("push_component", result.reason, `Failed to delete component ${staleComponents[i].name}`);
10517
+ }
10518
+ }
10519
+ const staleDatasources = diffResult.diffs.filter((d) => d.type === "datasource" && d.action === "stale");
10520
+ const deleteDatasourceResults = await Promise.allSettled(
10521
+ staleDatasources.map(async (diff) => {
10522
+ const existing = remote.datasources.get(diff.name);
10523
+ if (existing?.id) {
10524
+ await client.datasources.delete(existing.id, {
10525
+ path: { space_id: spaceIdNum },
10526
+ throwOnError: true
10527
+ });
10528
+ return true;
10529
+ }
10530
+ })
10531
+ );
10532
+ for (let i = 0; i < deleteDatasourceResults.length; i++) {
10533
+ const result = deleteDatasourceResults[i];
10534
+ if (result.status === "fulfilled") {
10535
+ if (result.value) {
10536
+ deleted++;
10537
+ }
10538
+ } else {
10539
+ handleAPIError("delete_datasource", result.reason, `Failed to delete datasource ${staleDatasources[i].name}`);
10540
+ }
10541
+ }
10542
+ const staleFolders = diffResult.diffs.filter((d) => d.type === "componentFolder" && d.action === "stale");
10543
+ const deleteFolderResults = await Promise.allSettled(
10544
+ staleFolders.map(async (diff) => {
10545
+ const existing = remote.componentFolders.get(diff.name);
10546
+ if (existing?.id) {
10547
+ await client.componentFolders.delete(existing.id, {
10548
+ path: { space_id: spaceIdNum },
10549
+ throwOnError: true
10550
+ });
10551
+ return true;
10552
+ }
10553
+ })
10554
+ );
10555
+ for (let i = 0; i < deleteFolderResults.length; i++) {
10556
+ const result = deleteFolderResults[i];
10557
+ if (result.status === "fulfilled") {
10558
+ if (result.value) {
10559
+ deleted++;
10560
+ }
10561
+ } else {
10562
+ handleAPIError("push_component_group", result.reason, `Failed to delete folder ${staleFolders[i].name}`);
10563
+ }
10564
+ }
10565
+ }
10566
+ return { created, updated, deleted };
10567
+ }
10568
+ function buildChangesetEntries(diffResult, local, remote, options) {
10569
+ const changes = [];
10570
+ for (const diff of diffResult.diffs) {
10571
+ if (diff.action === "unchanged") {
10572
+ continue;
10573
+ }
10574
+ if (diff.action === "stale" && !options.delete) {
10575
+ continue;
10576
+ }
10577
+ const action = diff.action === "stale" ? "delete" : diff.action;
10578
+ let remoteSrc;
10579
+ let localSrc;
10580
+ if (diff.type === "component") {
10581
+ remoteSrc = remote.components.get(diff.name);
10582
+ localSrc = local.components.find((c) => c.name === diff.name);
10583
+ } else if (diff.type === "componentFolder") {
10584
+ remoteSrc = remote.componentFolders.get(diff.name);
10585
+ localSrc = local.componentFolders.find((f) => f.name === diff.name);
10586
+ } else if (diff.type === "datasource") {
10587
+ remoteSrc = remote.datasources.get(diff.name);
10588
+ localSrc = local.datasources.find((d) => d.name === diff.name);
10589
+ }
10590
+ changes.push({
10591
+ type: diff.type,
10592
+ name: diff.name,
10593
+ action,
10594
+ ...remoteSrc && { before: { ...remoteSrc } },
10595
+ ...localSrc && { after: { ...localSrc } }
10596
+ });
10597
+ }
10598
+ return changes;
10599
+ }
10600
+
10601
+ function findRemoteFolderByUuid(remoteFolders, uuid) {
10602
+ for (const folder of remoteFolders.values()) {
10603
+ if (folder.uuid === uuid) {
10604
+ return folder;
10605
+ }
10606
+ }
10607
+ return void 0;
10608
+ }
10609
+ function resolveFolderReferences(local, remote) {
10610
+ const localUuidToName = /* @__PURE__ */ new Map();
10611
+ for (const folder of local.componentFolders) {
10612
+ if (folder.uuid) {
10613
+ localUuidToName.set(folder.uuid, folder.name);
10614
+ }
10615
+ }
10616
+ const pendingFolderAssignments = /* @__PURE__ */ new Map();
10617
+ const resolvedComponents = local.components.map((comp) => {
10618
+ if (!comp.component_group_uuid) {
10619
+ return comp;
10620
+ }
10621
+ const folderName = localUuidToName.get(comp.component_group_uuid);
10622
+ if (!folderName) {
10623
+ return comp;
10624
+ }
10625
+ const remoteByUuid = findRemoteFolderByUuid(remote.componentFolders, comp.component_group_uuid);
10626
+ if (remoteByUuid) {
10627
+ return comp;
10628
+ }
10629
+ const remoteByName = remote.componentFolders.get(folderName);
10630
+ if (remoteByName) {
10631
+ return { ...comp, component_group_uuid: remoteByName.uuid };
10632
+ }
10633
+ const pending = pendingFolderAssignments.get(folderName) ?? [];
10634
+ pending.push(comp.name);
10635
+ pendingFolderAssignments.set(folderName, pending);
10636
+ return comp;
10637
+ });
10638
+ return {
10639
+ resolved: { ...local, components: resolvedComponents },
10640
+ pendingFolderAssignments
10641
+ };
10642
+ }
10643
+
10644
+ async function ensureDir(dir) {
10645
+ await mkdir(dir, { recursive: true });
10646
+ }
10647
+ async function saveChangeset(basePath, data) {
10648
+ const dir = join(basePath, "schema", "changesets");
10649
+ await ensureDir(dir);
10650
+ const fileName = `${fileTimestamp(data.timestamp)}.json`;
10651
+ const filePath = join(dir, fileName);
10652
+ await writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
10653
+ return filePath;
10654
+ }
10655
+
10656
+ const SENTINEL_FIELDS = /* @__PURE__ */ new Set(["_uid", "component"]);
10657
+ function classifyFieldChanges(remoteSchema, localSchema) {
10658
+ const removed = [];
10659
+ const added = [];
10660
+ const typeChanged = [];
10661
+ const requiredAdded = [];
10662
+ const requiredChanged = [];
10663
+ for (const [field, remoteField] of Object.entries(remoteSchema)) {
10664
+ if (SENTINEL_FIELDS.has(field)) {
10665
+ continue;
10666
+ }
10667
+ if (typeof remoteField.type !== "string") {
10668
+ continue;
10669
+ }
10670
+ if (!(field in localSchema)) {
10671
+ removed.push({ field, type: remoteField.type });
10672
+ }
10673
+ }
10674
+ for (const [field, localField] of Object.entries(localSchema)) {
10675
+ if (SENTINEL_FIELDS.has(field)) {
10676
+ continue;
10677
+ }
10678
+ if (typeof localField.type !== "string") {
10679
+ continue;
10680
+ }
10681
+ if (!(field in remoteSchema)) {
10682
+ if (localField.required) {
10683
+ requiredAdded.push({ field, type: localField.type });
10684
+ } else {
10685
+ added.push({ field, type: localField.type, required: false });
10686
+ }
10687
+ } else {
10688
+ const remoteField = remoteSchema[field];
10689
+ if (typeof remoteField?.type !== "string") {
10690
+ continue;
10691
+ }
10692
+ if (remoteField.type !== localField.type) {
10693
+ typeChanged.push({ field, oldType: remoteField.type, newType: localField.type });
10694
+ }
10695
+ if (localField.required && !remoteField.required) {
10696
+ requiredChanged.push({ field, type: localField.type });
10697
+ }
10698
+ }
10699
+ }
10700
+ return { removed, added, typeChanged, requiredAdded, requiredChanged };
10701
+ }
10702
+ function longestCommonSubstring(a, b) {
10703
+ let maxLen = 0;
10704
+ for (let i = 0; i < a.length; i++) {
10705
+ for (let j = 0; j < b.length; j++) {
10706
+ let len = 0;
10707
+ while (i + len < a.length && j + len < b.length && a[i + len] === b[j + len]) {
10708
+ len++;
10709
+ }
10710
+ if (len > maxLen) {
10711
+ maxLen = len;
10712
+ }
10713
+ }
10714
+ }
10715
+ return maxLen;
10716
+ }
10717
+ function nameSimilarity(a, b) {
10718
+ const longer = Math.max(a.length, b.length);
10719
+ if (longer === 0) {
10720
+ return 1;
10721
+ }
10722
+ return longestCommonSubstring(a, b) / longer;
10723
+ }
10724
+ function detectRenames(removed, added) {
10725
+ const renames = [];
10726
+ const usedRemoved = /* @__PURE__ */ new Set();
10727
+ const usedAdded = /* @__PURE__ */ new Set();
10728
+ const addedByType = /* @__PURE__ */ new Map();
10729
+ for (const addedField of added) {
10730
+ if (!addedByType.has(addedField.type)) {
10731
+ addedByType.set(addedField.type, []);
10732
+ }
10733
+ addedByType.get(addedField.type).push(addedField);
10734
+ }
10735
+ const isSinglePair = removed.length === 1 && added.length === 1;
10736
+ for (const removedField of removed) {
10737
+ const candidates = addedByType.get(removedField.type) ?? [];
10738
+ const availableCandidates = candidates.filter((c) => !usedAdded.has(c.field));
10739
+ if (availableCandidates.length === 0) {
10740
+ continue;
10741
+ }
10742
+ let bestCandidate = availableCandidates[0];
10743
+ let bestScore = nameSimilarity(removedField.field, bestCandidate.field);
10744
+ for (let i = 1; i < availableCandidates.length; i++) {
10745
+ const score = nameSimilarity(removedField.field, availableCandidates[i].field);
10746
+ if (score > bestScore) {
10747
+ bestScore = score;
10748
+ bestCandidate = availableCandidates[i];
10749
+ }
10750
+ }
10751
+ if (!isSinglePair && bestScore < 0.3) {
10752
+ continue;
10753
+ }
10754
+ renames.push({ oldField: removedField.field, newField: bestCandidate.field, fieldType: removedField.type });
10755
+ usedRemoved.add(removedField.field);
10756
+ usedAdded.add(bestCandidate.field);
10757
+ }
10758
+ const unmatchedRemoved = removed.filter((r) => !usedRemoved.has(r.field));
10759
+ const unmatchedAdded = added.filter((a) => !usedAdded.has(a.field));
10760
+ return { renames, unmatchedRemoved, unmatchedAdded };
10761
+ }
10762
+ function analyzeBreakingChanges(diffResult, local, remote) {
10763
+ const results = [];
10764
+ const updatedComponents = diffResult.diffs.filter(
10765
+ (d) => d.type === "component" && d.action === "update"
10766
+ );
10767
+ for (const diff of updatedComponents) {
10768
+ const localComp = local.components.find((c) => c.name === diff.name);
10769
+ const remoteComp = remote.components.get(diff.name);
10770
+ if (!localComp?.schema || !remoteComp?.schema) {
10771
+ continue;
10772
+ }
10773
+ const classification = classifyFieldChanges(
10774
+ remoteComp.schema,
10775
+ localComp.schema
10776
+ );
10777
+ const changes = [];
10778
+ const { renames, unmatchedRemoved } = detectRenames(classification.removed, classification.added);
10779
+ for (const rename of renames) {
10780
+ changes.push({ kind: "rename", field: rename.newField, oldField: rename.oldField });
10781
+ }
10782
+ for (const removed of unmatchedRemoved) {
10783
+ changes.push({ kind: "removed", field: removed.field });
10784
+ }
10785
+ for (const tc of classification.typeChanged) {
10786
+ changes.push({ kind: "type_changed", field: tc.field, oldType: tc.oldType, newType: tc.newType });
10787
+ }
10788
+ for (const ra of classification.requiredAdded) {
10789
+ changes.push({ kind: "required_added", field: ra.field, fieldType: ra.type });
10790
+ }
10791
+ for (const rc of classification.requiredChanged) {
10792
+ changes.push({ kind: "required_changed", field: rc.field, fieldType: rc.type });
10793
+ }
10794
+ if (changes.length > 0) {
10795
+ results.push({ componentName: diff.name, changes });
10796
+ }
10797
+ }
10798
+ return results;
10799
+ }
10800
+
10801
+ const COMPATIBLE_TYPES = /* @__PURE__ */ new Set(["text:textarea", "textarea:text"]);
10802
+ function defaultForType(fieldType) {
10803
+ switch (fieldType) {
10804
+ case "text":
10805
+ case "textarea":
10806
+ case "markdown":
10807
+ return `''`;
10808
+ case "number":
10809
+ return "0";
10810
+ case "boolean":
10811
+ return "false";
10812
+ default:
10813
+ return null;
10814
+ }
10815
+ }
10816
+ function typeConversion(field, oldType, newType) {
10817
+ const key = `${oldType}:${newType}`;
10818
+ if (COMPATIBLE_TYPES.has(key)) {
10819
+ return null;
10820
+ }
10821
+ const accessor = `block.${field}`;
10822
+ switch (key) {
10823
+ case "text:number":
10824
+ return `${accessor} = Number(${accessor}) || 0;`;
10825
+ case "number:text":
10826
+ return `${accessor} = String(${accessor});`;
10827
+ case "text:boolean":
10828
+ return `${accessor} = !!${accessor};`;
10829
+ case "boolean:text":
10830
+ return `${accessor} = String(${accessor});`;
10831
+ default:
10832
+ return `${accessor}; // TODO: convert from ${oldType} to ${newType}`;
10833
+ }
10834
+ }
10835
+ function renderMigrationCode(changes) {
10836
+ const lines = [];
10837
+ lines.push(" // Review this migration before running it against your space.");
10838
+ lines.push(" // Generated migrations are scaffolds and may need manual adjustments.");
10839
+ lines.push(" // Example rename migration:");
10840
+ lines.push(" // block.new_field = block.old_field;");
10841
+ lines.push(" // delete block.old_field;");
10842
+ lines.push("");
10843
+ for (const change of changes) {
10844
+ switch (change.kind) {
10845
+ case "rename":
10846
+ lines.push(` // Rename: ${change.oldField} \u2192 ${change.field}`);
10847
+ lines.push(` if ('${change.oldField}' in block) {`);
10848
+ lines.push(` block.${change.field} = block.${change.oldField};`);
10849
+ lines.push(` delete block.${change.oldField};`);
10850
+ lines.push(` }`);
10851
+ break;
10852
+ case "removed":
10853
+ if (change.renameHint) {
10854
+ lines.push(` // If '${change.field}' was renamed to '${change.renameHint.newField}', uncomment:`);
10855
+ lines.push(` // block.${change.renameHint.newField} = block.${change.field};`);
10856
+ } else {
10857
+ lines.push(` // Removed field: ${change.field}`);
10858
+ }
10859
+ lines.push(` delete block.${change.field};`);
10860
+ break;
10861
+ case "type_changed": {
10862
+ const conversion = typeConversion(change.field, change.oldType, change.newType);
10863
+ if (conversion) {
10864
+ lines.push(` // Type change: ${change.field} (${change.oldType} \u2192 ${change.newType})`);
10865
+ lines.push(` ${conversion}`);
10866
+ }
10867
+ break;
10868
+ }
10869
+ case "required_added": {
10870
+ const defaultValue = defaultForType(change.fieldType);
10871
+ lines.push(` // New required field: ${change.field} (${change.fieldType})`);
10872
+ if (defaultValue !== null) {
10873
+ lines.push(` // TODO: provide a meaningful default value`);
10874
+ lines.push(` block.${change.field} = block.${change.field} ?? ${defaultValue};`);
10875
+ } else {
10876
+ lines.push(` // TODO: provide a default value appropriate for the '${change.fieldType}' type`);
10877
+ lines.push(` // block.${change.field} = block.${change.field} ?? <default>;`);
10878
+ }
10879
+ break;
10880
+ }
10881
+ case "required_changed": {
10882
+ const defaultValue = defaultForType(change.fieldType);
10883
+ lines.push(` // Field is now required: ${change.field} (${change.fieldType})`);
10884
+ lines.push(` // Existing stories may have null/undefined values \u2014 provide a default for those.`);
10885
+ if (defaultValue !== null) {
10886
+ lines.push(` // TODO: provide a meaningful default value`);
10887
+ lines.push(` block.${change.field} = block.${change.field} ?? ${defaultValue};`);
10888
+ } else {
10889
+ lines.push(` // TODO: provide a default value appropriate for the '${change.fieldType}' type`);
10890
+ lines.push(` // block.${change.field} = block.${change.field} ?? <default>;`);
10891
+ }
10892
+ break;
10893
+ }
10894
+ }
10895
+ lines.push("");
10896
+ }
10897
+ const body = lines.length > 0 ? `
10898
+ ${lines.join("\n")}` : "\n";
10899
+ return `export default function (block) {${body} return block;
10900
+ }
10901
+ `;
10902
+ }
10903
+ async function writeMigrationFile(options) {
10904
+ const { spaceId, componentName, code, timestamp, basePath } = options;
10905
+ const dir = resolvePath(basePath, `migrations/${spaceId}`);
10906
+ await mkdir(dir, { recursive: true });
10907
+ const fileName = `${componentName}.${fileTimestamp(timestamp)}.js`;
10908
+ const filePath = join(dir, fileName);
10909
+ await writeFile(filePath, code, "utf-8");
10910
+ return filePath;
10911
+ }
10912
+
10913
+ const DEFAULT_GROUPS_FILENAME = "groups.json";
10914
+ const CONSOLIDATED_COMPONENTS_FILENAME = "components.json";
10915
+ async function writeLocalComponents({
10916
+ space,
10917
+ basePath,
10918
+ resolved,
10919
+ diffResult,
10920
+ deleteRemoved,
10921
+ ui,
10922
+ logger
10923
+ }) {
10924
+ const componentsDir = resolveCommandPath(directories.components, space, basePath);
10925
+ const consolidatedPath = join(componentsDir, CONSOLIDATED_COMPONENTS_FILENAME);
10926
+ if (await fileExists(consolidatedPath)) {
10927
+ ui.warn(
10928
+ `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.`
10929
+ );
10930
+ }
10931
+ for (const component of resolved.components) {
10932
+ const filePath = join(componentsDir, `${sanitizeFilename(component.name || "")}.json`);
10933
+ await saveToFile(filePath, JSON.stringify(component, null, 2));
10934
+ }
10935
+ const groupsPath = join(componentsDir, DEFAULT_GROUPS_FILENAME);
10936
+ if (resolved.componentFolders.length > 0) {
10937
+ await saveToFile(groupsPath, JSON.stringify(resolved.componentFolders, null, 2));
10938
+ } else if (await fileExists(groupsPath)) {
10939
+ try {
10940
+ await unlink(groupsPath);
10941
+ logger.info("Removed stale local groups file", { path: displayPath(groupsPath, basePath) });
10942
+ } catch (error) {
10943
+ if (error.code !== "ENOENT") {
10944
+ throw error;
10945
+ }
10946
+ }
10947
+ }
10948
+ if (deleteRemoved) {
10949
+ const staleComponents = diffResult.diffs.filter(
10950
+ (d) => d.type === "component" && d.action === "stale"
10951
+ );
10952
+ for (const stale of staleComponents) {
10953
+ const filePath = join(componentsDir, `${sanitizeFilename(stale.name)}.json`);
10954
+ try {
10955
+ await unlink(filePath);
10956
+ logger.info("Removed stale local component file", { path: displayPath(filePath, basePath) });
10957
+ } catch (error) {
10958
+ if (error.code !== "ENOENT") {
10959
+ throw error;
10960
+ }
10961
+ }
10962
+ }
10963
+ }
10964
+ logger.info("Wrote local component files", {
10965
+ space,
10966
+ componentsWritten: resolved.components.length,
10967
+ groupsWritten: resolved.componentFolders.length
10968
+ });
10969
+ }
10970
+
10971
+ 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) => {
10972
+ const ui = getUI();
10973
+ const logger = getLogger();
10974
+ const reporter = getReporter();
10975
+ const { space, path: basePath, verbose } = command.optsWithGlobals();
10976
+ const { state } = session();
10977
+ ui.title(commands.SCHEMA, colorPalette.SCHEMA, "Pushing schema...");
10978
+ logger.info("Schema push started", { entryFile, space });
10979
+ if (!requireAuthentication(state, verbose)) {
10980
+ return;
10981
+ }
10982
+ if (!space) {
10983
+ handleError(new CommandError("Please provide the space as argument --space SPACE_ID."), verbose);
10984
+ return;
10985
+ }
10986
+ const summary = { total: 0, succeeded: 0, failed: 0 };
10987
+ try {
10988
+ const loadSpinner = ui.createSpinner("Resolving schema...");
10989
+ let local;
10990
+ try {
10991
+ local = await loadSchema(entryFile);
10992
+ } catch (maybeError) {
10993
+ loadSpinner.failed("Failed to resolve schema");
10994
+ handleError(toError(maybeError), verbose);
10995
+ return;
10996
+ }
10997
+ loadSpinner.succeed(`Found: ${local.components.length} components, ${local.componentFolders.length} component folders, ${local.datasources.length} datasources`);
10998
+ const totalLocal = local.components.length + local.componentFolders.length + local.datasources.length;
10999
+ if (totalLocal === 0) {
11000
+ ui.warn("No components, folders, or datasources found in the entry file. Verify the file exports schema definitions.");
11001
+ return;
11002
+ }
11003
+ const remoteSpinner = ui.createSpinner(`Fetching remote state from space ${space}...`);
11004
+ let remoteResult;
11005
+ try {
11006
+ remoteResult = await fetchRemoteSchema(space);
11007
+ } catch (maybeError) {
11008
+ remoteSpinner.failed("Failed to fetch remote schema");
11009
+ handleError(toError(maybeError), verbose);
11010
+ return;
11011
+ }
11012
+ const { remote, rawComponents, rawComponentFolders, rawDatasources } = remoteResult;
11013
+ remoteSpinner.succeed(`Remote: ${remote.components.size} components, ${remote.componentFolders.size} component folders, ${remote.datasources.size} datasources`);
11014
+ const { resolved, pendingFolderAssignments } = resolveFolderReferences(local, remote);
11015
+ const diffResult = diffSchema(resolved, remote);
11016
+ ui.br();
11017
+ ui.log(formatDiffOutput(diffResult, { delete: options.delete }));
11018
+ if (options.migrations) {
11019
+ const breakingChanges = analyzeBreakingChanges(diffResult, resolved, remote);
11020
+ if (breakingChanges.length > 0) {
11021
+ const totalChanges = breakingChanges.reduce((sum, c) => sum + c.changes.length, 0);
11022
+ ui.br();
11023
+ ui.warn(`${totalChanges} breaking change(s) detected in ${breakingChanges.length} component(s).`);
11024
+ ui.info("Generated migrations are scaffolds. Review and adjust them before running `storyblok migrations run`.");
11025
+ if (!options.dryRun) {
11026
+ const explicitMigrations = command.getOptionValueSource("migrations") === "cli";
11027
+ const shouldGenerate = explicitMigrations || await confirm({
11028
+ message: "Generate migration files for breaking changes?",
11029
+ default: true
11030
+ });
11031
+ if (shouldGenerate) {
11032
+ const migrationTimestamp = (/* @__PURE__ */ new Date()).toISOString();
11033
+ const resolvedBase = resolvePath(basePath, "");
11034
+ for (const comp of breakingChanges) {
11035
+ const renames = comp.changes.filter((c) => c.kind === "rename");
11036
+ if (renames.length > 0 && explicitMigrations) {
11037
+ for (const r of renames) {
11038
+ if (r.kind === "rename") {
11039
+ ui.log(` Assumed rename in '${comp.componentName}': ${r.oldField} \u2192 ${r.field}`);
11040
+ }
11041
+ }
11042
+ }
11043
+ if (renames.length > 0 && !explicitMigrations) {
11044
+ ui.br();
11045
+ ui.log(`Detected renames in '${comp.componentName}':`);
11046
+ for (const r of renames) {
11047
+ if (r.kind === "rename") {
11048
+ ui.log(` ${r.oldField} \u2192 ${r.field}`);
11049
+ }
11050
+ }
11051
+ const renameConfirmed = await confirm({
11052
+ message: "Are these renames correct?",
11053
+ default: true
11054
+ });
11055
+ if (!renameConfirmed) {
11056
+ comp.changes = comp.changes.map((c) => {
11057
+ if (c.kind === "rename") {
11058
+ return { kind: "removed", field: c.oldField, renameHint: { newField: c.field } };
11059
+ }
11060
+ return c;
11061
+ });
11062
+ }
11063
+ }
11064
+ const code = renderMigrationCode(comp.changes);
11065
+ const path = await writeMigrationFile({
11066
+ spaceId: space,
11067
+ componentName: comp.componentName,
11068
+ code,
11069
+ timestamp: migrationTimestamp,
11070
+ basePath: resolvedBase
11071
+ });
11072
+ const migrationPath = displayPath(path, basePath);
11073
+ logger.info("Migration generated", { component: comp.componentName, path: migrationPath });
11074
+ ui.log(` Generated: ${migrationPath}`);
11075
+ }
11076
+ ui.br();
11077
+ ui.info(`Run migrations when ready: storyblok migrations run --space ${space}`);
11078
+ }
11079
+ }
11080
+ }
11081
+ }
11082
+ if (options.delete) {
11083
+ const deletedComponents = diffResult.diffs.filter((d) => d.type === "component" && d.action === "stale");
11084
+ for (const comp of deletedComponents) {
11085
+ ui.warn(`Component '${comp.name}' will be deleted. Stories using it will have out-of-schema content.`);
11086
+ }
11087
+ }
11088
+ if (diffResult.stale > 0 && !options.delete) {
11089
+ ui.warn(`${diffResult.stale} stale entity(s) exist remotely but not in schema. Use --delete to remove.`);
11090
+ }
11091
+ if (options.dryRun) {
11092
+ ui.info("Dry run \u2014 no changes applied.");
11093
+ logger.info("Dry run completed", { creates: diffResult.creates, updates: diffResult.updates });
11094
+ return;
11095
+ }
11096
+ const resolvedPath = resolvePath(basePath, "");
11097
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
11098
+ const changesetPath = await saveChangeset(resolvedPath, {
11099
+ timestamp,
11100
+ spaceId: Number(space),
11101
+ remote: { components: rawComponents, componentFolders: rawComponentFolders, datasources: rawDatasources },
11102
+ changes: buildChangesetEntries(diffResult, resolved, remote, { delete: options.delete })
11103
+ });
11104
+ logger.info("Changeset saved", { path: displayPath(changesetPath, basePath) });
11105
+ const nothingToPush = diffResult.creates === 0 && diffResult.updates === 0 && (!options.delete || diffResult.stale === 0);
11106
+ if (nothingToPush) {
11107
+ ui.ok("Everything up to date \u2014 nothing to push.");
11108
+ } else {
11109
+ const pushSpinner = ui.createSpinner("Pushing schema...");
11110
+ let result;
11111
+ try {
11112
+ result = await executePush(space, resolved, remote, diffResult, { delete: options.delete, pendingFolderAssignments });
11113
+ } catch (error) {
11114
+ pushSpinner.failed("Failed to push schema");
11115
+ throw error;
11116
+ }
11117
+ summary.total = result.created + result.updated + result.deleted;
11118
+ summary.succeeded = summary.total;
11119
+ pushSpinner.succeed(`Pushed ${result.created} creations, ${result.updated} updates${result.deleted > 0 ? `, ${result.deleted} deletions` : ""}.`);
11120
+ }
11121
+ if (options.writeComponents) {
11122
+ try {
11123
+ await writeLocalComponents({
11124
+ space,
11125
+ basePath,
11126
+ resolved,
11127
+ diffResult,
11128
+ deleteRemoved: options.delete,
11129
+ ui,
11130
+ logger
11131
+ });
11132
+ } catch (writeError) {
11133
+ ui.warn(`Failed to write local component files: ${toError(writeError).message}`);
11134
+ logger.warn("Failed to write local component files", { error: toError(writeError).message });
11135
+ }
11136
+ }
11137
+ } catch (maybeError) {
11138
+ summary.failed += 1;
11139
+ handleError(toError(maybeError), verbose);
11140
+ } finally {
11141
+ logger.info("Schema push finished", { summary });
11142
+ reporter.addSummary("schemaPushResults", summary);
11143
+ reporter.finalize();
11144
+ }
11145
+ });
11146
+
11147
+ const FIELD_STRIP_KEYS = /* @__PURE__ */ new Set(["id", "pos"]);
11148
+ function toCamelCase(str) {
11149
+ return str.toLowerCase().replace(/[\s_-]+(.)/g, (_, char) => char.toUpperCase());
11150
+ }
11151
+ function toKebabCase(str) {
11152
+ return str.replace(/[\s_]+/g, "-").replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
11153
+ }
11154
+ function componentVarName(name) {
11155
+ return `${toCamelCase(name)}Block`;
11156
+ }
11157
+ function folderVarName(name) {
11158
+ return `${toCamelCase(name)}Folder`;
11159
+ }
11160
+ function datasourceVarName(name) {
11161
+ return `${toCamelCase(name)}Datasource`;
11162
+ }
11163
+ function componentFileName(name) {
11164
+ return toKebabCase(name);
11165
+ }
11166
+ function folderFileName(name) {
11167
+ return toKebabCase(name);
11168
+ }
11169
+ function datasourceFileName(datasource) {
11170
+ return toKebabCase(datasource.slug || datasource.name);
11171
+ }
11172
+ function generateFieldCode(fieldName, fieldData, depth) {
11173
+ const clean = stripKeys(fieldData, FIELD_STRIP_KEYS);
11174
+ return `defineField('${fieldName.replace(/'/g, "\\'")}', ${formatValue(clean, depth + 1)})`;
11175
+ }
11176
+ function sortSchemaByPos(schema) {
11177
+ return Object.entries(schema).filter(([key]) => key !== "_uid" && key !== "component").sort(([, a], [, b]) => {
11178
+ const posA = typeof a.pos === "number" ? a.pos : Infinity;
11179
+ const posB = typeof b.pos === "number" ? b.pos : Infinity;
11180
+ return posA - posB;
11181
+ });
11182
+ }
11183
+ function generateComponentFile(component, componentFolders) {
11184
+ const lines = [];
11185
+ let matchedFolder;
11186
+ if (component.component_group_uuid && componentFolders) {
11187
+ matchedFolder = componentFolders.find((f) => f.uuid === component.component_group_uuid);
11188
+ }
11189
+ lines.push("import {");
11190
+ lines.push(" defineBlock,");
11191
+ lines.push(" defineField,");
11192
+ lines.push("} from '@storyblok/schema';");
11193
+ if (matchedFolder) {
11194
+ lines.push("");
11195
+ const fVarName = folderVarName(matchedFolder.name);
11196
+ const fFileName = folderFileName(matchedFolder.name);
11197
+ lines.push(`import { ${fVarName} } from './folders/${fFileName}';`);
11198
+ }
11199
+ lines.push("");
11200
+ const varName = componentVarName(component.name);
11201
+ lines.push(`export const ${varName} = defineBlock({`);
11202
+ const clean = stripKeys(component, COMPONENT_STRIP_KEYS);
11203
+ if (matchedFolder) {
11204
+ delete clean.component_group_uuid;
11205
+ }
11206
+ const orderedKeys = [];
11207
+ if (clean.name !== void 0) {
11208
+ orderedKeys.push("name");
11209
+ }
11210
+ if (clean.display_name !== void 0) {
11211
+ orderedKeys.push("display_name");
11212
+ }
11213
+ if (clean.is_root !== void 0) {
11214
+ orderedKeys.push("is_root");
11215
+ }
11216
+ if (clean.is_nestable !== void 0) {
11217
+ orderedKeys.push("is_nestable");
11218
+ }
11219
+ const handled = /* @__PURE__ */ new Set(["name", "display_name", "is_root", "is_nestable", "schema"]);
11220
+ for (const key of Object.keys(clean).sort()) {
11221
+ if (!handled.has(key)) {
11222
+ orderedKeys.push(key);
11223
+ }
11224
+ }
11225
+ if (matchedFolder) {
11226
+ const fVarName = folderVarName(matchedFolder.name);
11227
+ lines.push(`${INDENT}component_group_uuid: ${fVarName}.uuid,`);
11228
+ }
11229
+ for (const key of orderedKeys) {
11230
+ lines.push(`${INDENT}${key}: ${formatValue(clean[key], 1)},`);
11231
+ }
11232
+ if (clean.schema && typeof clean.schema === "object") {
11233
+ const schema = clean.schema;
11234
+ const sortedFields = sortSchemaByPos(schema);
11235
+ if (sortedFields.length > 0) {
11236
+ lines.push(`${INDENT}schema: [`);
11237
+ for (const [fieldName, fieldData] of sortedFields) {
11238
+ const fieldCode = generateFieldCode(fieldName, fieldData, 2);
11239
+ lines.push(`${INDENT}${INDENT}${fieldCode},`);
11240
+ }
11241
+ lines.push(`${INDENT}],`);
11242
+ } else {
11243
+ lines.push(`${INDENT}schema: [],`);
11244
+ }
11245
+ }
11246
+ lines.push("});");
11247
+ lines.push("");
11248
+ return lines.join("\n");
11249
+ }
11250
+ function generateFolderFile(folder) {
11251
+ const lines = [];
11252
+ lines.push("import { defineBlockFolder } from '@storyblok/schema';");
11253
+ lines.push("");
11254
+ const varName = folderVarName(folder.name);
11255
+ lines.push(`export const ${varName} = defineBlockFolder({`);
11256
+ const clean = stripKeys(folder, FOLDER_INIT_STRIP_KEYS);
11257
+ if (clean.name !== void 0) {
11258
+ lines.push(`${INDENT}name: ${formatValue(clean.name, 1)},`);
11259
+ }
11260
+ const handled = /* @__PURE__ */ new Set(["name"]);
11261
+ for (const [key, value] of Object.entries(clean).sort(([a], [b]) => a.localeCompare(b))) {
11262
+ if (!handled.has(key)) {
11263
+ lines.push(`${INDENT}${key}: ${formatValue(value, 1)},`);
11264
+ }
11265
+ }
11266
+ lines.push("});");
11267
+ lines.push("");
11268
+ return lines.join("\n");
11269
+ }
11270
+ function generateDatasourceFile(datasource) {
11271
+ const lines = [];
11272
+ lines.push("import { defineDatasource } from '@storyblok/schema';");
11273
+ lines.push("");
11274
+ const varName = datasourceVarName(datasource.name);
11275
+ lines.push(`export const ${varName} = defineDatasource({`);
11276
+ const clean = stripKeys(datasource, DATASOURCE_STRIP_KEYS);
11277
+ if (clean.name !== void 0) {
11278
+ lines.push(`${INDENT}name: ${formatValue(clean.name, 1)},`);
11279
+ }
11280
+ if (clean.slug !== void 0) {
11281
+ lines.push(`${INDENT}slug: ${formatValue(clean.slug, 1)},`);
11282
+ }
11283
+ const handled = /* @__PURE__ */ new Set(["name", "slug"]);
11284
+ for (const [key, value] of Object.entries(clean).sort(([a], [b]) => a.localeCompare(b))) {
11285
+ if (!handled.has(key)) {
11286
+ lines.push(`${INDENT}${key}: ${formatValue(value, 1)},`);
11287
+ }
11288
+ }
11289
+ lines.push("});");
11290
+ lines.push("");
11291
+ return lines.join("\n");
11292
+ }
11293
+ function generateSchemaFile(components, componentFolders, datasources) {
11294
+ const lines = [];
11295
+ lines.push("import type { Schema as InferSchema, Story as InferStory } from '@storyblok/schema';");
11296
+ lines.push("import type { MapiStory as InferStoryMapi } from '@storyblok/schema';");
11297
+ lines.push("");
11298
+ for (const component of components) {
11299
+ const varName = componentVarName(component.name);
11300
+ const fileName = componentFileName(component.name);
11301
+ lines.push(`import { ${varName} } from './components/${fileName}';`);
11302
+ }
11303
+ for (const folder of componentFolders) {
11304
+ const varName = folderVarName(folder.name);
11305
+ const fileName = folderFileName(folder.name);
11306
+ lines.push(`import { ${varName} } from './components/folders/${fileName}';`);
11307
+ }
11308
+ for (const datasource of datasources) {
11309
+ const varName = datasourceVarName(datasource.name);
11310
+ const fileName = datasourceFileName(datasource);
11311
+ lines.push(`import { ${varName} } from './datasources/${fileName}';`);
11312
+ }
11313
+ lines.push("");
11314
+ lines.push("export const schema = {");
11315
+ if (components.length > 0) {
11316
+ lines.push(" blocks: {");
11317
+ for (const component of components) {
11318
+ const varName = componentVarName(component.name);
11319
+ lines.push(` ${varName},`);
11320
+ }
11321
+ lines.push(" },");
11322
+ }
11323
+ if (componentFolders.length > 0) {
11324
+ lines.push(" blockFolders: {");
11325
+ for (const folder of componentFolders) {
11326
+ const varName = folderVarName(folder.name);
11327
+ lines.push(` ${varName},`);
11328
+ }
11329
+ lines.push(" },");
11330
+ }
11331
+ if (datasources.length > 0) {
11332
+ lines.push(" datasources: {");
11333
+ for (const datasource of datasources) {
11334
+ const varName = datasourceVarName(datasource.name);
11335
+ lines.push(` ${varName},`);
11336
+ }
11337
+ lines.push(" },");
11338
+ }
11339
+ lines.push("};");
11340
+ lines.push("");
11341
+ lines.push("export type Schema = InferSchema<typeof schema>;");
11342
+ lines.push("export type Blocks = Schema['blocks'];");
11343
+ lines.push("export type Story = InferStory<Blocks>;");
11344
+ lines.push("export type StoryMapi = InferStoryMapi<Blocks>;");
11345
+ lines.push("");
11346
+ return lines.join("\n");
11347
+ }
11348
+
11349
+ async function writeFileWithDirs(filePath, content) {
11350
+ const dir = dirname(filePath);
11351
+ await mkdir(dir, { recursive: true });
11352
+ await writeFile(filePath, content, "utf-8");
11353
+ }
11354
+ async function writeSchemaFiles(targetPath, components, componentFolders, datasources) {
11355
+ const writtenFiles = [];
11356
+ for (const comp of components) {
11357
+ const fileName = componentFileName(comp.name);
11358
+ const filePath = join(targetPath, "components", `${fileName}.ts`);
11359
+ await writeFileWithDirs(filePath, generateComponentFile(comp, componentFolders));
11360
+ writtenFiles.push(filePath);
11361
+ }
11362
+ for (const folder of componentFolders) {
11363
+ const fileName = folderFileName(folder.name);
11364
+ const filePath = join(targetPath, "components", "folders", `${fileName}.ts`);
11365
+ await writeFileWithDirs(filePath, generateFolderFile(folder));
11366
+ writtenFiles.push(filePath);
11367
+ }
11368
+ for (const ds of datasources) {
11369
+ const fileName = datasourceFileName(ds);
11370
+ const filePath = join(targetPath, "datasources", `${fileName}.ts`);
11371
+ await writeFileWithDirs(filePath, generateDatasourceFile(ds));
11372
+ writtenFiles.push(filePath);
11373
+ }
11374
+ const schemaPath = join(targetPath, "schema.ts");
11375
+ await writeFileWithDirs(schemaPath, generateSchemaFile(components, componentFolders, datasources));
11376
+ writtenFiles.push(schemaPath);
11377
+ return writtenFiles;
11378
+ }
11379
+
11380
+ async function isTargetEmpty(targetPath) {
11381
+ try {
11382
+ const entries = await readdir(targetPath);
11383
+ return entries.every((entry) => entry.startsWith("."));
11384
+ } catch (maybeError) {
11385
+ const error = maybeError;
11386
+ if (error?.code === "ENOENT") {
11387
+ return true;
11388
+ }
11389
+ throw error;
11390
+ }
11391
+ }
11392
+ 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) => {
11393
+ const ui = getUI();
11394
+ const logger = getLogger();
11395
+ const reporter = getReporter();
11396
+ const { space, verbose } = command.optsWithGlobals();
11397
+ const { state } = session();
11398
+ ui.title(commands.SCHEMA, colorPalette.SCHEMA, "Initializing schema...");
11399
+ logger.info("Schema init started", { space });
11400
+ if (!requireAuthentication(state, verbose)) {
11401
+ return;
11402
+ }
11403
+ if (!space) {
11404
+ handleError(new CommandError("Please provide the space as argument --space SPACE_ID."), verbose);
11405
+ return;
11406
+ }
11407
+ const targetPath = resolve(options.outDir);
11408
+ const targetDisplayPath = displayPath(targetPath, options.outDir);
11409
+ if (!await isTargetEmpty(targetPath)) {
11410
+ handleError(
11411
+ 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.`),
11412
+ verbose
11413
+ );
11414
+ return;
11415
+ }
11416
+ const summary = { total: 0, succeeded: 0, failed: 0 };
11417
+ try {
11418
+ const fetchSpinner = ui.createSpinner(`Fetching schema from space ${space}...`);
11419
+ let fetchResult;
11420
+ try {
11421
+ fetchResult = await fetchRemoteSchema(space);
11422
+ } catch (maybeError) {
11423
+ fetchSpinner.failed("Failed to fetch remote schema");
11424
+ handleError(toError(maybeError), verbose);
11425
+ return;
11426
+ }
11427
+ const { rawComponents, rawComponentFolders, rawDatasources } = fetchResult;
11428
+ fetchSpinner.succeed(`Found: ${rawComponents.length} components, ${rawComponentFolders.length} component folders, ${rawDatasources.length} datasources`);
11429
+ const writeSpinner = ui.createSpinner(`Generating TypeScript files to ${targetDisplayPath}...`);
11430
+ const writtenFiles = await writeSchemaFiles(targetPath, rawComponents, rawComponentFolders, rawDatasources);
11431
+ summary.total = writtenFiles.length;
11432
+ summary.succeeded = writtenFiles.length;
11433
+ writeSpinner.succeed(`Generated ${writtenFiles.length} files`);
11434
+ ui.list(writtenFiles.map((file) => displayPath(file, options.outDir)));
11435
+ ui.warn("`schema init` is a one-time bootstrap step for adopting an existing space. Review generated files before continuing.");
11436
+ ui.info("After bootstrapping, keep your local schema as the source of truth and use `schema push` for ongoing changes.");
11437
+ ui.info("Make sure `@storyblok/schema` is installed in the project that imports these files (e.g. `pnpm add @storyblok/schema`).");
11438
+ } catch (maybeError) {
11439
+ summary.failed += 1;
11440
+ handleError(toError(maybeError), verbose);
11441
+ } finally {
11442
+ logger.info("Schema init finished", { summary });
11443
+ reporter.addSummary("schemaInitResults", summary);
11444
+ reporter.finalize();
11445
+ }
11446
+ });
11447
+
11448
+ const API_ASSIGNED_FIELDS = [
11449
+ "id",
11450
+ "created_at",
11451
+ "updated_at",
11452
+ "real_name",
11453
+ "all_presets",
11454
+ "image",
11455
+ "uuid"
11456
+ ];
11457
+ function stripApiFields(payload) {
11458
+ const result = { ...payload };
11459
+ for (const field of API_ASSIGNED_FIELDS) {
11460
+ delete result[field];
11461
+ }
11462
+ return result;
11463
+ }
11464
+ async function listChangesets(basePath) {
11465
+ const dir = join(basePath, "schema", "changesets");
11466
+ if (!await fileExists(dir)) {
11467
+ return [];
11468
+ }
11469
+ const files = await readDirectory(dir);
11470
+ return files.filter((f) => f.endsWith(".json")).sort().reverse().map((f) => join(dir, f));
11471
+ }
11472
+ async function loadChangeset(filePath) {
11473
+ const content = await readFile$1(filePath, "utf-8");
11474
+ return JSON.parse(content);
11475
+ }
11476
+ function buildRollbackOps(changeset) {
11477
+ if (changeset.changes.length === 0) {
11478
+ return [];
11479
+ }
11480
+ return changeset.changes.map((entry) => {
11481
+ switch (entry.action) {
11482
+ case "create":
11483
+ return { type: entry.type, name: entry.name, action: "delete", payload: {} };
11484
+ case "update":
11485
+ return { type: entry.type, name: entry.name, action: "update", payload: entry.before ?? {} };
11486
+ case "delete":
11487
+ return { type: entry.type, name: entry.name, action: "create", payload: entry.before ?? {} };
11488
+ default:
11489
+ return { type: entry.type, name: entry.name, action: entry.action, payload: {} };
11490
+ }
11491
+ });
11492
+ }
11493
+ function rollbackAction(original) {
11494
+ switch (original) {
11495
+ case "create":
11496
+ return "delete";
11497
+ case "update":
11498
+ return "update";
11499
+ case "delete":
11500
+ return "create";
11501
+ }
11502
+ }
11503
+ function formatRollbackOutput(changes) {
11504
+ const byType = {
11505
+ component: [],
11506
+ componentFolder: [],
11507
+ datasource: []
11508
+ };
11509
+ for (const entry of changes) {
11510
+ byType[entry.type]?.push(entry);
11511
+ }
11512
+ const icons = {
11513
+ create: chalk.green("+"),
11514
+ update: chalk.yellow("~"),
11515
+ delete: chalk.red("-")
11516
+ };
11517
+ const lines = [];
11518
+ const sections = [
11519
+ ["Components", byType.component],
11520
+ ["Component Folders", byType.componentFolder],
11521
+ ["Datasources", byType.datasource]
11522
+ ];
11523
+ for (const [label, entries] of sections) {
11524
+ if (entries.length === 0) {
11525
+ continue;
11526
+ }
11527
+ lines.push(chalk.bold(label));
11528
+ for (const entry of entries) {
11529
+ const action = rollbackAction(entry.action);
11530
+ const icon = icons[action] ?? " ";
11531
+ const name = action === "delete" ? chalk.red(entry.name) : entry.name;
11532
+ lines.push(` ${icon} ${name} ${chalk.dim(`(${action})`)}`);
11533
+ if (entry.action === "update" && entry.before && entry.after) {
11534
+ let fromStr;
11535
+ let toStr;
11536
+ if (entry.type === "component") {
11537
+ fromStr = serializeComponent(applyDefaults(entry.after, COMPONENT_DEFAULTS));
11538
+ toStr = serializeComponent(applyDefaults(entry.before, COMPONENT_DEFAULTS));
11539
+ } else if (entry.type === "componentFolder") {
11540
+ fromStr = serializeComponentFolder(entry.after);
11541
+ toStr = serializeComponentFolder(entry.before);
11542
+ } else {
11543
+ fromStr = serializeDatasource(entry.after);
11544
+ toStr = serializeDatasource(entry.before);
11545
+ }
11546
+ if (fromStr !== toStr) {
11547
+ const patch = createTwoFilesPatch(
11548
+ `current/${entry.name}`,
11549
+ `restore/${entry.name}`,
11550
+ fromStr,
11551
+ toStr,
11552
+ "current",
11553
+ "restore"
11554
+ );
11555
+ for (const line of patch.split("\n")) {
11556
+ if (line.startsWith("+") && !line.startsWith("+++")) {
11557
+ lines.push(` ${chalk.green(line)}`);
11558
+ } else if (line.startsWith("-") && !line.startsWith("---")) {
11559
+ lines.push(` ${chalk.red(line)}`);
11560
+ }
11561
+ }
11562
+ }
11563
+ }
11564
+ }
11565
+ lines.push("");
11566
+ }
11567
+ return lines.join("\n").trimEnd();
11568
+ }
11569
+ async function executeRollback(spaceId, ops, remote) {
11570
+ const client = getMapiClient();
11571
+ const spaceIdNum = Number(spaceId);
11572
+ let created = 0;
11573
+ let updated = 0;
11574
+ let deleted = 0;
11575
+ const folderOps = ops.filter((op) => op.type === "componentFolder");
11576
+ const componentOps = ops.filter((op) => op.type === "component");
11577
+ const datasourceOps = ops.filter((op) => op.type === "datasource");
11578
+ const folderUuidRemap = /* @__PURE__ */ new Map();
11579
+ for (const op of folderOps.filter((o) => o.action !== "delete")) {
11580
+ if (op.action === "create") {
11581
+ const oldUuid = op.payload.uuid;
11582
+ const payload = toComponentFolderCreate(stripApiFields(op.payload));
11583
+ try {
11584
+ const response = await client.componentFolders.create({
11585
+ path: { space_id: spaceIdNum },
11586
+ body: { component_group: payload },
11587
+ throwOnError: true
11588
+ });
11589
+ const remoteUuid = response.data?.component_group?.uuid;
11590
+ if (remoteUuid && typeof oldUuid === "string") {
11591
+ folderUuidRemap.set(oldUuid, remoteUuid);
11592
+ }
11593
+ created++;
11594
+ } catch (error) {
11595
+ handleAPIError("push_component_group", error, `Failed to create folder ${op.name}`);
11596
+ }
11597
+ } else if (op.action === "update") {
11598
+ const existing = remote.componentFolders.get(op.name);
11599
+ if (existing?.id) {
11600
+ const payload = toComponentFolderCreate(stripApiFields(op.payload));
11601
+ try {
11602
+ await client.componentFolders.update(existing.id, {
11603
+ path: { space_id: spaceIdNum },
11604
+ body: { component_group: payload },
11605
+ throwOnError: true
11606
+ });
11607
+ updated++;
11608
+ } catch (error) {
11609
+ handleAPIError("update_component_group", error, `Failed to update folder ${op.name}`);
11610
+ }
11611
+ }
11612
+ }
11613
+ }
11614
+ for (const op of componentOps) {
11615
+ const oldUuid = op.payload.component_group_uuid;
11616
+ if (typeof oldUuid !== "string") {
11617
+ continue;
11618
+ }
11619
+ const newUuid = folderUuidRemap.get(oldUuid);
11620
+ if (newUuid) {
11621
+ op.payload.component_group_uuid = newUuid;
11622
+ }
11623
+ }
11624
+ for (const op of componentOps.filter((o) => o.action !== "delete")) {
11625
+ if (op.action === "create") {
11626
+ const payload = toComponentCreate(stripApiFields(op.payload));
11627
+ try {
11628
+ await client.components.create({
11629
+ path: { space_id: spaceIdNum },
11630
+ body: { component: payload },
11631
+ throwOnError: true
11632
+ });
11633
+ created++;
11634
+ } catch (error) {
11635
+ handleAPIError("push_component", error, `Failed to create component ${op.name}`);
11636
+ }
11637
+ } else if (op.action === "update") {
11638
+ const existing = remote.components.get(op.name);
11639
+ if (existing?.id) {
11640
+ const payload = toComponentUpdate(stripApiFields(op.payload));
11641
+ try {
11642
+ await client.components.update(existing.id, {
11643
+ path: { space_id: spaceIdNum },
11644
+ body: { component: payload },
11645
+ throwOnError: true
11646
+ });
11647
+ updated++;
11648
+ } catch (error) {
11649
+ handleAPIError("update_component", error, `Failed to update component ${op.name}`);
11650
+ }
11651
+ }
11652
+ }
11653
+ }
11654
+ for (const op of datasourceOps.filter((o) => o.action !== "delete")) {
11655
+ if (op.action === "create") {
11656
+ const payload = toDatasourceCreate(stripApiFields(op.payload));
11657
+ try {
11658
+ await client.datasources.create({
11659
+ path: { space_id: spaceIdNum },
11660
+ body: { datasource: payload },
11661
+ throwOnError: true
11662
+ });
11663
+ created++;
11664
+ } catch (error) {
11665
+ handleAPIError("push_datasource", error, `Failed to create datasource ${op.name}`);
11666
+ }
11667
+ } else if (op.action === "update") {
11668
+ const existing = remote.datasources.get(op.name);
11669
+ if (existing?.id) {
11670
+ const payload = toDatasourceUpdate(stripApiFields(op.payload), existing);
11671
+ try {
11672
+ await client.datasources.update(existing.id, {
11673
+ path: { space_id: spaceIdNum },
11674
+ body: { datasource: payload },
11675
+ throwOnError: true
11676
+ });
11677
+ updated++;
11678
+ } catch (error) {
11679
+ handleAPIError("update_datasource", error, `Failed to update datasource ${op.name}`);
11680
+ }
11681
+ }
11682
+ }
11683
+ }
11684
+ for (const op of datasourceOps.filter((o) => o.action === "delete")) {
11685
+ const existing = remote.datasources.get(op.name);
11686
+ if (existing?.id) {
11687
+ try {
11688
+ await client.datasources.delete(existing.id, {
11689
+ path: { space_id: spaceIdNum },
11690
+ throwOnError: true
11691
+ });
11692
+ deleted++;
11693
+ } catch (error) {
11694
+ handleAPIError("delete_datasource", error, `Failed to delete datasource ${op.name}`);
11695
+ }
11696
+ }
11697
+ }
11698
+ for (const op of componentOps.filter((o) => o.action === "delete")) {
11699
+ const existing = remote.components.get(op.name);
11700
+ if (existing?.id) {
11701
+ try {
11702
+ await client.components.delete(existing.id, {
11703
+ path: { space_id: spaceIdNum },
11704
+ throwOnError: true
11705
+ });
11706
+ deleted++;
11707
+ } catch (error) {
11708
+ handleAPIError("push_component", error, `Failed to delete component ${op.name}`);
11709
+ }
11710
+ }
11711
+ }
11712
+ for (const op of folderOps.filter((o) => o.action === "delete")) {
11713
+ const existing = remote.componentFolders.get(op.name);
11714
+ if (existing?.id) {
11715
+ try {
11716
+ await client.componentFolders.delete(existing.id, {
11717
+ path: { space_id: spaceIdNum },
11718
+ throwOnError: true
11719
+ });
11720
+ deleted++;
11721
+ } catch (error) {
11722
+ handleAPIError("push_component_group", error, `Failed to delete folder ${op.name}`);
11723
+ }
11724
+ }
11725
+ }
11726
+ return { created, updated, deleted };
11727
+ }
11728
+
11729
+ 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) => {
11730
+ const ui = getUI();
11731
+ const logger = getLogger();
11732
+ const reporter = getReporter();
11733
+ const { space, path: basePath, verbose } = command.optsWithGlobals();
11734
+ const { state } = session();
11735
+ ui.title(commands.SCHEMA, colorPalette.SCHEMA, "Rolling back schema...");
11736
+ logger.info("Schema rollback started", { changesetFile, space });
11737
+ if (!requireAuthentication(state, verbose)) {
11738
+ return;
11739
+ }
11740
+ if (!space) {
11741
+ handleError(new CommandError("Please provide the space as argument --space SPACE_ID."), verbose);
11742
+ return;
11743
+ }
11744
+ const summary = { total: 0, succeeded: 0, failed: 0 };
11745
+ try {
11746
+ const resolvedBase = resolvePath(basePath, "");
11747
+ let resolvedFile;
11748
+ if (changesetFile) {
11749
+ resolvedFile = changesetFile;
11750
+ } else if (options.latest) {
11751
+ const available = await listChangesets(resolvedBase);
11752
+ if (available.length === 0) {
11753
+ ui.warn("No changesets found. Run `schema push` first to create one.");
11754
+ return;
11755
+ }
11756
+ resolvedFile = available[0];
11757
+ ui.info(`Using latest changeset: ${basename(resolvedFile)}`);
11758
+ } else {
11759
+ const available = await listChangesets(resolvedBase);
11760
+ if (available.length === 0) {
11761
+ ui.warn("No changesets found. Run `schema push` first to create one.");
11762
+ return;
11763
+ }
11764
+ resolvedFile = await select({
11765
+ message: "Select a changeset to roll back:",
11766
+ choices: available.map((f) => ({ name: basename(f), value: f }))
11767
+ });
11768
+ }
11769
+ let changeset;
11770
+ try {
11771
+ changeset = await loadChangeset(resolvedFile);
11772
+ } catch (maybeError) {
11773
+ handleError(toError(maybeError), verbose);
11774
+ return;
11775
+ }
11776
+ logger.info("Changeset loaded", { file: resolvedFile, changes: changeset.changes.length });
11777
+ const ops = buildRollbackOps(changeset);
11778
+ if (ops.length === 0) {
11779
+ ui.ok("Changeset has no changes \u2014 nothing to roll back.");
11780
+ return;
11781
+ }
11782
+ ui.br();
11783
+ ui.log(formatRollbackOutput(changeset.changes));
11784
+ if (options.dryRun) {
11785
+ ui.info("Dry run \u2014 no changes applied.");
11786
+ logger.info("Dry run completed", { ops: ops.length });
11787
+ return;
11788
+ }
11789
+ if (!options.yes) {
11790
+ const confirmed = await confirm({
11791
+ message: `Apply rollback of ${ops.length} change(s) from ${basename(resolvedFile)}?`,
11792
+ default: false
11793
+ });
11794
+ if (!confirmed) {
11795
+ ui.info("Rollback cancelled.");
11796
+ return;
11797
+ }
11798
+ }
11799
+ const remoteSpinner = ui.createSpinner(`Fetching current remote state from space ${space}...`);
11800
+ let remoteResult;
11801
+ try {
11802
+ remoteResult = await fetchRemoteSchema(space);
11803
+ } catch (maybeError) {
11804
+ remoteSpinner.failed("Failed to fetch remote schema");
11805
+ handleError(toError(maybeError), verbose);
11806
+ return;
11807
+ }
11808
+ const { remote } = remoteResult;
11809
+ remoteSpinner.succeed(`Remote: ${remote.components.size} components, ${remote.componentFolders.size} component folders, ${remote.datasources.size} datasources`);
11810
+ const rollbackSpinner = ui.createSpinner("Applying rollback...");
11811
+ let result;
11812
+ try {
11813
+ result = await executeRollback(space, ops, remote);
11814
+ } catch (error) {
11815
+ rollbackSpinner.failed("Failed to apply rollback");
11816
+ throw error;
11817
+ }
11818
+ summary.total = result.created + result.updated + result.deleted;
11819
+ summary.succeeded = summary.total;
11820
+ rollbackSpinner.succeed(`Rolled back: ${result.created} created, ${result.updated} updated, ${result.deleted} deleted.`);
11821
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
11822
+ const rollbackChangesetPath = await saveChangeset(resolvedBase, {
11823
+ timestamp,
11824
+ spaceId: Number(space),
11825
+ remote: { components: remoteResult.rawComponents, componentFolders: remoteResult.rawComponentFolders, datasources: remoteResult.rawDatasources },
11826
+ changes: ops.map((op) => ({
11827
+ type: op.type,
11828
+ name: op.name,
11829
+ action: op.action,
11830
+ ...Object.keys(op.payload).length > 0 && { after: op.payload }
11831
+ }))
11832
+ });
11833
+ logger.info("Rollback changeset saved", { path: displayPath(rollbackChangesetPath, basePath) });
11834
+ } catch (maybeError) {
11835
+ summary.failed += 1;
11836
+ handleError(toError(maybeError), verbose);
11837
+ } finally {
11838
+ logger.info("Schema rollback finished", { summary });
11839
+ reporter.addSummary("schemaRollbackResults", summary);
11840
+ reporter.finalize();
11841
+ }
11842
+ });
11843
+
9785
11844
  const program = getProgram();
9786
11845
  konsola.br();
9787
11846
  konsola.br();