storyblok 4.17.4 → 4.18.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,10 +7,10 @@ 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
- import fs, { mkdir, writeFile, readdir, readFile as readFile$1, appendFile, access, constants, unlink } from 'node:fs/promises';
13
+ import fs, { mkdir, writeFile, access, constants, readFile as readFile$1, appendFile, readdir, unlink } from 'node:fs/promises';
14
14
  import filenamify from 'filenamify';
15
15
  import { createManagementApiClient, normalizeAssetUrl } from '@storyblok/management-api-client';
16
16
  import { select, password, input, confirm } from '@inquirer/prompts';
@@ -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",
@@ -148,18 +151,6 @@ const directories = {
148
151
  stories: "stories"
149
152
  };
150
153
 
151
- const chunk = (items, size) => {
152
- const all = Array.from(items);
153
- if (all.length === 0) {
154
- return [];
155
- }
156
- const chunks = [];
157
- for (let i = 0; i < all.length; i += size) {
158
- chunks.push(all.slice(i, i + size));
159
- }
160
- return chunks;
161
- };
162
-
163
154
  function isPlainObject(value) {
164
155
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
165
156
  }
@@ -1017,11 +1008,11 @@ function requireAuthentication(state, verbose = false) {
1017
1008
  return true;
1018
1009
  }
1019
1010
 
1020
- const toCamelCase = (str) => {
1011
+ const toCamelCase$1 = (str) => {
1021
1012
  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, "");
1022
1013
  };
1023
1014
  const toPascalCase = (str) => {
1024
- const camelCase = toCamelCase(str);
1015
+ const camelCase = toCamelCase$1(str);
1025
1016
  return camelCase ? camelCase[0].toUpperCase() + camelCase.slice(1) : camelCase;
1026
1017
  };
1027
1018
  const capitalize = (str) => {
@@ -1119,6 +1110,25 @@ function getPackageJson() {
1119
1110
  return packageJson$1;
1120
1111
  }
1121
1112
 
1113
+ async function fetchAllPages(fetchFunction, extractDataFunction) {
1114
+ const items = [];
1115
+ let page = 1;
1116
+ while (true) {
1117
+ const { data, response } = await fetchFunction(page);
1118
+ const totalHeader = response.headers.get("total");
1119
+ const fetchedItems = extractDataFunction(data);
1120
+ items.push(...fetchedItems);
1121
+ if (!totalHeader) {
1122
+ return items;
1123
+ }
1124
+ const total = Number(totalHeader);
1125
+ if (Number.isNaN(total) || items.length >= total || fetchedItems.length === 0) {
1126
+ return items;
1127
+ }
1128
+ page++;
1129
+ }
1130
+ }
1131
+
1122
1132
  const __filename$1 = fileURLToPath(import.meta.url);
1123
1133
  const __dirname$1 = dirname(__filename$1);
1124
1134
  function isRegion(value) {
@@ -1207,6 +1217,10 @@ class UI {
1207
1217
  this.br();
1208
1218
  }
1209
1219
  }
1220
+ /** Plain console.log passthrough — use for preformatted or multi-line text. */
1221
+ log(message) {
1222
+ this.console?.log(message);
1223
+ }
1210
1224
  list(items) {
1211
1225
  for (const item of items) {
1212
1226
  this.console?.log(` ${item}`);
@@ -1394,16 +1408,14 @@ async function fileExists(path) {
1394
1408
  return false;
1395
1409
  }
1396
1410
  }
1397
- function filterJsonBySuffix(files, suffix) {
1398
- return files.filter((file) => {
1399
- if (!file.endsWith(".json")) {
1400
- return false;
1401
- }
1402
- if (suffix) {
1403
- return file.endsWith(`.${suffix}.json`);
1404
- }
1405
- return true;
1406
- });
1411
+ function consolidatedFilename(defaultFilename, suffix) {
1412
+ return suffix ? `${defaultFilename}.${suffix}.json` : `${defaultFilename}.json`;
1413
+ }
1414
+ async function shouldUseSeparateFiles(resolvedPath, defaultFilename, separateFiles, suffix) {
1415
+ if (separateFiles !== void 0) {
1416
+ return separateFiles;
1417
+ }
1418
+ return !await fileExists(join(resolvedPath, consolidatedFilename(defaultFilename, suffix)));
1407
1419
  }
1408
1420
 
1409
1421
  const REPORT_STATUS = {
@@ -2115,14 +2127,14 @@ async function performInteractiveLogin(options) {
2115
2127
  }
2116
2128
  }
2117
2129
 
2118
- const program$e = getProgram();
2130
+ const program$f = getProgram();
2119
2131
  const allRegionsText = Object.values(regions).join(",");
2120
- 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(
2132
+ 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(
2121
2133
  "-r, --region <region>",
2122
2134
  `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}.`
2123
2135
  ).action(async (options) => {
2124
2136
  konsola.title(`${commands.LOGIN}`, colorPalette.LOGIN);
2125
- const verbose = program$e.opts().verbose;
2137
+ const verbose = program$f.opts().verbose;
2126
2138
  const { token, region } = options;
2127
2139
  const { state, updateSession, persistCredentials } = session();
2128
2140
  if (state.isLoggedIn && !state.envLogin) {
@@ -2180,10 +2192,10 @@ program$e.command(commands.LOGIN).description("Login to the Storyblok CLI").opti
2180
2192
  konsola.br();
2181
2193
  });
2182
2194
 
2183
- const program$d = getProgram();
2184
- program$d.command(commands.LOGOUT).description("Logout from the Storyblok CLI").action(async () => {
2195
+ const program$e = getProgram();
2196
+ program$e.command(commands.LOGOUT).description("Logout from the Storyblok CLI").action(async () => {
2185
2197
  konsola.title(`${commands.LOGOUT}`, colorPalette.LOGOUT);
2186
- const verbose = program$d.opts().verbose;
2198
+ const verbose = program$e.opts().verbose;
2187
2199
  try {
2188
2200
  const { state } = session();
2189
2201
  if (!state.isLoggedIn || !state.password || !state.region) {
@@ -2230,10 +2242,10 @@ async function openSignupInBrowser(url) {
2230
2242
  }
2231
2243
  }
2232
2244
 
2233
- const program$c = getProgram();
2234
- program$c.command(commands.SIGNUP).description("Sign up for Storyblok").action(async () => {
2245
+ const program$d = getProgram();
2246
+ program$d.command(commands.SIGNUP).description("Sign up for Storyblok").action(async () => {
2235
2247
  konsola.title(`${commands.SIGNUP}`, colorPalette.SIGNUP);
2236
- const verbose = program$c.opts().verbose;
2248
+ const verbose = program$d.opts().verbose;
2237
2249
  const { state } = session();
2238
2250
  if (state.isLoggedIn && !state.envLogin) {
2239
2251
  konsola.ok(`You are already logged in. If you want to signup with a different account, please logout first.`);
@@ -2254,10 +2266,10 @@ program$c.command(commands.SIGNUP).description("Sign up for Storyblok").action(a
2254
2266
  konsola.br();
2255
2267
  });
2256
2268
 
2257
- const program$b = getProgram();
2258
- program$b.command(commands.USER).description("Get the current user").action(async () => {
2269
+ const program$c = getProgram();
2270
+ program$c.command(commands.USER).description("Get the current user").action(async () => {
2259
2271
  konsola.title(`${commands.USER}`, colorPalette.USER);
2260
- const verbose = program$b.opts().verbose;
2272
+ const verbose = program$c.opts().verbose;
2261
2273
  const { state } = session();
2262
2274
  if (!requireAuthentication(state)) {
2263
2275
  return;
@@ -2285,117 +2297,24 @@ program$b.command(commands.USER).description("Get the current user").action(asyn
2285
2297
  konsola.br();
2286
2298
  });
2287
2299
 
2288
- const program$a = getProgram();
2289
- const componentsCommand = program$a.command(commands.COMPONENTS).alias("comp").description(`Manage your space's block schema`);
2290
-
2291
- function isComponent(item) {
2292
- return "schema" in item;
2293
- }
2294
- function isPreset(item) {
2295
- return "component_id" in item && "preset" in item;
2296
- }
2297
- function isInternalTag(item) {
2298
- return "object_type" in item;
2299
- }
2300
- function isComponentGroup(item) {
2301
- return "uuid" in item && !("schema" in item);
2302
- }
2303
- async function loadComponents(directoryPath, options) {
2304
- const files = await readDirectory(directoryPath);
2305
- const { suffix } = options ?? {};
2306
- const componentMap = /* @__PURE__ */ new Map();
2307
- const groupMap = /* @__PURE__ */ new Map();
2308
- const tagMap = /* @__PURE__ */ new Map();
2309
- const presets = [];
2310
- const duplicates = [];
2311
- for (const file of filterJsonBySuffix(files, suffix)) {
2312
- const { data, error } = await readJsonFile(join(directoryPath, file));
2313
- if (error) {
2314
- handleFileSystemError("read", error);
2315
- continue;
2316
- }
2317
- for (const item of data) {
2318
- if (isPreset(item)) {
2319
- presets.push(item);
2320
- } else if (isInternalTag(item)) {
2321
- if (!item.id) {
2322
- throw new Error('Internal tag is missing "id"!');
2323
- }
2324
- tagMap.set(item.id, item);
2325
- } else if (isComponent(item)) {
2326
- const existing = componentMap.get(item.name);
2327
- if (existing) {
2328
- duplicates.push(`Component "${item.name}" found in both "${existing.file}" and "${file}"`);
2329
- }
2330
- componentMap.set(item.name, { component: item, file });
2331
- } else if (isComponentGroup(item)) {
2332
- groupMap.set(item.id, item);
2333
- }
2334
- }
2335
- }
2336
- if (duplicates.length) {
2337
- throw new FileSystemError(
2338
- "invalid_argument",
2339
- "read",
2340
- new Error("Duplicate components detected"),
2341
- `Duplicate components found in ${directoryPath}:
2342
-
2343
- ${duplicates.join("\n")}
2344
-
2345
- This can happen when multiple environment snapshots (e.g. components.json and components.dev.json) or mixed formats coexist in the same directory.
2346
-
2347
- To fix this, either:
2348
- - Use --suffix <env> to target a specific environment (e.g. --suffix dev)
2349
- - Clean up the directory and pull components again in the format you intend`
2350
- );
2351
- }
2352
- return {
2353
- components: [...componentMap.values()].map(({ component }) => component),
2354
- groups: [...groupMap.values()],
2355
- presets,
2356
- internalTags: [...tagMap.values()]
2357
- };
2358
- }
2300
+ const program$b = getProgram();
2301
+ const componentsCommand = program$b.command(commands.COMPONENTS).alias("comp").description(`Manage your space's block schema`);
2359
2302
 
2360
2303
  const DEFAULT_COMPONENTS_FILENAME = "components";
2361
- const DEFAULT_GROUPS_FILENAME = "groups";
2304
+ const DEFAULT_GROUPS_FILENAME$1 = "groups";
2362
2305
  const DEFAULT_PRESETS_FILENAME = "presets";
2363
2306
  const DEFAULT_TAGS_FILENAME = "tags";
2364
2307
 
2365
- async function fetchAllPages(fetchFunction, extractDataFunction) {
2366
- const items = [];
2367
- let page = 1;
2368
- while (true) {
2369
- const { data, response } = await fetchFunction(page);
2370
- const totalHeader = response.headers.get("total");
2371
- const fetchedItems = extractDataFunction(data);
2372
- items.push(...fetchedItems);
2373
- if (!totalHeader) {
2374
- return items;
2375
- }
2376
- const total = Number(totalHeader);
2377
- if (Number.isNaN(total) || items.length >= total || fetchedItems.length === 0) {
2378
- return items;
2379
- }
2380
- page++;
2381
- }
2382
- }
2383
-
2384
2308
  const fetchComponents = async (spaceId) => {
2385
2309
  try {
2386
2310
  const client = getMapiClient();
2387
- return await fetchAllPages(
2388
- (page) => client.components.list({
2389
- path: {
2390
- space_id: Number(spaceId)
2391
- },
2392
- query: {
2393
- page
2394
- },
2395
- throwOnError: true
2396
- }),
2397
- (data) => data?.components ?? []
2398
- );
2311
+ const { data } = await client.components.list({
2312
+ path: {
2313
+ space_id: Number(spaceId)
2314
+ },
2315
+ throwOnError: true
2316
+ });
2317
+ return data?.components ?? [];
2399
2318
  } catch (error) {
2400
2319
  handleAPIError("pull_components", error);
2401
2320
  }
@@ -2403,19 +2322,16 @@ const fetchComponents = async (spaceId) => {
2403
2322
  const fetchComponent = async (spaceId, componentName) => {
2404
2323
  try {
2405
2324
  const client = getMapiClient();
2406
- const matches = await fetchAllPages(
2407
- (page) => client.components.list({
2408
- path: {
2409
- space_id: Number(spaceId)
2410
- },
2411
- query: {
2412
- page,
2413
- search: componentName
2414
- },
2415
- throwOnError: true
2416
- }),
2417
- (data) => data?.components ?? []
2418
- );
2325
+ const { data } = await client.components.list({
2326
+ path: {
2327
+ space_id: Number(spaceId)
2328
+ },
2329
+ query: {
2330
+ search: componentName
2331
+ },
2332
+ throwOnError: true
2333
+ });
2334
+ const matches = data?.components ?? [];
2419
2335
  return matches.find((c) => c.name === componentName);
2420
2336
  } catch (error) {
2421
2337
  handleAPIError("pull_components", error, `Failed to fetch component ${componentName}`);
@@ -2482,7 +2398,7 @@ const saveComponentsToFiles = async (space, spaceData, options) => {
2482
2398
  const presetsFilePath = join(resolvedPath, suffix ? `${sanitizedName}.${DEFAULT_PRESETS_FILENAME}.${suffix}.json` : `${sanitizedName}.${DEFAULT_PRESETS_FILENAME}.json`);
2483
2399
  await saveToFile(presetsFilePath, JSON.stringify(componentPresets, null, 2));
2484
2400
  }
2485
- const groupsFilePath = join(resolvedPath, suffix ? `${DEFAULT_GROUPS_FILENAME}.${suffix}.json` : `${DEFAULT_GROUPS_FILENAME}.json`);
2401
+ const groupsFilePath = join(resolvedPath, suffix ? `${DEFAULT_GROUPS_FILENAME$1}.${suffix}.json` : `${DEFAULT_GROUPS_FILENAME$1}.json`);
2486
2402
  await saveToFile(groupsFilePath, JSON.stringify(groups, null, 2));
2487
2403
  const internalTagsFilePath = join(resolvedPath, suffix ? `${DEFAULT_TAGS_FILENAME}.${suffix}.json` : `${DEFAULT_TAGS_FILENAME}.json`);
2488
2404
  await saveToFile(internalTagsFilePath, JSON.stringify(internalTags, null, 2));
@@ -2492,7 +2408,7 @@ const saveComponentsToFiles = async (space, spaceData, options) => {
2492
2408
  const componentsFilePath = join(resolvedPath, suffix ? `${filename}.${suffix}.json` : `${filename}.json`);
2493
2409
  await saveToFile(componentsFilePath, JSON.stringify(components, null, 2));
2494
2410
  if (groups.length > 0) {
2495
- const groupsFilePath = join(resolvedPath, suffix ? `${DEFAULT_GROUPS_FILENAME}.${suffix}.json` : `${DEFAULT_GROUPS_FILENAME}.json`);
2411
+ const groupsFilePath = join(resolvedPath, suffix ? `${DEFAULT_GROUPS_FILENAME$1}.${suffix}.json` : `${DEFAULT_GROUPS_FILENAME$1}.json`);
2496
2412
  await saveToFile(groupsFilePath, JSON.stringify(groups, null, 2));
2497
2413
  }
2498
2414
  if (presets.length > 0) {
@@ -2508,6 +2424,21 @@ const saveComponentsToFiles = async (space, spaceData, options) => {
2508
2424
  }
2509
2425
  };
2510
2426
 
2427
+ function isSchemaField$1(value) {
2428
+ return typeof value === "object" && value !== null && "type" in value;
2429
+ }
2430
+ function toWritableSchema(schema) {
2431
+ if (!schema) {
2432
+ return void 0;
2433
+ }
2434
+ const result = {};
2435
+ for (const [key, value] of Object.entries(schema)) {
2436
+ if (isSchemaField$1(value)) {
2437
+ result[key] = value;
2438
+ }
2439
+ }
2440
+ return result;
2441
+ }
2511
2442
  const pushComponent = async (space, component) => {
2512
2443
  try {
2513
2444
  const client = getMapiClient();
@@ -2542,10 +2473,23 @@ const updateComponent = async (space, componentId, component) => {
2542
2473
  }
2543
2474
  };
2544
2475
  const upsertComponent = async (space, component, existingId) => {
2476
+ const { name, display_name, schema, is_root, is_nestable, component_group_uuid, color, icon, preview_field, internal_tag_ids } = component;
2477
+ const payload = {
2478
+ name,
2479
+ display_name,
2480
+ schema: toWritableSchema(schema),
2481
+ is_root,
2482
+ is_nestable,
2483
+ component_group_uuid: component_group_uuid ?? void 0,
2484
+ color: color ?? void 0,
2485
+ icon: icon ?? void 0,
2486
+ preview_field: preview_field ?? void 0,
2487
+ internal_tag_ids
2488
+ };
2545
2489
  if (existingId) {
2546
- return await updateComponent(space, existingId, component);
2490
+ return await updateComponent(space, existingId, payload);
2547
2491
  } else {
2548
- return await pushComponent(space, component);
2492
+ return await pushComponent(space, payload);
2549
2493
  }
2550
2494
  };
2551
2495
  const pushComponentGroup = async (space, componentGroup) => {
@@ -2648,7 +2592,7 @@ const pushComponentInternalTag = async (space, componentInternalTag) => {
2648
2592
  path: {
2649
2593
  space_id: Number(space)
2650
2594
  },
2651
- body: componentInternalTag,
2595
+ body: { internal_tag: componentInternalTag },
2652
2596
  throwOnError: true
2653
2597
  });
2654
2598
  return data.internal_tag;
@@ -2663,7 +2607,7 @@ const updateComponentInternalTag = async (space, tagId, componentInternalTag) =>
2663
2607
  path: {
2664
2608
  space_id: Number(space)
2665
2609
  },
2666
- body: componentInternalTag,
2610
+ body: { internal_tag: componentInternalTag },
2667
2611
  throwOnError: true
2668
2612
  });
2669
2613
  return data.internal_tag;
@@ -2679,15 +2623,11 @@ const upsertComponentInternalTag = async (space, tag, existingId) => {
2679
2623
  }
2680
2624
  };
2681
2625
  const readComponentsFiles = async (options) => {
2682
- const { from, path, suffix } = options;
2626
+ const { from, path, separateFiles, suffix } = options;
2683
2627
  const resolvedPath = resolvePath(path, `components/${from}`);
2684
- let result;
2685
2628
  try {
2686
- result = await loadComponents(resolvedPath, { suffix });
2629
+ await readdir(resolvedPath);
2687
2630
  } catch (error) {
2688
- if (error instanceof FileSystemError && error.code !== "ENOENT") {
2689
- throw error;
2690
- }
2691
2631
  const message = `No local components found for space ${chalk.bold(from)}. To push components, you need to pull them first:
2692
2632
 
2693
2633
  1. Pull the components from your source space:
@@ -2702,16 +2642,89 @@ const readComponentsFiles = async (options) => {
2702
2642
  message
2703
2643
  );
2704
2644
  }
2705
- if (!result.components.length) {
2645
+ if (await shouldUseSeparateFiles(resolvedPath, DEFAULT_COMPONENTS_FILENAME, separateFiles, suffix)) {
2646
+ return await readSeparateFiles$1(resolvedPath, suffix);
2647
+ }
2648
+ return await readConsolidatedFiles$1(resolvedPath, suffix);
2649
+ };
2650
+ async function readSeparateFiles$1(resolvedPath, suffix) {
2651
+ const files = await readdir(resolvedPath);
2652
+ const components = [];
2653
+ const presets = [];
2654
+ let groups = [];
2655
+ let internalTags = [];
2656
+ const filteredFiles = files.filter((file) => {
2657
+ if (suffix) {
2658
+ return file.endsWith(`.${suffix}.json`);
2659
+ } else {
2660
+ return !/\.\w+\.json$/.test(file) || file.endsWith(".presets.json");
2661
+ }
2662
+ });
2663
+ for (const file of filteredFiles) {
2664
+ const filePath = join(resolvedPath, file);
2665
+ if (file === `${DEFAULT_GROUPS_FILENAME$1}.json` || file === `${DEFAULT_GROUPS_FILENAME$1}.${suffix}.json`) {
2666
+ const result = await readJsonFile(filePath);
2667
+ if (result.error) {
2668
+ handleFileSystemError("read", result.error);
2669
+ continue;
2670
+ }
2671
+ groups = result.data;
2672
+ } else if (file === `${DEFAULT_TAGS_FILENAME}.json` || file === `${DEFAULT_TAGS_FILENAME}.${suffix}.json`) {
2673
+ const result = await readJsonFile(filePath);
2674
+ if (result.error) {
2675
+ handleFileSystemError("read", result.error);
2676
+ continue;
2677
+ }
2678
+ internalTags = result.data;
2679
+ } else if (file.endsWith(`.${DEFAULT_PRESETS_FILENAME}.json`) || file.endsWith(`.${DEFAULT_PRESETS_FILENAME}.${suffix}.json`)) {
2680
+ const result = await readJsonFile(filePath);
2681
+ if (result.error) {
2682
+ handleFileSystemError("read", result.error);
2683
+ continue;
2684
+ }
2685
+ presets.push(...result.data);
2686
+ } else if (file.endsWith(".json") || file.endsWith(`${suffix}.json`)) {
2687
+ if (file === `${DEFAULT_COMPONENTS_FILENAME}.json` || file === `${DEFAULT_COMPONENTS_FILENAME}.${suffix}.json`) {
2688
+ continue;
2689
+ }
2690
+ const result = await readJsonFile(filePath);
2691
+ if (result.error) {
2692
+ handleFileSystemError("read", result.error);
2693
+ continue;
2694
+ }
2695
+ components.push(...result.data);
2696
+ }
2697
+ }
2698
+ return {
2699
+ components,
2700
+ groups,
2701
+ presets,
2702
+ internalTags
2703
+ };
2704
+ }
2705
+ async function readConsolidatedFiles$1(resolvedPath, suffix) {
2706
+ const componentsPath = join(resolvedPath, suffix ? `${DEFAULT_COMPONENTS_FILENAME}.${suffix}.json` : `${DEFAULT_COMPONENTS_FILENAME}.json`);
2707
+ const componentsResult = await readJsonFile(componentsPath);
2708
+ if (componentsResult.error || !componentsResult.data.length) {
2706
2709
  throw new FileSystemError(
2707
2710
  "file_not_found",
2708
2711
  "read",
2709
- new Error("No component data found"),
2710
- `No components found in ${resolvedPath}. Please make sure you have pulled the components first.`
2712
+ componentsResult.error || new Error("Components file is empty"),
2713
+ `No components found in ${componentsPath}. Please make sure you have pulled the components first.`
2711
2714
  );
2712
2715
  }
2713
- return result;
2714
- };
2716
+ const [groupsResult, presetsResult, tagsResult] = await Promise.all([
2717
+ readJsonFile(join(resolvedPath, suffix ? `${DEFAULT_GROUPS_FILENAME$1}.${suffix}.json` : `${DEFAULT_GROUPS_FILENAME$1}.json`)),
2718
+ readJsonFile(join(resolvedPath, suffix ? `${DEFAULT_PRESETS_FILENAME}.${suffix}.json` : `${DEFAULT_PRESETS_FILENAME}.json`)),
2719
+ readJsonFile(join(resolvedPath, suffix ? `${DEFAULT_TAGS_FILENAME}.${suffix}.json` : `${DEFAULT_TAGS_FILENAME}.json`))
2720
+ ]);
2721
+ return {
2722
+ components: componentsResult.data,
2723
+ groups: groupsResult.data,
2724
+ presets: presetsResult.data,
2725
+ internalTags: tagsResult.data
2726
+ };
2727
+ }
2715
2728
 
2716
2729
  const pullCmd$4 = componentsCommand.command("pull [componentName]").option("-f, --filename <filename>", "custom name to be used in file(s) name instead of space id").option("--sf, --separate-files", "Argument to create a single file for each component").option("--su, --suffix <suffix>", "suffix to add to the file name (e.g. components.<suffix>.json)").option("-s, --space <space>", "space ID").description(`Download your space's components schema as json. Optionally specify a component name to pull a single component.`);
2717
2730
  pullCmd$4.action(async (componentName, options, command) => {
@@ -3742,7 +3755,7 @@ async function pushWithDependencyGraph(space, spaceState, maxConcurrency = getAc
3742
3755
  return results;
3743
3756
  }
3744
3757
 
3745
- const pushCmd$3 = componentsCommand.command("push [componentName]").description(`Push your space's components schema as json`).option("-f, --from <from>", "source space id").option("--fi, --filter <filter>", "glob filter to apply to the components before pushing").option("--sf, --separate-files", "Read from separate files instead of consolidated files", false).option("--su, --suffix <suffix>", "Load only files matching *.<suffix>.json (e.g. components.dev.json)").option("-s, --space <space>", "space ID");
3758
+ const pushCmd$3 = componentsCommand.command("push [componentName]").description(`Push your space's components schema as json`).option("-f, --from <from>", "source space id").option("--fi, --filter <filter>", "glob filter to apply to the components before pushing").option("--sf, --separate-files", "Read from separate files instead of consolidated files", false).option("--su, --suffix <suffix>", "Suffix to add to the component name").option("-s, --space <space>", "space ID");
3746
3759
  pushCmd$3.action(async (componentName, options, command) => {
3747
3760
  const ui = getUI();
3748
3761
  const logger = getLogger();
@@ -3959,8 +3972,8 @@ const saveLanguagesToFile = async (space, internationalizationOptions, options)
3959
3972
  }
3960
3973
  };
3961
3974
 
3962
- const program$9 = getProgram();
3963
- const languagesCommand = program$9.command(commands.LANGUAGES).alias("lang").description(`Manage your space's languages`);
3975
+ const program$a = getProgram();
3976
+ const languagesCommand = program$a.command(commands.LANGUAGES).alias("lang").description(`Manage your space's languages`);
3964
3977
  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");
3965
3978
  pullCmd$3.action(async (options, command) => {
3966
3979
  konsola.title(`${commands.LANGUAGES}`, colorPalette.LANGUAGES);
@@ -4006,8 +4019,8 @@ pullCmd$3.action(async (options, command) => {
4006
4019
  konsola.br();
4007
4020
  });
4008
4021
 
4009
- const program$8 = getProgram();
4010
- const migrationsCommand = program$8.command(commands.MIGRATIONS).alias("mig").description(`Manage your space's migrations`);
4022
+ const program$9 = getProgram();
4023
+ const migrationsCommand = program$9.command(commands.MIGRATIONS).alias("mig").description(`Manage your space's migrations`);
4011
4024
 
4012
4025
  const getMigrationTemplate = () => {
4013
4026
  return `export default function (block) {
@@ -4174,84 +4187,40 @@ const updateStory = async (spaceId, storyId, payload) => {
4174
4187
  handleAPIError("update_story", error);
4175
4188
  }
4176
4189
  };
4177
- const PREFETCH_CHUNK_SIZE = 100;
4178
- const PREFETCH_PER_PAGE = 100;
4179
- const addRef = (result, story) => {
4180
- const ref = { id: story.id, uuid: story.uuid, is_folder: story.is_folder };
4181
- if (story.full_slug) {
4182
- const key = normalizeFullSlug(story.full_slug);
4183
- const existing = result.bySlug.get(key);
4184
- if (existing) {
4185
- if (!existing.some((r) => r.id === ref.id)) {
4186
- existing.push(ref);
4187
- }
4188
- } else {
4189
- result.bySlug.set(key, [ref]);
4190
- }
4191
- }
4192
- result.byId.set(story.id, ref);
4193
- };
4194
- const fetchChunkAllPages = async (spaceId, params, onPageStories) => {
4195
- let page = 1;
4196
- while (true) {
4197
- const response = await fetchStories(spaceId, { ...params, page, per_page: PREFETCH_PER_PAGE });
4198
- if (!response) {
4199
- return;
4200
- }
4201
- onPageStories(response.stories);
4202
- const total = Number(response.headers.get("Total"));
4203
- const perPage = Number(response.headers.get("Per-Page")) || PREFETCH_PER_PAGE;
4204
- if (!Number.isFinite(total) || total <= page * perPage) {
4205
- return;
4206
- }
4207
- page++;
4208
- }
4209
- };
4210
- const prefetchTargetStoriesByKeys = async (spaceId, keys, options) => {
4190
+ const prefetchTargetStories = async (spaceId, options) => {
4211
4191
  const result = {
4212
4192
  bySlug: /* @__PURE__ */ new Map(),
4213
4193
  byId: /* @__PURE__ */ new Map()
4214
4194
  };
4215
- const slugSet = /* @__PURE__ */ new Set();
4216
- for (const slug of keys.slugs) {
4217
- if (slug) {
4218
- slugSet.add(normalizeFullSlug(slug));
4219
- }
4220
- }
4221
- const idSet = /* @__PURE__ */ new Set();
4222
- for (const id of keys.ids) {
4223
- if (typeof id === "number" && Number.isFinite(id)) {
4224
- idSet.add(id);
4195
+ let page = 1;
4196
+ let totalPages = 1;
4197
+ while (page <= totalPages) {
4198
+ const response = await fetchStories(spaceId, { page, per_page: 100 });
4199
+ if (!response) {
4200
+ break;
4225
4201
  }
4226
- }
4227
- options?.onTotal?.(slugSet.size + idSet.size);
4228
- if (slugSet.size === 0 && idSet.size === 0) {
4229
- return result;
4230
- }
4231
- const slugChunks = chunk(slugSet, PREFETCH_CHUNK_SIZE);
4232
- const idChunks = chunk(idSet, PREFETCH_CHUNK_SIZE);
4233
- const requests = [];
4234
- for (const slugs of slugChunks) {
4235
- requests.push((async () => {
4236
- await fetchChunkAllPages(spaceId, { by_slugs: slugs.join(",") }, (stories) => {
4237
- for (const story of stories) {
4238
- addRef(result, story);
4239
- }
4240
- });
4241
- options?.onIncrement?.(slugs.length);
4242
- })());
4243
- }
4244
- for (const ids of idChunks) {
4245
- requests.push((async () => {
4246
- await fetchChunkAllPages(spaceId, { by_ids: ids.join(",") }, (stories) => {
4247
- for (const story of stories) {
4248
- addRef(result, story);
4202
+ const total = Number(response.headers.get("Total"));
4203
+ const perPage = Number(response.headers.get("Per-Page"));
4204
+ totalPages = Math.ceil(total / perPage);
4205
+ if (page === 1) {
4206
+ options?.onTotal?.(total);
4207
+ }
4208
+ for (const story of response.stories) {
4209
+ const ref = { id: story.id, uuid: story.uuid, is_folder: story.is_folder };
4210
+ if (story.full_slug) {
4211
+ const key = normalizeFullSlug(story.full_slug);
4212
+ const existing = result.bySlug.get(key);
4213
+ if (existing) {
4214
+ existing.push(ref);
4215
+ } else {
4216
+ result.bySlug.set(key, [ref]);
4249
4217
  }
4250
- });
4251
- options?.onIncrement?.(ids.length);
4252
- })());
4218
+ }
4219
+ result.byId.set(story.id, ref);
4220
+ }
4221
+ options?.onIncrement?.(response.stories.length);
4222
+ page++;
4253
4223
  }
4254
- await Promise.all(requests);
4255
4224
  return result;
4256
4225
  };
4257
4226
 
@@ -4708,26 +4677,45 @@ const isStoryPublishedWithoutChanges = (story) => {
4708
4677
  const isStoryWithUnpublishedChanges = (story) => {
4709
4678
  return story.published && story.unpublished_changes;
4710
4679
  };
4680
+ const toComponent = (maybeComponent) => {
4681
+ if (maybeComponent.component_group_uuid === void 0) {
4682
+ return null;
4683
+ }
4684
+ return maybeComponent;
4685
+ };
4711
4686
  const findComponentSchemas = async (directoryPath) => {
4712
- try {
4713
- const { components } = await loadComponents(directoryPath);
4714
- const schemas = {};
4715
- for (const component of components) {
4716
- schemas[component.name] = component.schema;
4717
- }
4718
- return schemas;
4719
- } catch (error) {
4687
+ const files = await readdir(directoryPath).catch((error) => {
4720
4688
  if (error.code === "ENOENT") {
4721
- return {};
4722
- }
4723
- if (error instanceof FileSystemError) {
4724
- error.message = `Failed to load component schemas for content validation.
4725
-
4726
- ${error.message}`;
4727
- throw error;
4689
+ return [];
4728
4690
  }
4729
4691
  throw error;
4692
+ });
4693
+ const fileContents = files.filter((f) => extname(f) === ".json").map((f) => {
4694
+ const filePath = join(directoryPath, f);
4695
+ const fileContent = readFileSync(filePath, "utf-8");
4696
+ return JSON.parse(fileContent);
4697
+ });
4698
+ const components = [];
4699
+ for (const content of fileContents) {
4700
+ if (Array.isArray(content)) {
4701
+ for (const maybeComponent of content) {
4702
+ const component2 = toComponent(maybeComponent);
4703
+ if (component2) {
4704
+ components.push(component2);
4705
+ }
4706
+ }
4707
+ continue;
4708
+ }
4709
+ const component = toComponent(content);
4710
+ if (component) {
4711
+ components.push(component);
4712
+ }
4730
4713
  }
4714
+ const schemas = {};
4715
+ for (const component of components) {
4716
+ schemas[component.name] = component.schema;
4717
+ }
4718
+ return schemas;
4731
4719
  };
4732
4720
  const getStoryFilename = (story) => {
4733
4721
  return `${story.slug}_${story.uuid}.json`;
@@ -5059,8 +5047,8 @@ rollbackCmd.action(async (migrationFile, _options, command) => {
5059
5047
  }
5060
5048
  });
5061
5049
 
5062
- const program$7 = getProgram();
5063
- const typesCommand = program$7.command(commands.TYPES).alias("ts").description(`Generate types d.ts for your component schemas`);
5050
+ const program$8 = getProgram();
5051
+ const typesCommand = program$8.command(commands.TYPES).alias("ts").description(`Generate types d.ts for your component schemas`);
5064
5052
 
5065
5053
  const getAssetJSONSchema = (title) => ({
5066
5054
  $id: "#/asset",
@@ -5671,7 +5659,7 @@ const getPropertyTypeAnnotation = (property, prefix, suffix) => {
5671
5659
  }
5672
5660
  };
5673
5661
  function getStoryType(property, prefix, suffix) {
5674
- return `${STORY_TYPE}<${prefix ?? ""}${capitalize(toCamelCase(property))}${suffix ?? ""}>`;
5662
+ return `${STORY_TYPE}<${prefix ?? ""}${capitalize(toCamelCase$1(property))}${suffix ?? ""}>`;
5675
5663
  }
5676
5664
  const getComponentType = (componentName, options) => {
5677
5665
  const prefix = options.typePrefix ?? "";
@@ -5961,6 +5949,8 @@ const generateStoryblokTypes = async (options = {}) => {
5961
5949
  }
5962
5950
  };
5963
5951
 
5952
+ const DEFAULT_DATASOURCES_FILENAME = "datasources";
5953
+
5964
5954
  const pushDatasource = async (spaceId, datasource) => {
5965
5955
  try {
5966
5956
  const client = getMapiClient();
@@ -6062,15 +6052,11 @@ const deleteDatasourceEntry = async (spaceId, entryId) => {
6062
6052
  handleAPIError("delete_datasource_entry", error, `Failed to delete datasource entry ${entryId}`);
6063
6053
  }
6064
6054
  };
6065
- function isDatasource(item) {
6066
- return typeof item === "object" && item !== null && "slug" in item && typeof item.slug === "string";
6067
- }
6068
6055
  const readDatasourcesFiles = async (options) => {
6069
- const { from, path, suffix } = options;
6056
+ const { from, path, separateFiles, suffix } = options;
6070
6057
  const resolvedPath = resolvePath(path, `datasources/${from}`);
6071
- let files;
6072
6058
  try {
6073
- files = await readdir(resolvedPath);
6059
+ await readdir(resolvedPath);
6074
6060
  } catch (error) {
6075
6061
  const message = `No local datasources found for space ${chalk.bold(from)}. To push datasources, you need to pull them first:
6076
6062
 
@@ -6086,53 +6072,56 @@ const readDatasourcesFiles = async (options) => {
6086
6072
  message
6087
6073
  );
6088
6074
  }
6089
- const datasourceMap = /* @__PURE__ */ new Map();
6090
- const duplicates = [];
6091
- for (const file of filterJsonBySuffix(files, suffix)) {
6092
- const { data, error } = await readJsonFile(join(resolvedPath, file));
6093
- if (error) {
6094
- handleFileSystemError("read", error);
6095
- continue;
6075
+ if (await shouldUseSeparateFiles(resolvedPath, DEFAULT_DATASOURCES_FILENAME, separateFiles, suffix)) {
6076
+ return await readSeparateFiles(resolvedPath, suffix);
6077
+ }
6078
+ return await readConsolidatedFiles(resolvedPath, suffix);
6079
+ };
6080
+ async function readSeparateFiles(resolvedPath, suffix) {
6081
+ const files = await readdir(resolvedPath);
6082
+ const datasources = [];
6083
+ const filteredFiles = files.filter((file) => {
6084
+ if (suffix) {
6085
+ return file.endsWith(`.${suffix}.json`);
6086
+ } else {
6087
+ return !/\.\w+\.json$/.test(file);
6096
6088
  }
6097
- for (const item of data) {
6098
- if (isDatasource(item)) {
6099
- const existing = datasourceMap.get(item.slug);
6100
- if (existing) {
6101
- duplicates.push(`Datasource "${item.slug}" found in both "${existing.file}" and "${file}"`);
6102
- }
6103
- datasourceMap.set(item.slug, { datasource: item, file });
6089
+ });
6090
+ for (const file of filteredFiles) {
6091
+ const filePath = join(resolvedPath, file);
6092
+ if (file.endsWith(".json") || file.endsWith(`${suffix}.json`)) {
6093
+ if (file === `${DEFAULT_DATASOURCES_FILENAME}.json` || new RegExp(`^${DEFAULT_DATASOURCES_FILENAME}\\.\\w+\\.json$`).test(file)) {
6094
+ continue;
6104
6095
  }
6096
+ const result = await readJsonFile(filePath);
6097
+ if (result.error) {
6098
+ handleFileSystemError("read", result.error);
6099
+ continue;
6100
+ }
6101
+ datasources.push(...result.data);
6105
6102
  }
6106
6103
  }
6107
- if (duplicates.length) {
6108
- throw new FileSystemError(
6109
- "file_not_found",
6110
- "read",
6111
- new Error("Duplicate datasources detected"),
6112
- `Duplicate datasources found in ${resolvedPath}:
6113
-
6114
- ${duplicates.join("\n")}
6115
-
6116
- This can happen when multiple environment snapshots (e.g. datasources.json and datasources.dev.json) or mixed formats coexist in the same directory.
6117
-
6118
- To fix this, either:
6119
- - Use --suffix <env> to target a specific environment (e.g. --suffix dev)
6120
- - Clean up the directory and pull datasources again in the format you intend`
6121
- );
6122
- }
6123
- const datasources = [...datasourceMap.values()].map(({ datasource }) => datasource);
6124
- if (!datasources.length) {
6104
+ return {
6105
+ datasources
6106
+ };
6107
+ }
6108
+ async function readConsolidatedFiles(resolvedPath, suffix) {
6109
+ const datasourcesPath = join(resolvedPath, suffix ? `${DEFAULT_DATASOURCES_FILENAME}.${suffix}.json` : `${DEFAULT_DATASOURCES_FILENAME}.json`);
6110
+ const datasourcesResult = await readJsonFile(datasourcesPath);
6111
+ if (datasourcesResult.error || !datasourcesResult.data.length) {
6125
6112
  throw new FileSystemError(
6126
6113
  "file_not_found",
6127
6114
  "read",
6128
- new Error("No datasource data found"),
6129
- `No datasources found in ${resolvedPath}. Please make sure you have pulled the datasources first.`
6115
+ datasourcesResult.error || new Error("Datasources file is empty"),
6116
+ `No datasources found in ${datasourcesPath}. Please make sure you have pulled the datasources first.`
6130
6117
  );
6131
6118
  }
6132
- return { datasources };
6133
- };
6134
-
6135
- const generateCmd = typesCommand.command("generate").description("Generate types d.ts for your component schemas").option(
6119
+ return {
6120
+ datasources: datasourcesResult.data
6121
+ };
6122
+ }
6123
+
6124
+ const generateCmd = typesCommand.command("generate").description("Generate types d.ts for your component schemas").option(
6136
6125
  "--filename <name>",
6137
6126
  "Base file name for all component types when generating a single declarations file (e.g. components.d.ts). Ignored when using --separate-files."
6138
6127
  ).option("--sf, --separate-files", "Generate one .d.ts file per component instead of a single combined file").option("--strict", "strict mode, no loose typing").option("--type-prefix <prefix>", "prefix to be prepended to all generated component type names").option("--type-suffix <suffix>", "suffix to be appended to all generated component type names").option("--suffix <suffix>", "Components suffix").option("--custom-fields-parser <path>", "Path to the parser file for Custom Field Types").option("--compiler-options <options>", "path to the compiler options from json-schema-to-typescript").option("-s, --space <space>", "space ID");
@@ -6195,10 +6184,8 @@ generateCmd.action(async (options, command) => {
6195
6184
  }
6196
6185
  });
6197
6186
 
6198
- const program$6 = getProgram();
6199
- const datasourcesCommand = program$6.command(commands.DATASOURCES).alias("ds").description(`Manage your space's datasources`);
6200
-
6201
- const DEFAULT_DATASOURCES_FILENAME = "datasources";
6187
+ const program$7 = getProgram();
6188
+ const datasourcesCommand = program$7.command(commands.DATASOURCES).alias("ds").description(`Manage your space's datasources`);
6202
6189
 
6203
6190
  const fetchDatasourceEntries = async (spaceId, datasourceId) => {
6204
6191
  try {
@@ -6368,7 +6355,7 @@ pullCmd$2.action(async (datasourceName, options, command) => {
6368
6355
  }
6369
6356
  });
6370
6357
 
6371
- const pushCmd$2 = datasourcesCommand.command("push [datasourceName]").description(`Push your space's datasources schema as json`).option("-f, --from <from>", "source space id").option("--fi, --filter <filter>", "glob filter to apply to the datasources before pushing").option("--sf, --separate-files", "Read from separate files instead of consolidated files").option("--su, --suffix <suffix>", "Load only files matching *.<suffix>.json (e.g. datasources.dev.json)").option("-s, --space <space>", "space ID");
6358
+ const pushCmd$2 = datasourcesCommand.command("push [datasourceName]").description(`Push your space's datasources schema as json`).option("-f, --from <from>", "source space id").option("--fi, --filter <filter>", "glob filter to apply to the datasources before pushing").option("--sf, --separate-files", "Read from separate files instead of consolidated files").option("--su, --suffix <suffix>", "Suffix to add to the datasource name").option("-s, --space <space>", "space ID");
6372
6359
  pushCmd$2.action(async (datasourceName, options, command) => {
6373
6360
  konsola.title(`${commands.DATASOURCES}`, colorPalette.DATASOURCES, datasourceName ? `Pushing datasource ${datasourceName}...` : "Pushing datasources...");
6374
6361
  const { space, path, verbose } = command.optsWithGlobals();
@@ -6749,13 +6736,13 @@ async function promptForLogin(verbose) {
6749
6736
  return null;
6750
6737
  }
6751
6738
  }
6752
- const program$5 = getProgram();
6753
- 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(
6739
+ const program$6 = getProgram();
6740
+ 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(
6754
6741
  "-r, --region <region>",
6755
6742
  `The region to apply to the generated project template (does not affect space creation).`
6756
6743
  ).action(async (projectPath, options) => {
6757
6744
  konsola.title(`${commands.CREATE}`, colorPalette.CREATE);
6758
- const verbose = program$5.opts().verbose;
6745
+ const verbose = program$6.opts().verbose;
6759
6746
  const { template, blueprint, token } = options;
6760
6747
  if (options.region && !isRegion(options.region)) {
6761
6748
  handleError(new CommandError(`The provided region: ${options.region} is not valid. Please use one of the following values: ${Object.values(regions).join(" | ")}`));
@@ -6974,8 +6961,8 @@ program$5.command(`${commands.CREATE} [project-path]`).alias("c").description(`S
6974
6961
  konsola.br();
6975
6962
  });
6976
6963
 
6977
- const program$4 = getProgram();
6978
- const logsCommand = program$4.command(commands.LOGS).alias("lg").description(`Inspect and manage logs.`);
6964
+ const program$5 = getProgram();
6965
+ const logsCommand = program$5.command(commands.LOGS).alias("lg").description(`Inspect and manage logs.`);
6979
6966
 
6980
6967
  const listCmd$1 = logsCommand.command("list").description("List logs").option("-s, --space <space>", "space ID");
6981
6968
  listCmd$1.action(async (_options, command) => {
@@ -7000,8 +6987,8 @@ pruneCmd$1.action(async (options, command) => {
7000
6987
  ui.info(`Deleted ${deletedFilesCount} log file${deletedFilesCount === 1 ? "" : "s"}`);
7001
6988
  });
7002
6989
 
7003
- const program$3 = getProgram();
7004
- const reportsCommand = program$3.command(commands.REPORTS).alias("rp").description("Inspect and manage reports.");
6990
+ const program$4 = getProgram();
6991
+ const reportsCommand = program$4.command(commands.REPORTS).alias("rp").description("Inspect and manage reports.");
7005
6992
 
7006
6993
  const listCmd = reportsCommand.command("list").description("List reports").option("-s, --space <space>", "space ID");
7007
6994
  listCmd.action(async (_options, command) => {
@@ -7026,8 +7013,8 @@ pruneCmd.action(async (options, command) => {
7026
7013
  ui.info(`Deleted ${deletedFilesCount} report file${deletedFilesCount === 1 ? "" : "s"}`);
7027
7014
  });
7028
7015
 
7029
- const program$2 = getProgram();
7030
- const assetsCommand = program$2.command(commands.ASSETS).description(`Manage your space's assets`);
7016
+ const program$3 = getProgram();
7017
+ const assetsCommand = program$3.command(commands.ASSETS).description(`Manage your space's assets`);
7031
7018
 
7032
7019
  const fetchAssets = async ({ spaceId, params }) => {
7033
7020
  try {
@@ -7217,7 +7204,7 @@ const isRemoteSource = (assetBinaryPath) => {
7217
7204
  return false;
7218
7205
  }
7219
7206
  };
7220
- const isValidManifestEntry = (entry) => Boolean(typeof entry.old_id === "number" && typeof entry.new_id === "number" && entry.old_filename && entry.new_filename);
7207
+ const isValidManifestEntry = (entry) => Boolean(typeof entry.old_id === "number" && typeof entry.new_id === "number" && entry.new_filename);
7221
7208
  const loadAssetMap = async (manifestFile) => {
7222
7209
  const manifest = await loadManifest(manifestFile);
7223
7210
  const entries = manifest.filter(isValidManifestEntry).map((e) => [
@@ -7918,8 +7905,8 @@ const traverseAndMapBySchema = (data, {
7918
7905
  const dataNew = { ...data };
7919
7906
  for (const [fieldName, fieldValue] of Object.entries(data)) {
7920
7907
  const fieldSchema = schema[fieldName.replace(/__i18n__.*/, "")];
7921
- const fieldType = fieldSchema && typeof fieldSchema === "object" && "type" in fieldSchema && fieldSchema.type;
7922
- const fieldRefMapper = typeof fieldType === "string" && fieldRefMappers2[fieldType];
7908
+ const fieldType = fieldSchema && typeof fieldSchema === "object" && "type" in fieldSchema ? fieldSchema.type : void 0;
7909
+ const fieldRefMapper = typeof fieldType === "string" ? fieldRefMappers2[fieldType] : void 0;
7923
7910
  if (fieldRefMapper) {
7924
7911
  dataNew[fieldName] = fieldRefMapper(fieldValue, {
7925
7912
  schema: fieldSchema,
@@ -7984,6 +7971,9 @@ const richtextFieldRefMapper = (data, { schemas, maps, fieldRefMappers: fieldRef
7984
7971
  fieldRefMappers: fieldRefMappers2
7985
7972
  });
7986
7973
  const multilinkFieldRefMapper = (data, { maps }) => {
7974
+ if (!data || typeof data !== "object") {
7975
+ return data;
7976
+ }
7987
7977
  if (data.linktype !== "story") {
7988
7978
  return data;
7989
7979
  }
@@ -8003,6 +7993,9 @@ const bloksFieldRefMapper = (data, { schemas, maps, fieldRefMappers: fieldRefMap
8003
7993
  }));
8004
7994
  };
8005
7995
  const assetFieldRefMapper = (data, { maps }) => {
7996
+ if (!data || typeof data !== "object") {
7997
+ return data;
7998
+ }
8006
7999
  const mappedAsset = typeof data.id === "number" ? maps.assets?.get(data.id) : void 0;
8007
8000
  if (!mappedAsset) {
8008
8001
  return data;
@@ -8020,7 +8013,7 @@ const multiassetFieldRefMapper = (data, options) => {
8020
8013
  return data.map((d) => assetFieldRefMapper(d, options));
8021
8014
  };
8022
8015
  const optionsFieldRefMapper = (data, { schema, maps }) => {
8023
- if (schema.source !== "internal_stories" || !Array.isArray(data)) {
8016
+ if (!schema || !("source" in schema) || schema.source !== "internal_stories" || !Array.isArray(data)) {
8024
8017
  return data;
8025
8018
  }
8026
8019
  return data.map((d) => maps.stories?.get(d) || d);
@@ -8937,8 +8930,8 @@ pushCmd$1.action(async (assetInput, options, command) => {
8937
8930
  }
8938
8931
  });
8939
8932
 
8940
- const program$1 = getProgram();
8941
- const storiesCommand = program$1.command(commands.STORIES).description(`Manage your space's stories`);
8933
+ const program$2 = getProgram();
8934
+ const storiesCommand = program$2.command(commands.STORIES).description(`Manage your space's stories`);
8942
8935
 
8943
8936
  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.`);
8944
8937
  pullCmd.action(async (options, command) => {
@@ -9230,6 +9223,12 @@ pushCmd.action(async (options, command) => {
9230
9223
  };
9231
9224
  visit(story.content);
9232
9225
  };
9226
+ const fetchProgress = ui.createProgressBar({ title: "Matching Stories...".padEnd(21) });
9227
+ const existingTargetStories = await prefetchTargetStories(space, {
9228
+ onTotal: (total) => fetchProgress.setTotal(total),
9229
+ onIncrement: (count) => fetchProgress.increment(count)
9230
+ });
9231
+ fetchProgress.stop();
9233
9232
  const scanProgress = ui.createProgressBar({ title: "Scanning Stories...".padEnd(21) });
9234
9233
  const storyIndex = await scanLocalStoryIndex({
9235
9234
  directoryPath: storiesDirectoryPath,
@@ -9248,24 +9247,6 @@ pushCmd.action(async (options, command) => {
9248
9247
  });
9249
9248
  const levels = groupStoriesByDepth(storyIndex);
9250
9249
  scanProgress.stop();
9251
- const localSlugs = storyIndex.map((entry) => entry.full_slug).filter(Boolean);
9252
- const localIdSet = new Set(storyIndex.map((entry) => entry.id));
9253
- const manifestIds = [];
9254
- for (const [key, value] of maps.stories.entries()) {
9255
- if (typeof key === "number" && localIdSet.has(key) && typeof value === "number") {
9256
- manifestIds.push(value);
9257
- }
9258
- }
9259
- const fetchProgress = ui.createProgressBar({ title: "Matching Stories...".padEnd(21) });
9260
- const existingTargetStories = await prefetchTargetStoriesByKeys(
9261
- space,
9262
- { slugs: localSlugs, ids: manifestIds },
9263
- {
9264
- onTotal: (total) => fetchProgress.setTotal(total),
9265
- onIncrement: (count) => fetchProgress.increment(count)
9266
- }
9267
- );
9268
- fetchProgress.stop();
9269
9250
  const creationProgress = ui.createProgressBar({ title: "Creating Stories...".padEnd(21) });
9270
9251
  const processProgress = ui.createProgressBar({ title: "Processing Stories...".padEnd(21) });
9271
9252
  const updateProgress = ui.createProgressBar({ title: "Updating Stories...".padEnd(21) });
@@ -9443,6 +9424,1957 @@ pushCmd.action(async (options, command) => {
9443
9424
  }
9444
9425
  });
9445
9426
 
9427
+ const program$1 = getProgram();
9428
+ const schemaCommand = program$1.command(commands.SCHEMA).description(`Manage your space's schema from code`);
9429
+
9430
+ function isObject(value) {
9431
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9432
+ }
9433
+ function isComponent(value) {
9434
+ return isObject(value) && typeof value.name === "string" && "schema" in value && isObject(value.schema);
9435
+ }
9436
+ function isDatasource(value) {
9437
+ return isObject(value) && typeof value.name === "string" && typeof value.slug === "string" && !("schema" in value);
9438
+ }
9439
+ function isComponentFolder(value) {
9440
+ return isObject(value) && typeof value.name === "string" && !("schema" in value) && !("slug" in value) && ("uuid" in value || "parent_id" in value);
9441
+ }
9442
+ function isSchemaObject(value) {
9443
+ return isObject(value) && ("blocks" in value || "blockFolders" in value || "datasources" in value);
9444
+ }
9445
+ function classifyExports(moduleExports) {
9446
+ const components = [];
9447
+ const componentFolders = [];
9448
+ const datasources = [];
9449
+ function collect(value) {
9450
+ if (isComponent(value)) {
9451
+ components.push(value);
9452
+ } else if (isDatasource(value)) {
9453
+ datasources.push(value);
9454
+ } else if (isComponentFolder(value)) {
9455
+ componentFolders.push(value);
9456
+ }
9457
+ }
9458
+ for (const value of Object.values(moduleExports)) {
9459
+ if (isSchemaObject(value)) {
9460
+ for (const group of Object.values(value)) {
9461
+ if (isObject(group)) {
9462
+ for (const entity of Object.values(group)) {
9463
+ collect(entity);
9464
+ }
9465
+ }
9466
+ }
9467
+ } else {
9468
+ collect(value);
9469
+ }
9470
+ }
9471
+ return { components, componentFolders, datasources };
9472
+ }
9473
+ async function loadSchema(entryPath) {
9474
+ const { createJiti } = await import('jiti');
9475
+ const jiti = createJiti(import.meta.url, {
9476
+ interopDefault: true
9477
+ });
9478
+ const absolutePath = (await import('pathe')).resolve(entryPath);
9479
+ const mod = await jiti.import(absolutePath);
9480
+ return classifyExports(mod);
9481
+ }
9482
+
9483
+ const COMPONENT_STRIP_KEYS = /* @__PURE__ */ new Set([
9484
+ "id",
9485
+ "created_at",
9486
+ "updated_at",
9487
+ "real_name",
9488
+ // API-computed display/technical name, read-only
9489
+ "preset_id",
9490
+ // Instance-level preset selection, not part of schema definition
9491
+ "all_presets",
9492
+ // Computed list of presets, managed via /presets API
9493
+ "internal_tags_list",
9494
+ // Read-only expanded form of internal_tag_ids ({id, name} objects)
9495
+ "content_type_asset_preview",
9496
+ // Read-only, not in ComponentCreate/ComponentUpdate
9497
+ "image",
9498
+ // Read-only preview image URL
9499
+ "preview_tmpl",
9500
+ // Read-only preview template
9501
+ "metadata"
9502
+ // Not in current API types, stripped defensively
9503
+ ]);
9504
+ const DATASOURCE_STRIP_KEYS = /* @__PURE__ */ new Set(["id", "created_at", "updated_at"]);
9505
+ const DATASOURCE_DIMENSION_STRIP_KEYS = /* @__PURE__ */ new Set(["id", "datasource_id", "created_at", "updated_at"]);
9506
+ const FOLDER_INIT_STRIP_KEYS = /* @__PURE__ */ new Set(["id"]);
9507
+ const FOLDER_STRIP_KEYS = /* @__PURE__ */ new Set(["id", "uuid"]);
9508
+ const DATASOURCE_DEFAULTS = {
9509
+ dimensions: []
9510
+ };
9511
+ const COMPONENT_DEFAULTS = {
9512
+ display_name: "",
9513
+ color: "",
9514
+ icon: "",
9515
+ preview_field: "",
9516
+ internal_tag_ids: []
9517
+ };
9518
+ function applyDefaults(entity, defaults) {
9519
+ const result = { ...entity };
9520
+ for (const [key, defaultValue] of Object.entries(defaults)) {
9521
+ if (result[key] === void 0 || result[key] === null) {
9522
+ Object.assign(result, { [key]: defaultValue });
9523
+ }
9524
+ }
9525
+ return result;
9526
+ }
9527
+ const INDENT = " ";
9528
+ function formatValue(value, depth) {
9529
+ const indent = INDENT.repeat(depth);
9530
+ const innerIndent = INDENT.repeat(depth + 1);
9531
+ if (value === null || value === void 0) {
9532
+ return String(value);
9533
+ }
9534
+ if (typeof value === "string") {
9535
+ return `'${value.replace(/'/g, "\\'")}'`;
9536
+ }
9537
+ if (typeof value === "number" || typeof value === "boolean") {
9538
+ return String(value);
9539
+ }
9540
+ if (Array.isArray(value)) {
9541
+ if (value.length === 0) {
9542
+ return "[]";
9543
+ }
9544
+ const items = value.map((item) => `${innerIndent}${formatValue(item, depth + 1)},`);
9545
+ return `[
9546
+ ${items.join("\n")}
9547
+ ${indent}]`;
9548
+ }
9549
+ if (typeof value === "object") {
9550
+ const entries = Object.entries(value).filter(([, v]) => v !== void 0 && v !== null).sort(([a], [b]) => a.localeCompare(b));
9551
+ if (entries.length === 0) {
9552
+ return "{}";
9553
+ }
9554
+ const props = entries.map(
9555
+ ([key, val]) => `${innerIndent}${key}: ${formatValue(val, depth + 1)},`
9556
+ );
9557
+ return `{
9558
+ ${props.join("\n")}
9559
+ ${indent}}`;
9560
+ }
9561
+ return String(value);
9562
+ }
9563
+ function fileTimestamp(iso) {
9564
+ return iso.replace(/[:.]/g, "-");
9565
+ }
9566
+ function stripKeys(obj, keysToStrip) {
9567
+ const result = {};
9568
+ for (const [key, value] of Object.entries(obj)) {
9569
+ if (!keysToStrip.has(key) && value !== void 0 && value !== null) {
9570
+ result[key] = value;
9571
+ }
9572
+ }
9573
+ return result;
9574
+ }
9575
+
9576
+ function sortSchemaByPos$1(schema) {
9577
+ const entries = Object.entries(schema).filter(([key]) => key !== "_uid" && key !== "component").sort(([, a], [, b]) => {
9578
+ const posA = typeof a.pos === "number" ? a.pos : Infinity;
9579
+ const posB = typeof b.pos === "number" ? b.pos : Infinity;
9580
+ return posA - posB;
9581
+ });
9582
+ return Object.fromEntries(
9583
+ entries.map(([key, field]) => {
9584
+ const { id, ...rest } = field;
9585
+ return [key, rest];
9586
+ })
9587
+ );
9588
+ }
9589
+ function serializeComponent(component) {
9590
+ const clean = stripKeys(component, COMPONENT_STRIP_KEYS);
9591
+ if (clean.schema && typeof clean.schema === "object") {
9592
+ clean.schema = sortSchemaByPos$1(clean.schema);
9593
+ }
9594
+ const ordered = {};
9595
+ if (clean.name !== void 0) {
9596
+ ordered.name = clean.name;
9597
+ }
9598
+ if (clean.display_name !== void 0) {
9599
+ ordered.display_name = clean.display_name;
9600
+ }
9601
+ if (clean.is_root !== void 0) {
9602
+ ordered.is_root = clean.is_root;
9603
+ }
9604
+ if (clean.is_nestable !== void 0) {
9605
+ ordered.is_nestable = clean.is_nestable;
9606
+ }
9607
+ const handled = /* @__PURE__ */ new Set(["name", "display_name", "is_root", "is_nestable", "schema"]);
9608
+ for (const [key, value] of Object.entries(clean).sort(([a], [b]) => a.localeCompare(b))) {
9609
+ if (!handled.has(key)) {
9610
+ ordered[key] = value;
9611
+ }
9612
+ }
9613
+ if (clean.schema !== void 0) {
9614
+ ordered.schema = clean.schema;
9615
+ }
9616
+ return `defineBlock(${formatValue(ordered, 0)})`;
9617
+ }
9618
+ function serializeDatasource(datasource) {
9619
+ const clean = stripKeys(datasource, DATASOURCE_STRIP_KEYS);
9620
+ if (Array.isArray(clean.dimensions)) {
9621
+ clean.dimensions = clean.dimensions.map(
9622
+ (dim) => typeof dim === "object" && dim !== null ? stripKeys(dim, DATASOURCE_DIMENSION_STRIP_KEYS) : dim
9623
+ );
9624
+ }
9625
+ const ordered = {};
9626
+ if (clean.name !== void 0) {
9627
+ ordered.name = clean.name;
9628
+ }
9629
+ if (clean.slug !== void 0) {
9630
+ ordered.slug = clean.slug;
9631
+ }
9632
+ const handled = /* @__PURE__ */ new Set(["name", "slug"]);
9633
+ for (const [key, value] of Object.entries(clean).sort(([a], [b]) => a.localeCompare(b))) {
9634
+ if (!handled.has(key)) {
9635
+ ordered[key] = value;
9636
+ }
9637
+ }
9638
+ return `defineDatasource(${formatValue(ordered, 0)})`;
9639
+ }
9640
+ function serializeComponentFolder(folder) {
9641
+ const clean = stripKeys(folder, FOLDER_STRIP_KEYS);
9642
+ const ordered = {};
9643
+ if (clean.name !== void 0) {
9644
+ ordered.name = clean.name;
9645
+ }
9646
+ const handled = /* @__PURE__ */ new Set(["name"]);
9647
+ for (const [key, value] of Object.entries(clean).sort(([a], [b]) => a.localeCompare(b))) {
9648
+ if (!handled.has(key)) {
9649
+ ordered[key] = value;
9650
+ }
9651
+ }
9652
+ return `defineBlockFolder(${formatValue(ordered, 0)})`;
9653
+ }
9654
+
9655
+ function diffEntity(type, name, localSerialized, remoteSerialized) {
9656
+ if (!remoteSerialized && localSerialized) {
9657
+ return { type, name, action: "create", diff: null, local: null, remote: null };
9658
+ }
9659
+ if (remoteSerialized && !localSerialized) {
9660
+ return { type, name, action: "stale", diff: null, local: null, remote: null };
9661
+ }
9662
+ if (localSerialized === remoteSerialized) {
9663
+ return { type, name, action: "unchanged", diff: null, local: null, remote: null };
9664
+ }
9665
+ const patch = createTwoFilesPatch(
9666
+ `remote/${name}`,
9667
+ `local/${name}`,
9668
+ remoteSerialized,
9669
+ localSerialized,
9670
+ "remote",
9671
+ "local"
9672
+ );
9673
+ return { type, name, action: "update", diff: patch, local: null, remote: null };
9674
+ }
9675
+ function diffSchema(local, remote) {
9676
+ const diffs = [];
9677
+ const processedComponentNames = /* @__PURE__ */ new Set();
9678
+ for (const comp of local.components) {
9679
+ processedComponentNames.add(comp.name);
9680
+ const remoteComp = remote.components.get(comp.name);
9681
+ const localSerialized = serializeComponent(applyDefaults(comp, COMPONENT_DEFAULTS));
9682
+ const remoteSerialized = remoteComp ? serializeComponent(applyDefaults(remoteComp, COMPONENT_DEFAULTS)) : null;
9683
+ diffs.push(diffEntity("component", comp.name, localSerialized, remoteSerialized));
9684
+ }
9685
+ for (const [name] of remote.components) {
9686
+ if (!processedComponentNames.has(name)) {
9687
+ diffs.push(diffEntity("component", name, null, "stale"));
9688
+ }
9689
+ }
9690
+ const processedFolderNames = /* @__PURE__ */ new Set();
9691
+ for (const folder of local.componentFolders) {
9692
+ processedFolderNames.add(folder.name);
9693
+ const remoteFolder = remote.componentFolders.get(folder.name);
9694
+ const localSerialized = serializeComponentFolder(folder);
9695
+ const remoteSerialized = remoteFolder ? serializeComponentFolder(remoteFolder) : null;
9696
+ diffs.push(diffEntity("componentFolder", folder.name, localSerialized, remoteSerialized));
9697
+ }
9698
+ for (const [name] of remote.componentFolders) {
9699
+ if (!processedFolderNames.has(name)) {
9700
+ diffs.push(diffEntity("componentFolder", name, null, "stale"));
9701
+ }
9702
+ }
9703
+ const processedDatasourceNames = /* @__PURE__ */ new Set();
9704
+ for (const ds of local.datasources) {
9705
+ processedDatasourceNames.add(ds.name);
9706
+ const remoteDs = remote.datasources.get(ds.name);
9707
+ const localSerialized = serializeDatasource(applyDefaults(ds, DATASOURCE_DEFAULTS));
9708
+ const remoteSerialized = remoteDs ? serializeDatasource(applyDefaults(remoteDs, DATASOURCE_DEFAULTS)) : null;
9709
+ diffs.push(diffEntity("datasource", ds.name, localSerialized, remoteSerialized));
9710
+ }
9711
+ for (const [name] of remote.datasources) {
9712
+ if (!processedDatasourceNames.has(name)) {
9713
+ diffs.push(diffEntity("datasource", name, null, "stale"));
9714
+ }
9715
+ }
9716
+ return {
9717
+ diffs,
9718
+ creates: diffs.filter((d) => d.action === "create").length,
9719
+ updates: diffs.filter((d) => d.action === "update").length,
9720
+ unchanged: diffs.filter((d) => d.action === "unchanged").length,
9721
+ stale: diffs.filter((d) => d.action === "stale").length
9722
+ };
9723
+ }
9724
+
9725
+ async function fetchRemoteSchema(spaceId) {
9726
+ const client = getMapiClient();
9727
+ const spaceIdNum = Number(spaceId);
9728
+ const [componentsRes, foldersRes, rawDatasources] = await Promise.all([
9729
+ client.components.list({ path: { space_id: spaceIdNum }, throwOnError: true }),
9730
+ client.componentFolders.list({ path: { space_id: spaceIdNum }, throwOnError: true }),
9731
+ fetchAllPages(
9732
+ (page) => client.datasources.list({ path: { space_id: spaceIdNum }, query: { page }, throwOnError: true }),
9733
+ (data) => data?.datasources ?? []
9734
+ )
9735
+ ]);
9736
+ const rawComponents = componentsRes.data?.components ?? [];
9737
+ const rawComponentFolders = foldersRes.data?.component_groups ?? [];
9738
+ const remote = {
9739
+ components: new Map(rawComponents.map((c) => [c.name, c])),
9740
+ componentFolders: new Map(rawComponentFolders.map((f) => [f.name, f])),
9741
+ datasources: new Map(rawDatasources.map((d) => [d.name, d]))
9742
+ };
9743
+ return { remote, rawComponents, rawComponentFolders, rawDatasources };
9744
+ }
9745
+
9746
+ function isRecord(value) {
9747
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9748
+ }
9749
+ function isSchemaField(value) {
9750
+ return isRecord(value) && "type" in value;
9751
+ }
9752
+ function toSchemaRecord(schema) {
9753
+ const result = {};
9754
+ for (const [key, value] of Object.entries(schema)) {
9755
+ if (key === "_uid" || key === "component" || !isSchemaField(value)) {
9756
+ continue;
9757
+ }
9758
+ result[key] = value;
9759
+ }
9760
+ return result;
9761
+ }
9762
+ function buildComponentPayload(input) {
9763
+ if (!isRecord(input)) {
9764
+ return { name: "" };
9765
+ }
9766
+ return {
9767
+ name: typeof input.name === "string" ? input.name : "",
9768
+ // Fields in COMPONENT_DEFAULTS are always sent with their reset value so that
9769
+ // removing a field from the local schema actually clears it on the API.
9770
+ // (Root-level fields are additive on MAPI update — omitting preserves the old value.)
9771
+ display_name: typeof input.display_name === "string" ? input.display_name : "",
9772
+ color: typeof input.color === "string" ? input.color : "",
9773
+ icon: typeof input.icon === "string" ? input.icon : "",
9774
+ preview_field: typeof input.preview_field === "string" ? input.preview_field : "",
9775
+ internal_tag_ids: Array.isArray(input.internal_tag_ids) ? input.internal_tag_ids : [],
9776
+ // Conditionally sent: only included when explicitly set in local schema
9777
+ ...isRecord(input.schema) && { schema: toSchemaRecord(input.schema) },
9778
+ ...typeof input.is_root === "boolean" && { is_root: input.is_root },
9779
+ ...typeof input.is_nestable === "boolean" && { is_nestable: input.is_nestable },
9780
+ ...typeof input.component_group_uuid === "string" && { component_group_uuid: input.component_group_uuid }
9781
+ };
9782
+ }
9783
+ function toComponentCreate(input) {
9784
+ return buildComponentPayload(input);
9785
+ }
9786
+ function toComponentUpdate(input) {
9787
+ return buildComponentPayload(input);
9788
+ }
9789
+ function toComponentFolderCreate(input) {
9790
+ if (!isRecord(input)) {
9791
+ return { name: "" };
9792
+ }
9793
+ return {
9794
+ name: typeof input.name === "string" ? input.name : "",
9795
+ ...typeof input.parent_id === "number" && { parent_id: input.parent_id }
9796
+ };
9797
+ }
9798
+ function toDatasourceCreate(input) {
9799
+ if (!isRecord(input)) {
9800
+ return { name: "", slug: "" };
9801
+ }
9802
+ const result = {
9803
+ name: typeof input.name === "string" ? input.name : "",
9804
+ slug: typeof input.slug === "string" ? input.slug : ""
9805
+ };
9806
+ if (Array.isArray(input.dimensions)) {
9807
+ result.dimensions_attributes = input.dimensions.filter((d) => isRecord(d) && typeof d.name === "string" && typeof d.entry_value === "string").map((d) => ({
9808
+ name: d.name,
9809
+ entry_value: d.entry_value
9810
+ }));
9811
+ }
9812
+ return result;
9813
+ }
9814
+ function toDatasourceUpdate(input, remote) {
9815
+ const base = toDatasourceCreate(input);
9816
+ const localDims = base.dimensions_attributes ?? [];
9817
+ const remoteDims = remote.dimensions ?? [];
9818
+ if (remoteDims.length === 0) {
9819
+ return base;
9820
+ }
9821
+ const localKeys = new Set(localDims.map((d) => `${d.name}::${d.entry_value}`));
9822
+ const destroyEntries = remoteDims.filter((rd) => rd.id != null && !localKeys.has(`${rd.name}::${rd.entry_value}`)).map((rd) => ({ id: rd.id, _destroy: true }));
9823
+ if (destroyEntries.length > 0) {
9824
+ return {
9825
+ ...base,
9826
+ dimensions_attributes: [...localDims, ...destroyEntries]
9827
+ };
9828
+ }
9829
+ return base;
9830
+ }
9831
+
9832
+ function formatDiffOutput(result, options) {
9833
+ const lines = [];
9834
+ const byType = {
9835
+ component: [],
9836
+ componentFolder: [],
9837
+ datasource: []
9838
+ };
9839
+ for (const diff of result.diffs) {
9840
+ byType[diff.type].push(diff);
9841
+ }
9842
+ const willDelete = options?.delete ?? false;
9843
+ const icons = {
9844
+ create: chalk.green("+"),
9845
+ update: chalk.yellow("~"),
9846
+ unchanged: chalk.dim("="),
9847
+ stale: chalk.red("-")
9848
+ };
9849
+ const sections = [
9850
+ ["Components", byType.component],
9851
+ ["Component Folders", byType.componentFolder],
9852
+ ["Datasources", byType.datasource]
9853
+ ];
9854
+ for (const [label, diffs] of sections) {
9855
+ if (diffs.length === 0) {
9856
+ continue;
9857
+ }
9858
+ lines.push(chalk.bold(label));
9859
+ for (const diff of diffs) {
9860
+ const icon = icons[diff.action] ?? " ";
9861
+ const name = diff.action === "stale" ? chalk.red(diff.name) : diff.name;
9862
+ const actionLabel = diff.action === "stale" && willDelete ? "delete" : diff.action;
9863
+ lines.push(` ${icon} ${name} ${chalk.dim(`(${actionLabel})`)}`);
9864
+ if (diff.diff) {
9865
+ for (const line of diff.diff.split("\n")) {
9866
+ if (line.startsWith("+") && !line.startsWith("+++")) {
9867
+ lines.push(` ${chalk.green(line)}`);
9868
+ } else if (line.startsWith("-") && !line.startsWith("---")) {
9869
+ lines.push(` ${chalk.red(line)}`);
9870
+ }
9871
+ }
9872
+ }
9873
+ }
9874
+ lines.push("");
9875
+ }
9876
+ const summary = [
9877
+ result.creates > 0 ? chalk.green(`${result.creates} to create`) : null,
9878
+ result.updates > 0 ? chalk.yellow(`${result.updates} to update`) : null,
9879
+ result.unchanged > 0 ? chalk.dim(`${result.unchanged} unchanged`) : null,
9880
+ result.stale > 0 ? chalk.red(`${result.stale} ${willDelete ? "to delete" : "stale"}`) : null
9881
+ ].filter(Boolean).join(", ");
9882
+ lines.push(`Summary: ${summary}`);
9883
+ return lines.join("\n");
9884
+ }
9885
+ async function executePush(spaceId, local, remote, diffResult, options) {
9886
+ const client = getMapiClient();
9887
+ const spaceIdNum = Number(spaceId);
9888
+ let created = 0;
9889
+ let updated = 0;
9890
+ let deleted = 0;
9891
+ const createdFolderUuids = /* @__PURE__ */ new Map();
9892
+ const folderDiffs = diffResult.diffs.filter((d) => d.type === "componentFolder");
9893
+ const folderResults = await Promise.allSettled(
9894
+ folderDiffs.map(async (diff) => {
9895
+ const localFolder = local.componentFolders.find((f) => f.name === diff.name);
9896
+ if (diff.action === "create" && localFolder) {
9897
+ const response = await client.componentFolders.create({
9898
+ path: { space_id: spaceIdNum },
9899
+ body: { component_group: toComponentFolderCreate(localFolder) },
9900
+ throwOnError: true
9901
+ });
9902
+ return { action: "created", uuid: response.data?.component_group?.uuid };
9903
+ }
9904
+ if (diff.action === "update" && localFolder) {
9905
+ const existing = remote.componentFolders.get(diff.name);
9906
+ if (existing?.id) {
9907
+ await client.componentFolders.update(existing.id, {
9908
+ path: { space_id: spaceIdNum },
9909
+ body: { component_group: toComponentFolderCreate(localFolder) },
9910
+ throwOnError: true
9911
+ });
9912
+ return { action: "updated" };
9913
+ }
9914
+ }
9915
+ })
9916
+ );
9917
+ for (let i = 0; i < folderResults.length; i++) {
9918
+ const result = folderResults[i];
9919
+ const diff = folderDiffs[i];
9920
+ if (result.status === "fulfilled") {
9921
+ if (result.value?.action === "created") {
9922
+ if (result.value.uuid) {
9923
+ createdFolderUuids.set(diff.name, result.value.uuid);
9924
+ }
9925
+ created++;
9926
+ } else if (result.value?.action === "updated") {
9927
+ updated++;
9928
+ }
9929
+ } else {
9930
+ const eventId = diff.action === "create" ? "push_component_group" : "update_component_group";
9931
+ handleAPIError(eventId, result.reason, `Failed to ${diff.action} component folder ${diff.name}`);
9932
+ }
9933
+ }
9934
+ const skippedComponents = /* @__PURE__ */ new Set();
9935
+ if (options.pendingFolderAssignments) {
9936
+ const logger = getLogger();
9937
+ for (const [folderName, componentNames] of options.pendingFolderAssignments) {
9938
+ const remoteUuid = createdFolderUuids.get(folderName);
9939
+ if (!remoteUuid) {
9940
+ logger.warn(`Could not resolve folder '${folderName}' \u2014 skipping components: ${componentNames.join(", ")}`);
9941
+ for (const name of componentNames) {
9942
+ skippedComponents.add(name);
9943
+ }
9944
+ continue;
9945
+ }
9946
+ for (const compName of componentNames) {
9947
+ const comp = local.components.find((c) => c.name === compName);
9948
+ if (comp) {
9949
+ comp.component_group_uuid = remoteUuid;
9950
+ }
9951
+ }
9952
+ }
9953
+ }
9954
+ const componentDiffs = diffResult.diffs.filter((d) => d.type === "component" && !skippedComponents.has(d.name));
9955
+ const componentResults = await Promise.allSettled(
9956
+ componentDiffs.map(async (diff) => {
9957
+ const localComp = local.components.find((c) => c.name === diff.name);
9958
+ if (diff.action === "create" && localComp) {
9959
+ await client.components.create({
9960
+ path: { space_id: spaceIdNum },
9961
+ body: { component: toComponentCreate(localComp) },
9962
+ throwOnError: true
9963
+ });
9964
+ return "created";
9965
+ }
9966
+ if (diff.action === "update" && localComp) {
9967
+ const existing = remote.components.get(diff.name);
9968
+ if (existing?.id) {
9969
+ await client.components.update(existing.id, {
9970
+ path: { space_id: spaceIdNum },
9971
+ body: { component: toComponentUpdate(localComp) },
9972
+ throwOnError: true
9973
+ });
9974
+ return "updated";
9975
+ }
9976
+ }
9977
+ })
9978
+ );
9979
+ for (let i = 0; i < componentResults.length; i++) {
9980
+ const result = componentResults[i];
9981
+ const diff = componentDiffs[i];
9982
+ if (result.status === "fulfilled") {
9983
+ if (result.value === "created") {
9984
+ created++;
9985
+ } else if (result.value === "updated") {
9986
+ updated++;
9987
+ }
9988
+ } else {
9989
+ const eventId = diff.action === "create" ? "push_component" : "update_component";
9990
+ handleAPIError(eventId, result.reason, `Failed to ${diff.action} component ${diff.name}`);
9991
+ }
9992
+ }
9993
+ const datasourceDiffs = diffResult.diffs.filter((d) => d.type === "datasource");
9994
+ const datasourceResults = await Promise.allSettled(
9995
+ datasourceDiffs.map(async (diff) => {
9996
+ const localDs = local.datasources.find((d) => d.name === diff.name);
9997
+ if (diff.action === "create" && localDs) {
9998
+ await client.datasources.create({
9999
+ path: { space_id: spaceIdNum },
10000
+ body: { datasource: toDatasourceCreate(localDs) },
10001
+ throwOnError: true
10002
+ });
10003
+ return "created";
10004
+ }
10005
+ if (diff.action === "update" && localDs) {
10006
+ const existing = remote.datasources.get(diff.name);
10007
+ if (existing?.id) {
10008
+ await client.datasources.update(existing.id, {
10009
+ path: { space_id: spaceIdNum },
10010
+ body: { datasource: toDatasourceUpdate(localDs, existing) },
10011
+ throwOnError: true
10012
+ });
10013
+ return "updated";
10014
+ }
10015
+ }
10016
+ })
10017
+ );
10018
+ for (let i = 0; i < datasourceResults.length; i++) {
10019
+ const result = datasourceResults[i];
10020
+ const diff = datasourceDiffs[i];
10021
+ if (result.status === "fulfilled") {
10022
+ if (result.value === "created") {
10023
+ created++;
10024
+ } else if (result.value === "updated") {
10025
+ updated++;
10026
+ }
10027
+ } else {
10028
+ const eventId = diff.action === "create" ? "push_datasource" : "update_datasource";
10029
+ handleAPIError(eventId, result.reason, `Failed to ${diff.action} datasource ${diff.name}`);
10030
+ }
10031
+ }
10032
+ if (options.delete) {
10033
+ const staleComponents = diffResult.diffs.filter((d) => d.type === "component" && d.action === "stale");
10034
+ const deleteComponentResults = await Promise.allSettled(
10035
+ staleComponents.map(async (diff) => {
10036
+ const existing = remote.components.get(diff.name);
10037
+ if (existing?.id) {
10038
+ await client.components.delete(existing.id, {
10039
+ path: { space_id: spaceIdNum },
10040
+ throwOnError: true
10041
+ });
10042
+ return true;
10043
+ }
10044
+ })
10045
+ );
10046
+ for (let i = 0; i < deleteComponentResults.length; i++) {
10047
+ const result = deleteComponentResults[i];
10048
+ if (result.status === "fulfilled") {
10049
+ if (result.value) {
10050
+ deleted++;
10051
+ }
10052
+ } else {
10053
+ handleAPIError("push_component", result.reason, `Failed to delete component ${staleComponents[i].name}`);
10054
+ }
10055
+ }
10056
+ const staleDatasources = diffResult.diffs.filter((d) => d.type === "datasource" && d.action === "stale");
10057
+ const deleteDatasourceResults = await Promise.allSettled(
10058
+ staleDatasources.map(async (diff) => {
10059
+ const existing = remote.datasources.get(diff.name);
10060
+ if (existing?.id) {
10061
+ await client.datasources.delete(existing.id, {
10062
+ path: { space_id: spaceIdNum },
10063
+ throwOnError: true
10064
+ });
10065
+ return true;
10066
+ }
10067
+ })
10068
+ );
10069
+ for (let i = 0; i < deleteDatasourceResults.length; i++) {
10070
+ const result = deleteDatasourceResults[i];
10071
+ if (result.status === "fulfilled") {
10072
+ if (result.value) {
10073
+ deleted++;
10074
+ }
10075
+ } else {
10076
+ handleAPIError("delete_datasource", result.reason, `Failed to delete datasource ${staleDatasources[i].name}`);
10077
+ }
10078
+ }
10079
+ const staleFolders = diffResult.diffs.filter((d) => d.type === "componentFolder" && d.action === "stale");
10080
+ const deleteFolderResults = await Promise.allSettled(
10081
+ staleFolders.map(async (diff) => {
10082
+ const existing = remote.componentFolders.get(diff.name);
10083
+ if (existing?.id) {
10084
+ await client.componentFolders.delete(existing.id, {
10085
+ path: { space_id: spaceIdNum },
10086
+ throwOnError: true
10087
+ });
10088
+ return true;
10089
+ }
10090
+ })
10091
+ );
10092
+ for (let i = 0; i < deleteFolderResults.length; i++) {
10093
+ const result = deleteFolderResults[i];
10094
+ if (result.status === "fulfilled") {
10095
+ if (result.value) {
10096
+ deleted++;
10097
+ }
10098
+ } else {
10099
+ handleAPIError("push_component_group", result.reason, `Failed to delete folder ${staleFolders[i].name}`);
10100
+ }
10101
+ }
10102
+ }
10103
+ return { created, updated, deleted };
10104
+ }
10105
+ function buildChangesetEntries(diffResult, local, remote, options) {
10106
+ const changes = [];
10107
+ for (const diff of diffResult.diffs) {
10108
+ if (diff.action === "unchanged") {
10109
+ continue;
10110
+ }
10111
+ if (diff.action === "stale" && !options.delete) {
10112
+ continue;
10113
+ }
10114
+ const action = diff.action === "stale" ? "delete" : diff.action;
10115
+ let remoteSrc;
10116
+ let localSrc;
10117
+ if (diff.type === "component") {
10118
+ remoteSrc = remote.components.get(diff.name);
10119
+ localSrc = local.components.find((c) => c.name === diff.name);
10120
+ } else if (diff.type === "componentFolder") {
10121
+ remoteSrc = remote.componentFolders.get(diff.name);
10122
+ localSrc = local.componentFolders.find((f) => f.name === diff.name);
10123
+ } else if (diff.type === "datasource") {
10124
+ remoteSrc = remote.datasources.get(diff.name);
10125
+ localSrc = local.datasources.find((d) => d.name === diff.name);
10126
+ }
10127
+ changes.push({
10128
+ type: diff.type,
10129
+ name: diff.name,
10130
+ action,
10131
+ ...remoteSrc && { before: { ...remoteSrc } },
10132
+ ...localSrc && { after: { ...localSrc } }
10133
+ });
10134
+ }
10135
+ return changes;
10136
+ }
10137
+
10138
+ function findRemoteFolderByUuid(remoteFolders, uuid) {
10139
+ for (const folder of remoteFolders.values()) {
10140
+ if (folder.uuid === uuid) {
10141
+ return folder;
10142
+ }
10143
+ }
10144
+ return void 0;
10145
+ }
10146
+ function resolveFolderReferences(local, remote) {
10147
+ const localUuidToName = /* @__PURE__ */ new Map();
10148
+ for (const folder of local.componentFolders) {
10149
+ if (folder.uuid) {
10150
+ localUuidToName.set(folder.uuid, folder.name);
10151
+ }
10152
+ }
10153
+ const pendingFolderAssignments = /* @__PURE__ */ new Map();
10154
+ const resolvedComponents = local.components.map((comp) => {
10155
+ if (!comp.component_group_uuid) {
10156
+ return comp;
10157
+ }
10158
+ const folderName = localUuidToName.get(comp.component_group_uuid);
10159
+ if (!folderName) {
10160
+ return comp;
10161
+ }
10162
+ const remoteByUuid = findRemoteFolderByUuid(remote.componentFolders, comp.component_group_uuid);
10163
+ if (remoteByUuid) {
10164
+ return comp;
10165
+ }
10166
+ const remoteByName = remote.componentFolders.get(folderName);
10167
+ if (remoteByName) {
10168
+ return { ...comp, component_group_uuid: remoteByName.uuid };
10169
+ }
10170
+ const pending = pendingFolderAssignments.get(folderName) ?? [];
10171
+ pending.push(comp.name);
10172
+ pendingFolderAssignments.set(folderName, pending);
10173
+ return comp;
10174
+ });
10175
+ return {
10176
+ resolved: { ...local, components: resolvedComponents },
10177
+ pendingFolderAssignments
10178
+ };
10179
+ }
10180
+
10181
+ async function ensureDir(dir) {
10182
+ await mkdir(dir, { recursive: true });
10183
+ }
10184
+ async function saveChangeset(basePath, data) {
10185
+ const dir = join(basePath, "schema", "changesets");
10186
+ await ensureDir(dir);
10187
+ const fileName = `${fileTimestamp(data.timestamp)}.json`;
10188
+ const filePath = join(dir, fileName);
10189
+ await writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
10190
+ return filePath;
10191
+ }
10192
+
10193
+ const SENTINEL_FIELDS = /* @__PURE__ */ new Set(["_uid", "component"]);
10194
+ function classifyFieldChanges(remoteSchema, localSchema) {
10195
+ const removed = [];
10196
+ const added = [];
10197
+ const typeChanged = [];
10198
+ const requiredAdded = [];
10199
+ const requiredChanged = [];
10200
+ for (const [field, remoteField] of Object.entries(remoteSchema)) {
10201
+ if (SENTINEL_FIELDS.has(field)) {
10202
+ continue;
10203
+ }
10204
+ if (typeof remoteField.type !== "string") {
10205
+ continue;
10206
+ }
10207
+ if (!(field in localSchema)) {
10208
+ removed.push({ field, type: remoteField.type });
10209
+ }
10210
+ }
10211
+ for (const [field, localField] of Object.entries(localSchema)) {
10212
+ if (SENTINEL_FIELDS.has(field)) {
10213
+ continue;
10214
+ }
10215
+ if (typeof localField.type !== "string") {
10216
+ continue;
10217
+ }
10218
+ if (!(field in remoteSchema)) {
10219
+ if (localField.required) {
10220
+ requiredAdded.push({ field, type: localField.type });
10221
+ } else {
10222
+ added.push({ field, type: localField.type, required: false });
10223
+ }
10224
+ } else {
10225
+ const remoteField = remoteSchema[field];
10226
+ if (typeof remoteField?.type !== "string") {
10227
+ continue;
10228
+ }
10229
+ if (remoteField.type !== localField.type) {
10230
+ typeChanged.push({ field, oldType: remoteField.type, newType: localField.type });
10231
+ }
10232
+ if (localField.required && !remoteField.required) {
10233
+ requiredChanged.push({ field, type: localField.type });
10234
+ }
10235
+ }
10236
+ }
10237
+ return { removed, added, typeChanged, requiredAdded, requiredChanged };
10238
+ }
10239
+ function longestCommonSubstring(a, b) {
10240
+ let maxLen = 0;
10241
+ for (let i = 0; i < a.length; i++) {
10242
+ for (let j = 0; j < b.length; j++) {
10243
+ let len = 0;
10244
+ while (i + len < a.length && j + len < b.length && a[i + len] === b[j + len]) {
10245
+ len++;
10246
+ }
10247
+ if (len > maxLen) {
10248
+ maxLen = len;
10249
+ }
10250
+ }
10251
+ }
10252
+ return maxLen;
10253
+ }
10254
+ function nameSimilarity(a, b) {
10255
+ const longer = Math.max(a.length, b.length);
10256
+ if (longer === 0) {
10257
+ return 1;
10258
+ }
10259
+ return longestCommonSubstring(a, b) / longer;
10260
+ }
10261
+ function detectRenames(removed, added) {
10262
+ const renames = [];
10263
+ const usedRemoved = /* @__PURE__ */ new Set();
10264
+ const usedAdded = /* @__PURE__ */ new Set();
10265
+ const addedByType = /* @__PURE__ */ new Map();
10266
+ for (const addedField of added) {
10267
+ if (!addedByType.has(addedField.type)) {
10268
+ addedByType.set(addedField.type, []);
10269
+ }
10270
+ addedByType.get(addedField.type).push(addedField);
10271
+ }
10272
+ const isSinglePair = removed.length === 1 && added.length === 1;
10273
+ for (const removedField of removed) {
10274
+ const candidates = addedByType.get(removedField.type) ?? [];
10275
+ const availableCandidates = candidates.filter((c) => !usedAdded.has(c.field));
10276
+ if (availableCandidates.length === 0) {
10277
+ continue;
10278
+ }
10279
+ let bestCandidate = availableCandidates[0];
10280
+ let bestScore = nameSimilarity(removedField.field, bestCandidate.field);
10281
+ for (let i = 1; i < availableCandidates.length; i++) {
10282
+ const score = nameSimilarity(removedField.field, availableCandidates[i].field);
10283
+ if (score > bestScore) {
10284
+ bestScore = score;
10285
+ bestCandidate = availableCandidates[i];
10286
+ }
10287
+ }
10288
+ if (!isSinglePair && bestScore < 0.3) {
10289
+ continue;
10290
+ }
10291
+ renames.push({ oldField: removedField.field, newField: bestCandidate.field, fieldType: removedField.type });
10292
+ usedRemoved.add(removedField.field);
10293
+ usedAdded.add(bestCandidate.field);
10294
+ }
10295
+ const unmatchedRemoved = removed.filter((r) => !usedRemoved.has(r.field));
10296
+ const unmatchedAdded = added.filter((a) => !usedAdded.has(a.field));
10297
+ return { renames, unmatchedRemoved, unmatchedAdded };
10298
+ }
10299
+ function analyzeBreakingChanges(diffResult, local, remote) {
10300
+ const results = [];
10301
+ const updatedComponents = diffResult.diffs.filter(
10302
+ (d) => d.type === "component" && d.action === "update"
10303
+ );
10304
+ for (const diff of updatedComponents) {
10305
+ const localComp = local.components.find((c) => c.name === diff.name);
10306
+ const remoteComp = remote.components.get(diff.name);
10307
+ if (!localComp?.schema || !remoteComp?.schema) {
10308
+ continue;
10309
+ }
10310
+ const classification = classifyFieldChanges(
10311
+ remoteComp.schema,
10312
+ localComp.schema
10313
+ );
10314
+ const changes = [];
10315
+ const { renames, unmatchedRemoved } = detectRenames(classification.removed, classification.added);
10316
+ for (const rename of renames) {
10317
+ changes.push({ kind: "rename", field: rename.newField, oldField: rename.oldField });
10318
+ }
10319
+ for (const removed of unmatchedRemoved) {
10320
+ changes.push({ kind: "removed", field: removed.field });
10321
+ }
10322
+ for (const tc of classification.typeChanged) {
10323
+ changes.push({ kind: "type_changed", field: tc.field, oldType: tc.oldType, newType: tc.newType });
10324
+ }
10325
+ for (const ra of classification.requiredAdded) {
10326
+ changes.push({ kind: "required_added", field: ra.field, fieldType: ra.type });
10327
+ }
10328
+ for (const rc of classification.requiredChanged) {
10329
+ changes.push({ kind: "required_changed", field: rc.field, fieldType: rc.type });
10330
+ }
10331
+ if (changes.length > 0) {
10332
+ results.push({ componentName: diff.name, changes });
10333
+ }
10334
+ }
10335
+ return results;
10336
+ }
10337
+
10338
+ const COMPATIBLE_TYPES = /* @__PURE__ */ new Set(["text:textarea", "textarea:text"]);
10339
+ function defaultForType(fieldType) {
10340
+ switch (fieldType) {
10341
+ case "text":
10342
+ case "textarea":
10343
+ case "markdown":
10344
+ return `''`;
10345
+ case "number":
10346
+ return "0";
10347
+ case "boolean":
10348
+ return "false";
10349
+ default:
10350
+ return null;
10351
+ }
10352
+ }
10353
+ function typeConversion(field, oldType, newType) {
10354
+ const key = `${oldType}:${newType}`;
10355
+ if (COMPATIBLE_TYPES.has(key)) {
10356
+ return null;
10357
+ }
10358
+ const accessor = `block.${field}`;
10359
+ switch (key) {
10360
+ case "text:number":
10361
+ return `${accessor} = Number(${accessor}) || 0;`;
10362
+ case "number:text":
10363
+ return `${accessor} = String(${accessor});`;
10364
+ case "text:boolean":
10365
+ return `${accessor} = !!${accessor};`;
10366
+ case "boolean:text":
10367
+ return `${accessor} = String(${accessor});`;
10368
+ default:
10369
+ return `${accessor}; // TODO: convert from ${oldType} to ${newType}`;
10370
+ }
10371
+ }
10372
+ function renderMigrationCode(changes) {
10373
+ const lines = [];
10374
+ lines.push(" // Review this migration before running it against your space.");
10375
+ lines.push(" // Generated migrations are scaffolds and may need manual adjustments.");
10376
+ lines.push(" // Example rename migration:");
10377
+ lines.push(" // block.new_field = block.old_field;");
10378
+ lines.push(" // delete block.old_field;");
10379
+ lines.push("");
10380
+ for (const change of changes) {
10381
+ switch (change.kind) {
10382
+ case "rename":
10383
+ lines.push(` // Rename: ${change.oldField} \u2192 ${change.field}`);
10384
+ lines.push(` if ('${change.oldField}' in block) {`);
10385
+ lines.push(` block.${change.field} = block.${change.oldField};`);
10386
+ lines.push(` delete block.${change.oldField};`);
10387
+ lines.push(` }`);
10388
+ break;
10389
+ case "removed":
10390
+ if (change.renameHint) {
10391
+ lines.push(` // If '${change.field}' was renamed to '${change.renameHint.newField}', uncomment:`);
10392
+ lines.push(` // block.${change.renameHint.newField} = block.${change.field};`);
10393
+ } else {
10394
+ lines.push(` // Removed field: ${change.field}`);
10395
+ }
10396
+ lines.push(` delete block.${change.field};`);
10397
+ break;
10398
+ case "type_changed": {
10399
+ const conversion = typeConversion(change.field, change.oldType, change.newType);
10400
+ if (conversion) {
10401
+ lines.push(` // Type change: ${change.field} (${change.oldType} \u2192 ${change.newType})`);
10402
+ lines.push(` ${conversion}`);
10403
+ }
10404
+ break;
10405
+ }
10406
+ case "required_added": {
10407
+ const defaultValue = defaultForType(change.fieldType);
10408
+ lines.push(` // New required field: ${change.field} (${change.fieldType})`);
10409
+ if (defaultValue !== null) {
10410
+ lines.push(` // TODO: provide a meaningful default value`);
10411
+ lines.push(` block.${change.field} = block.${change.field} ?? ${defaultValue};`);
10412
+ } else {
10413
+ lines.push(` // TODO: provide a default value appropriate for the '${change.fieldType}' type`);
10414
+ lines.push(` // block.${change.field} = block.${change.field} ?? <default>;`);
10415
+ }
10416
+ break;
10417
+ }
10418
+ case "required_changed": {
10419
+ const defaultValue = defaultForType(change.fieldType);
10420
+ lines.push(` // Field is now required: ${change.field} (${change.fieldType})`);
10421
+ lines.push(` // Existing stories may have null/undefined values \u2014 provide a default for those.`);
10422
+ if (defaultValue !== null) {
10423
+ lines.push(` // TODO: provide a meaningful default value`);
10424
+ lines.push(` block.${change.field} = block.${change.field} ?? ${defaultValue};`);
10425
+ } else {
10426
+ lines.push(` // TODO: provide a default value appropriate for the '${change.fieldType}' type`);
10427
+ lines.push(` // block.${change.field} = block.${change.field} ?? <default>;`);
10428
+ }
10429
+ break;
10430
+ }
10431
+ }
10432
+ lines.push("");
10433
+ }
10434
+ const body = lines.length > 0 ? `
10435
+ ${lines.join("\n")}` : "\n";
10436
+ return `export default function (block) {${body} return block;
10437
+ }
10438
+ `;
10439
+ }
10440
+ async function writeMigrationFile(options) {
10441
+ const { spaceId, componentName, code, timestamp, basePath } = options;
10442
+ const dir = resolvePath(basePath, `migrations/${spaceId}`);
10443
+ await mkdir(dir, { recursive: true });
10444
+ const fileName = `${componentName}.${fileTimestamp(timestamp)}.js`;
10445
+ const filePath = join(dir, fileName);
10446
+ await writeFile(filePath, code, "utf-8");
10447
+ return filePath;
10448
+ }
10449
+
10450
+ const DEFAULT_GROUPS_FILENAME = "groups.json";
10451
+ const CONSOLIDATED_COMPONENTS_FILENAME = "components.json";
10452
+ async function writeLocalComponents({
10453
+ space,
10454
+ basePath,
10455
+ resolved,
10456
+ diffResult,
10457
+ deleteRemoved,
10458
+ ui,
10459
+ logger
10460
+ }) {
10461
+ const componentsDir = resolveCommandPath(directories.components, space, basePath);
10462
+ const consolidatedPath = join(componentsDir, CONSOLIDATED_COMPONENTS_FILENAME);
10463
+ if (await fileExists(consolidatedPath)) {
10464
+ ui.warn(
10465
+ `A consolidated ${CONSOLIDATED_COMPONENTS_FILENAME} exists at ${componentsDir}. 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.`
10466
+ );
10467
+ }
10468
+ for (const component of resolved.components) {
10469
+ const filePath = join(componentsDir, `${sanitizeFilename(component.name || "")}.json`);
10470
+ await saveToFile(filePath, JSON.stringify(component, null, 2));
10471
+ }
10472
+ const groupsPath = join(componentsDir, DEFAULT_GROUPS_FILENAME);
10473
+ if (resolved.componentFolders.length > 0) {
10474
+ await saveToFile(groupsPath, JSON.stringify(resolved.componentFolders, null, 2));
10475
+ } else if (await fileExists(groupsPath)) {
10476
+ try {
10477
+ await unlink(groupsPath);
10478
+ logger.info("Removed stale local groups file", { path: groupsPath });
10479
+ } catch (error) {
10480
+ if (error.code !== "ENOENT") {
10481
+ throw error;
10482
+ }
10483
+ }
10484
+ }
10485
+ if (deleteRemoved) {
10486
+ const staleComponents = diffResult.diffs.filter(
10487
+ (d) => d.type === "component" && d.action === "stale"
10488
+ );
10489
+ for (const stale of staleComponents) {
10490
+ const filePath = join(componentsDir, `${sanitizeFilename(stale.name)}.json`);
10491
+ try {
10492
+ await unlink(filePath);
10493
+ logger.info("Removed stale local component file", { path: filePath });
10494
+ } catch (error) {
10495
+ if (error.code !== "ENOENT") {
10496
+ throw error;
10497
+ }
10498
+ }
10499
+ }
10500
+ }
10501
+ logger.info("Wrote local component files", {
10502
+ space,
10503
+ componentsWritten: resolved.components.length,
10504
+ groupsWritten: resolved.componentFolders.length
10505
+ });
10506
+ }
10507
+
10508
+ 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) => {
10509
+ const ui = getUI();
10510
+ const logger = getLogger();
10511
+ const reporter = getReporter();
10512
+ const { space, path: basePath, verbose } = command.optsWithGlobals();
10513
+ const { state } = session();
10514
+ ui.title(commands.SCHEMA, colorPalette.SCHEMA, "Pushing schema...");
10515
+ logger.info("Schema push started", { entryFile, space });
10516
+ if (!requireAuthentication(state, verbose)) {
10517
+ return;
10518
+ }
10519
+ if (!space) {
10520
+ handleError(new CommandError("Please provide the space as argument --space SPACE_ID."), verbose);
10521
+ return;
10522
+ }
10523
+ const summary = { total: 0, succeeded: 0, failed: 0 };
10524
+ try {
10525
+ const loadSpinner = ui.createSpinner("Resolving schema...");
10526
+ let local;
10527
+ try {
10528
+ local = await loadSchema(entryFile);
10529
+ } catch (maybeError) {
10530
+ loadSpinner.failed("Failed to resolve schema");
10531
+ handleError(toError(maybeError), verbose);
10532
+ return;
10533
+ }
10534
+ loadSpinner.succeed(`Found: ${local.components.length} components, ${local.componentFolders.length} component folders, ${local.datasources.length} datasources`);
10535
+ const totalLocal = local.components.length + local.componentFolders.length + local.datasources.length;
10536
+ if (totalLocal === 0) {
10537
+ ui.warn("No components, folders, or datasources found in the entry file. Verify the file exports schema definitions.");
10538
+ return;
10539
+ }
10540
+ const remoteSpinner = ui.createSpinner(`Fetching remote state from space ${space}...`);
10541
+ let remoteResult;
10542
+ try {
10543
+ remoteResult = await fetchRemoteSchema(space);
10544
+ } catch (maybeError) {
10545
+ remoteSpinner.failed("Failed to fetch remote schema");
10546
+ handleError(toError(maybeError), verbose);
10547
+ return;
10548
+ }
10549
+ const { remote, rawComponents, rawComponentFolders, rawDatasources } = remoteResult;
10550
+ remoteSpinner.succeed(`Remote: ${remote.components.size} components, ${remote.componentFolders.size} component folders, ${remote.datasources.size} datasources`);
10551
+ const { resolved, pendingFolderAssignments } = resolveFolderReferences(local, remote);
10552
+ const diffResult = diffSchema(resolved, remote);
10553
+ ui.br();
10554
+ ui.log(formatDiffOutput(diffResult, { delete: options.delete }));
10555
+ if (options.migrations) {
10556
+ const breakingChanges = analyzeBreakingChanges(diffResult, resolved, remote);
10557
+ if (breakingChanges.length > 0) {
10558
+ const totalChanges = breakingChanges.reduce((sum, c) => sum + c.changes.length, 0);
10559
+ ui.br();
10560
+ ui.warn(`${totalChanges} breaking change(s) detected in ${breakingChanges.length} component(s).`);
10561
+ ui.info("Generated migrations are scaffolds. Review and adjust them before running `storyblok migrations run`.");
10562
+ if (!options.dryRun) {
10563
+ const explicitMigrations = command.getOptionValueSource("migrations") === "cli";
10564
+ const shouldGenerate = explicitMigrations || await confirm({
10565
+ message: "Generate migration files for breaking changes?",
10566
+ default: true
10567
+ });
10568
+ if (shouldGenerate) {
10569
+ const migrationTimestamp = (/* @__PURE__ */ new Date()).toISOString();
10570
+ const resolvedBase = resolvePath(basePath, "");
10571
+ for (const comp of breakingChanges) {
10572
+ const renames = comp.changes.filter((c) => c.kind === "rename");
10573
+ if (renames.length > 0 && explicitMigrations) {
10574
+ for (const r of renames) {
10575
+ if (r.kind === "rename") {
10576
+ ui.log(` Assumed rename in '${comp.componentName}': ${r.oldField} \u2192 ${r.field}`);
10577
+ }
10578
+ }
10579
+ }
10580
+ if (renames.length > 0 && !explicitMigrations) {
10581
+ ui.br();
10582
+ ui.log(`Detected renames in '${comp.componentName}':`);
10583
+ for (const r of renames) {
10584
+ if (r.kind === "rename") {
10585
+ ui.log(` ${r.oldField} \u2192 ${r.field}`);
10586
+ }
10587
+ }
10588
+ const renameConfirmed = await confirm({
10589
+ message: "Are these renames correct?",
10590
+ default: true
10591
+ });
10592
+ if (!renameConfirmed) {
10593
+ comp.changes = comp.changes.map((c) => {
10594
+ if (c.kind === "rename") {
10595
+ return { kind: "removed", field: c.oldField, renameHint: { newField: c.field } };
10596
+ }
10597
+ return c;
10598
+ });
10599
+ }
10600
+ }
10601
+ const code = renderMigrationCode(comp.changes);
10602
+ const path = await writeMigrationFile({
10603
+ spaceId: space,
10604
+ componentName: comp.componentName,
10605
+ code,
10606
+ timestamp: migrationTimestamp,
10607
+ basePath: resolvedBase
10608
+ });
10609
+ ui.log(` Generated: ${path}`);
10610
+ }
10611
+ ui.br();
10612
+ ui.info(`Run migrations when ready: storyblok migrations run --space ${space}`);
10613
+ }
10614
+ }
10615
+ }
10616
+ }
10617
+ if (options.delete) {
10618
+ const deletedComponents = diffResult.diffs.filter((d) => d.type === "component" && d.action === "stale");
10619
+ for (const comp of deletedComponents) {
10620
+ ui.warn(`Component '${comp.name}' will be deleted. Stories using it will have out-of-schema content.`);
10621
+ }
10622
+ }
10623
+ if (diffResult.stale > 0 && !options.delete) {
10624
+ ui.warn(`${diffResult.stale} stale entity(s) exist remotely but not in schema. Use --delete to remove.`);
10625
+ }
10626
+ if (options.dryRun) {
10627
+ ui.info("Dry run \u2014 no changes applied.");
10628
+ logger.info("Dry run completed", { creates: diffResult.creates, updates: diffResult.updates });
10629
+ return;
10630
+ }
10631
+ const resolvedPath = resolvePath(basePath, "");
10632
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
10633
+ const changesetPath = await saveChangeset(resolvedPath, {
10634
+ timestamp,
10635
+ spaceId: Number(space),
10636
+ remote: { components: rawComponents, componentFolders: rawComponentFolders, datasources: rawDatasources },
10637
+ changes: buildChangesetEntries(diffResult, resolved, remote, { delete: options.delete })
10638
+ });
10639
+ logger.info("Changeset saved", { path: changesetPath });
10640
+ const nothingToPush = diffResult.creates === 0 && diffResult.updates === 0 && (!options.delete || diffResult.stale === 0);
10641
+ if (nothingToPush) {
10642
+ ui.ok("Everything up to date \u2014 nothing to push.");
10643
+ } else {
10644
+ const pushSpinner = ui.createSpinner("Pushing schema...");
10645
+ let result;
10646
+ try {
10647
+ result = await executePush(space, resolved, remote, diffResult, { delete: options.delete, pendingFolderAssignments });
10648
+ } catch (error) {
10649
+ pushSpinner.failed("Failed to push schema");
10650
+ throw error;
10651
+ }
10652
+ summary.total = result.created + result.updated + result.deleted;
10653
+ summary.succeeded = summary.total;
10654
+ pushSpinner.succeed(`Pushed ${result.created} creations, ${result.updated} updates${result.deleted > 0 ? `, ${result.deleted} deletions` : ""}.`);
10655
+ }
10656
+ if (options.writeComponents) {
10657
+ try {
10658
+ await writeLocalComponents({
10659
+ space,
10660
+ basePath,
10661
+ resolved,
10662
+ diffResult,
10663
+ deleteRemoved: options.delete,
10664
+ ui,
10665
+ logger
10666
+ });
10667
+ } catch (writeError) {
10668
+ ui.warn(`Failed to write local component files: ${toError(writeError).message}`);
10669
+ logger.warn("Failed to write local component files", { error: toError(writeError).message });
10670
+ }
10671
+ }
10672
+ } catch (maybeError) {
10673
+ summary.failed += 1;
10674
+ handleError(toError(maybeError), verbose);
10675
+ } finally {
10676
+ logger.info("Schema push finished", { summary });
10677
+ reporter.addSummary("schemaPushResults", summary);
10678
+ reporter.finalize();
10679
+ }
10680
+ });
10681
+
10682
+ const FIELD_STRIP_KEYS = /* @__PURE__ */ new Set(["id", "pos"]);
10683
+ function toCamelCase(str) {
10684
+ return str.toLowerCase().replace(/[\s_-]+(.)/g, (_, char) => char.toUpperCase());
10685
+ }
10686
+ function toKebabCase(str) {
10687
+ return str.replace(/[\s_]+/g, "-").replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
10688
+ }
10689
+ function componentVarName(name) {
10690
+ return `${toCamelCase(name)}Block`;
10691
+ }
10692
+ function folderVarName(name) {
10693
+ return `${toCamelCase(name)}Folder`;
10694
+ }
10695
+ function datasourceVarName(name) {
10696
+ return `${toCamelCase(name)}Datasource`;
10697
+ }
10698
+ function componentFileName(name) {
10699
+ return toKebabCase(name);
10700
+ }
10701
+ function folderFileName(name) {
10702
+ return toKebabCase(name);
10703
+ }
10704
+ function datasourceFileName(datasource) {
10705
+ return toKebabCase(datasource.slug || datasource.name);
10706
+ }
10707
+ function generateFieldCode(fieldName, fieldData, depth) {
10708
+ const clean = stripKeys(fieldData, FIELD_STRIP_KEYS);
10709
+ return `defineField('${fieldName.replace(/'/g, "\\'")}', ${formatValue(clean, depth + 1)})`;
10710
+ }
10711
+ function sortSchemaByPos(schema) {
10712
+ return Object.entries(schema).filter(([key]) => key !== "_uid" && key !== "component").sort(([, a], [, b]) => {
10713
+ const posA = typeof a.pos === "number" ? a.pos : Infinity;
10714
+ const posB = typeof b.pos === "number" ? b.pos : Infinity;
10715
+ return posA - posB;
10716
+ });
10717
+ }
10718
+ function generateComponentFile(component, componentFolders) {
10719
+ const lines = [];
10720
+ let matchedFolder;
10721
+ if (component.component_group_uuid && componentFolders) {
10722
+ matchedFolder = componentFolders.find((f) => f.uuid === component.component_group_uuid);
10723
+ }
10724
+ lines.push("import {");
10725
+ lines.push(" defineBlock,");
10726
+ lines.push(" defineField,");
10727
+ lines.push("} from '@storyblok/schema';");
10728
+ if (matchedFolder) {
10729
+ lines.push("");
10730
+ const fVarName = folderVarName(matchedFolder.name);
10731
+ const fFileName = folderFileName(matchedFolder.name);
10732
+ lines.push(`import { ${fVarName} } from './folders/${fFileName}';`);
10733
+ }
10734
+ lines.push("");
10735
+ const varName = componentVarName(component.name);
10736
+ lines.push(`export const ${varName} = defineBlock({`);
10737
+ const clean = stripKeys(component, COMPONENT_STRIP_KEYS);
10738
+ if (matchedFolder) {
10739
+ delete clean.component_group_uuid;
10740
+ }
10741
+ const orderedKeys = [];
10742
+ if (clean.name !== void 0) {
10743
+ orderedKeys.push("name");
10744
+ }
10745
+ if (clean.display_name !== void 0) {
10746
+ orderedKeys.push("display_name");
10747
+ }
10748
+ if (clean.is_root !== void 0) {
10749
+ orderedKeys.push("is_root");
10750
+ }
10751
+ if (clean.is_nestable !== void 0) {
10752
+ orderedKeys.push("is_nestable");
10753
+ }
10754
+ const handled = /* @__PURE__ */ new Set(["name", "display_name", "is_root", "is_nestable", "schema"]);
10755
+ for (const key of Object.keys(clean).sort()) {
10756
+ if (!handled.has(key)) {
10757
+ orderedKeys.push(key);
10758
+ }
10759
+ }
10760
+ if (matchedFolder) {
10761
+ const fVarName = folderVarName(matchedFolder.name);
10762
+ lines.push(`${INDENT}component_group_uuid: ${fVarName}.uuid,`);
10763
+ }
10764
+ for (const key of orderedKeys) {
10765
+ lines.push(`${INDENT}${key}: ${formatValue(clean[key], 1)},`);
10766
+ }
10767
+ if (clean.schema && typeof clean.schema === "object") {
10768
+ const schema = clean.schema;
10769
+ const sortedFields = sortSchemaByPos(schema);
10770
+ if (sortedFields.length > 0) {
10771
+ lines.push(`${INDENT}schema: [`);
10772
+ for (const [fieldName, fieldData] of sortedFields) {
10773
+ const fieldCode = generateFieldCode(fieldName, fieldData, 2);
10774
+ lines.push(`${INDENT}${INDENT}${fieldCode},`);
10775
+ }
10776
+ lines.push(`${INDENT}],`);
10777
+ } else {
10778
+ lines.push(`${INDENT}schema: [],`);
10779
+ }
10780
+ }
10781
+ lines.push("});");
10782
+ lines.push("");
10783
+ return lines.join("\n");
10784
+ }
10785
+ function generateFolderFile(folder) {
10786
+ const lines = [];
10787
+ lines.push("import { defineBlockFolder } from '@storyblok/schema';");
10788
+ lines.push("");
10789
+ const varName = folderVarName(folder.name);
10790
+ lines.push(`export const ${varName} = defineBlockFolder({`);
10791
+ const clean = stripKeys(folder, FOLDER_INIT_STRIP_KEYS);
10792
+ if (clean.name !== void 0) {
10793
+ lines.push(`${INDENT}name: ${formatValue(clean.name, 1)},`);
10794
+ }
10795
+ const handled = /* @__PURE__ */ new Set(["name"]);
10796
+ for (const [key, value] of Object.entries(clean).sort(([a], [b]) => a.localeCompare(b))) {
10797
+ if (!handled.has(key)) {
10798
+ lines.push(`${INDENT}${key}: ${formatValue(value, 1)},`);
10799
+ }
10800
+ }
10801
+ lines.push("});");
10802
+ lines.push("");
10803
+ return lines.join("\n");
10804
+ }
10805
+ function generateDatasourceFile(datasource) {
10806
+ const lines = [];
10807
+ lines.push("import { defineDatasource } from '@storyblok/schema';");
10808
+ lines.push("");
10809
+ const varName = datasourceVarName(datasource.name);
10810
+ lines.push(`export const ${varName} = defineDatasource({`);
10811
+ const clean = stripKeys(datasource, DATASOURCE_STRIP_KEYS);
10812
+ if (clean.name !== void 0) {
10813
+ lines.push(`${INDENT}name: ${formatValue(clean.name, 1)},`);
10814
+ }
10815
+ if (clean.slug !== void 0) {
10816
+ lines.push(`${INDENT}slug: ${formatValue(clean.slug, 1)},`);
10817
+ }
10818
+ const handled = /* @__PURE__ */ new Set(["name", "slug"]);
10819
+ for (const [key, value] of Object.entries(clean).sort(([a], [b]) => a.localeCompare(b))) {
10820
+ if (!handled.has(key)) {
10821
+ lines.push(`${INDENT}${key}: ${formatValue(value, 1)},`);
10822
+ }
10823
+ }
10824
+ lines.push("});");
10825
+ lines.push("");
10826
+ return lines.join("\n");
10827
+ }
10828
+ function generateSchemaFile(components, componentFolders, datasources) {
10829
+ const lines = [];
10830
+ lines.push("import type { Schema as InferSchema, Story as InferStory } from '@storyblok/schema';");
10831
+ lines.push("import type { MapiStory as InferStoryMapi } from '@storyblok/schema';");
10832
+ lines.push("");
10833
+ for (const component of components) {
10834
+ const varName = componentVarName(component.name);
10835
+ const fileName = componentFileName(component.name);
10836
+ lines.push(`import { ${varName} } from './components/${fileName}';`);
10837
+ }
10838
+ for (const folder of componentFolders) {
10839
+ const varName = folderVarName(folder.name);
10840
+ const fileName = folderFileName(folder.name);
10841
+ lines.push(`import { ${varName} } from './components/folders/${fileName}';`);
10842
+ }
10843
+ for (const datasource of datasources) {
10844
+ const varName = datasourceVarName(datasource.name);
10845
+ const fileName = datasourceFileName(datasource);
10846
+ lines.push(`import { ${varName} } from './datasources/${fileName}';`);
10847
+ }
10848
+ lines.push("");
10849
+ lines.push("export const schema = {");
10850
+ if (components.length > 0) {
10851
+ lines.push(" blocks: {");
10852
+ for (const component of components) {
10853
+ const varName = componentVarName(component.name);
10854
+ lines.push(` ${varName},`);
10855
+ }
10856
+ lines.push(" },");
10857
+ }
10858
+ if (componentFolders.length > 0) {
10859
+ lines.push(" blockFolders: {");
10860
+ for (const folder of componentFolders) {
10861
+ const varName = folderVarName(folder.name);
10862
+ lines.push(` ${varName},`);
10863
+ }
10864
+ lines.push(" },");
10865
+ }
10866
+ if (datasources.length > 0) {
10867
+ lines.push(" datasources: {");
10868
+ for (const datasource of datasources) {
10869
+ const varName = datasourceVarName(datasource.name);
10870
+ lines.push(` ${varName},`);
10871
+ }
10872
+ lines.push(" },");
10873
+ }
10874
+ lines.push("};");
10875
+ lines.push("");
10876
+ lines.push("export type Schema = InferSchema<typeof schema>;");
10877
+ lines.push("export type Blocks = Schema['blocks'];");
10878
+ lines.push("export type Story = InferStory<Blocks>;");
10879
+ lines.push("export type StoryMapi = InferStoryMapi<Blocks>;");
10880
+ lines.push("");
10881
+ return lines.join("\n");
10882
+ }
10883
+
10884
+ async function writeFileWithDirs(filePath, content) {
10885
+ const dir = dirname(filePath);
10886
+ await mkdir(dir, { recursive: true });
10887
+ await writeFile(filePath, content, "utf-8");
10888
+ }
10889
+ async function writeSchemaFiles(targetPath, components, componentFolders, datasources) {
10890
+ const writtenFiles = [];
10891
+ for (const comp of components) {
10892
+ const fileName = componentFileName(comp.name);
10893
+ const filePath = join(targetPath, "components", `${fileName}.ts`);
10894
+ await writeFileWithDirs(filePath, generateComponentFile(comp, componentFolders));
10895
+ writtenFiles.push(filePath);
10896
+ }
10897
+ for (const folder of componentFolders) {
10898
+ const fileName = folderFileName(folder.name);
10899
+ const filePath = join(targetPath, "components", "folders", `${fileName}.ts`);
10900
+ await writeFileWithDirs(filePath, generateFolderFile(folder));
10901
+ writtenFiles.push(filePath);
10902
+ }
10903
+ for (const ds of datasources) {
10904
+ const fileName = datasourceFileName(ds);
10905
+ const filePath = join(targetPath, "datasources", `${fileName}.ts`);
10906
+ await writeFileWithDirs(filePath, generateDatasourceFile(ds));
10907
+ writtenFiles.push(filePath);
10908
+ }
10909
+ const schemaPath = join(targetPath, "schema.ts");
10910
+ await writeFileWithDirs(schemaPath, generateSchemaFile(components, componentFolders, datasources));
10911
+ writtenFiles.push(schemaPath);
10912
+ return writtenFiles;
10913
+ }
10914
+
10915
+ async function isTargetEmpty(targetPath) {
10916
+ try {
10917
+ const entries = await readdir(targetPath);
10918
+ return entries.every((entry) => entry.startsWith("."));
10919
+ } catch (maybeError) {
10920
+ const error = maybeError;
10921
+ if (error?.code === "ENOENT") {
10922
+ return true;
10923
+ }
10924
+ throw error;
10925
+ }
10926
+ }
10927
+ 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) => {
10928
+ const ui = getUI();
10929
+ const logger = getLogger();
10930
+ const reporter = getReporter();
10931
+ const { space, verbose } = command.optsWithGlobals();
10932
+ const { state } = session();
10933
+ ui.title(commands.SCHEMA, colorPalette.SCHEMA, "Initializing schema...");
10934
+ logger.info("Schema init started", { space });
10935
+ if (!requireAuthentication(state, verbose)) {
10936
+ return;
10937
+ }
10938
+ if (!space) {
10939
+ handleError(new CommandError("Please provide the space as argument --space SPACE_ID."), verbose);
10940
+ return;
10941
+ }
10942
+ const targetPath = resolve(options.outDir);
10943
+ if (!await isTargetEmpty(targetPath)) {
10944
+ handleError(
10945
+ new CommandError(`Target directory ${targetPath} 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.`),
10946
+ verbose
10947
+ );
10948
+ return;
10949
+ }
10950
+ const summary = { total: 0, succeeded: 0, failed: 0 };
10951
+ try {
10952
+ const fetchSpinner = ui.createSpinner(`Fetching schema from space ${space}...`);
10953
+ let fetchResult;
10954
+ try {
10955
+ fetchResult = await fetchRemoteSchema(space);
10956
+ } catch (maybeError) {
10957
+ fetchSpinner.failed("Failed to fetch remote schema");
10958
+ handleError(toError(maybeError), verbose);
10959
+ return;
10960
+ }
10961
+ const { rawComponents, rawComponentFolders, rawDatasources } = fetchResult;
10962
+ fetchSpinner.succeed(`Found: ${rawComponents.length} components, ${rawComponentFolders.length} component folders, ${rawDatasources.length} datasources`);
10963
+ const writeSpinner = ui.createSpinner(`Generating TypeScript files to ${targetPath}...`);
10964
+ const writtenFiles = await writeSchemaFiles(targetPath, rawComponents, rawComponentFolders, rawDatasources);
10965
+ summary.total = writtenFiles.length;
10966
+ summary.succeeded = writtenFiles.length;
10967
+ writeSpinner.succeed(`Generated ${writtenFiles.length} files`);
10968
+ ui.list(writtenFiles);
10969
+ ui.warn("`schema init` is a one-time bootstrap step for adopting an existing space. Review generated files before continuing.");
10970
+ ui.info("After bootstrapping, keep your local schema as the source of truth and use `schema push` for ongoing changes.");
10971
+ ui.info("Make sure `@storyblok/schema` is installed in the project that imports these files (e.g. `pnpm add @storyblok/schema`).");
10972
+ } catch (maybeError) {
10973
+ summary.failed += 1;
10974
+ handleError(toError(maybeError), verbose);
10975
+ } finally {
10976
+ logger.info("Schema init finished", { summary });
10977
+ reporter.addSummary("schemaInitResults", summary);
10978
+ reporter.finalize();
10979
+ }
10980
+ });
10981
+
10982
+ const API_ASSIGNED_FIELDS = [
10983
+ "id",
10984
+ "created_at",
10985
+ "updated_at",
10986
+ "real_name",
10987
+ "all_presets",
10988
+ "image",
10989
+ "uuid"
10990
+ ];
10991
+ function stripApiFields(payload) {
10992
+ const result = { ...payload };
10993
+ for (const field of API_ASSIGNED_FIELDS) {
10994
+ delete result[field];
10995
+ }
10996
+ return result;
10997
+ }
10998
+ async function listChangesets(basePath) {
10999
+ const dir = join(basePath, "schema", "changesets");
11000
+ if (!await fileExists(dir)) {
11001
+ return [];
11002
+ }
11003
+ const files = await readDirectory(dir);
11004
+ return files.filter((f) => f.endsWith(".json")).sort().reverse().map((f) => join(dir, f));
11005
+ }
11006
+ async function loadChangeset(filePath) {
11007
+ const content = await readFile$1(filePath, "utf-8");
11008
+ return JSON.parse(content);
11009
+ }
11010
+ function buildRollbackOps(changeset) {
11011
+ if (changeset.changes.length === 0) {
11012
+ return [];
11013
+ }
11014
+ return changeset.changes.map((entry) => {
11015
+ switch (entry.action) {
11016
+ case "create":
11017
+ return { type: entry.type, name: entry.name, action: "delete", payload: {} };
11018
+ case "update":
11019
+ return { type: entry.type, name: entry.name, action: "update", payload: entry.before ?? {} };
11020
+ case "delete":
11021
+ return { type: entry.type, name: entry.name, action: "create", payload: entry.before ?? {} };
11022
+ default:
11023
+ return { type: entry.type, name: entry.name, action: entry.action, payload: {} };
11024
+ }
11025
+ });
11026
+ }
11027
+ function rollbackAction(original) {
11028
+ switch (original) {
11029
+ case "create":
11030
+ return "delete";
11031
+ case "update":
11032
+ return "update";
11033
+ case "delete":
11034
+ return "create";
11035
+ }
11036
+ }
11037
+ function formatRollbackOutput(changes) {
11038
+ const byType = {
11039
+ component: [],
11040
+ componentFolder: [],
11041
+ datasource: []
11042
+ };
11043
+ for (const entry of changes) {
11044
+ byType[entry.type]?.push(entry);
11045
+ }
11046
+ const icons = {
11047
+ create: chalk.green("+"),
11048
+ update: chalk.yellow("~"),
11049
+ delete: chalk.red("-")
11050
+ };
11051
+ const lines = [];
11052
+ const sections = [
11053
+ ["Components", byType.component],
11054
+ ["Component Folders", byType.componentFolder],
11055
+ ["Datasources", byType.datasource]
11056
+ ];
11057
+ for (const [label, entries] of sections) {
11058
+ if (entries.length === 0) {
11059
+ continue;
11060
+ }
11061
+ lines.push(chalk.bold(label));
11062
+ for (const entry of entries) {
11063
+ const action = rollbackAction(entry.action);
11064
+ const icon = icons[action] ?? " ";
11065
+ const name = action === "delete" ? chalk.red(entry.name) : entry.name;
11066
+ lines.push(` ${icon} ${name} ${chalk.dim(`(${action})`)}`);
11067
+ if (entry.action === "update" && entry.before && entry.after) {
11068
+ let fromStr;
11069
+ let toStr;
11070
+ if (entry.type === "component") {
11071
+ fromStr = serializeComponent(applyDefaults(entry.after, COMPONENT_DEFAULTS));
11072
+ toStr = serializeComponent(applyDefaults(entry.before, COMPONENT_DEFAULTS));
11073
+ } else if (entry.type === "componentFolder") {
11074
+ fromStr = serializeComponentFolder(entry.after);
11075
+ toStr = serializeComponentFolder(entry.before);
11076
+ } else {
11077
+ fromStr = serializeDatasource(entry.after);
11078
+ toStr = serializeDatasource(entry.before);
11079
+ }
11080
+ if (fromStr !== toStr) {
11081
+ const patch = createTwoFilesPatch(
11082
+ `current/${entry.name}`,
11083
+ `restore/${entry.name}`,
11084
+ fromStr,
11085
+ toStr,
11086
+ "current",
11087
+ "restore"
11088
+ );
11089
+ for (const line of patch.split("\n")) {
11090
+ if (line.startsWith("+") && !line.startsWith("+++")) {
11091
+ lines.push(` ${chalk.green(line)}`);
11092
+ } else if (line.startsWith("-") && !line.startsWith("---")) {
11093
+ lines.push(` ${chalk.red(line)}`);
11094
+ }
11095
+ }
11096
+ }
11097
+ }
11098
+ }
11099
+ lines.push("");
11100
+ }
11101
+ return lines.join("\n").trimEnd();
11102
+ }
11103
+ async function executeRollback(spaceId, ops, remote) {
11104
+ const client = getMapiClient();
11105
+ const spaceIdNum = Number(spaceId);
11106
+ let created = 0;
11107
+ let updated = 0;
11108
+ let deleted = 0;
11109
+ const folderOps = ops.filter((op) => op.type === "componentFolder");
11110
+ const componentOps = ops.filter((op) => op.type === "component");
11111
+ const datasourceOps = ops.filter((op) => op.type === "datasource");
11112
+ const folderUuidRemap = /* @__PURE__ */ new Map();
11113
+ for (const op of folderOps.filter((o) => o.action !== "delete")) {
11114
+ if (op.action === "create") {
11115
+ const oldUuid = op.payload.uuid;
11116
+ const payload = toComponentFolderCreate(stripApiFields(op.payload));
11117
+ try {
11118
+ const response = await client.componentFolders.create({
11119
+ path: { space_id: spaceIdNum },
11120
+ body: { component_group: payload },
11121
+ throwOnError: true
11122
+ });
11123
+ const remoteUuid = response.data?.component_group?.uuid;
11124
+ if (remoteUuid && typeof oldUuid === "string") {
11125
+ folderUuidRemap.set(oldUuid, remoteUuid);
11126
+ }
11127
+ created++;
11128
+ } catch (error) {
11129
+ handleAPIError("push_component_group", error, `Failed to create folder ${op.name}`);
11130
+ }
11131
+ } else if (op.action === "update") {
11132
+ const existing = remote.componentFolders.get(op.name);
11133
+ if (existing?.id) {
11134
+ const payload = toComponentFolderCreate(stripApiFields(op.payload));
11135
+ try {
11136
+ await client.componentFolders.update(existing.id, {
11137
+ path: { space_id: spaceIdNum },
11138
+ body: { component_group: payload },
11139
+ throwOnError: true
11140
+ });
11141
+ updated++;
11142
+ } catch (error) {
11143
+ handleAPIError("update_component_group", error, `Failed to update folder ${op.name}`);
11144
+ }
11145
+ }
11146
+ }
11147
+ }
11148
+ for (const op of componentOps) {
11149
+ const oldUuid = op.payload.component_group_uuid;
11150
+ if (typeof oldUuid !== "string") {
11151
+ continue;
11152
+ }
11153
+ const newUuid = folderUuidRemap.get(oldUuid);
11154
+ if (newUuid) {
11155
+ op.payload.component_group_uuid = newUuid;
11156
+ }
11157
+ }
11158
+ for (const op of componentOps.filter((o) => o.action !== "delete")) {
11159
+ if (op.action === "create") {
11160
+ const payload = toComponentCreate(stripApiFields(op.payload));
11161
+ try {
11162
+ await client.components.create({
11163
+ path: { space_id: spaceIdNum },
11164
+ body: { component: payload },
11165
+ throwOnError: true
11166
+ });
11167
+ created++;
11168
+ } catch (error) {
11169
+ handleAPIError("push_component", error, `Failed to create component ${op.name}`);
11170
+ }
11171
+ } else if (op.action === "update") {
11172
+ const existing = remote.components.get(op.name);
11173
+ if (existing?.id) {
11174
+ const payload = toComponentUpdate(stripApiFields(op.payload));
11175
+ try {
11176
+ await client.components.update(existing.id, {
11177
+ path: { space_id: spaceIdNum },
11178
+ body: { component: payload },
11179
+ throwOnError: true
11180
+ });
11181
+ updated++;
11182
+ } catch (error) {
11183
+ handleAPIError("update_component", error, `Failed to update component ${op.name}`);
11184
+ }
11185
+ }
11186
+ }
11187
+ }
11188
+ for (const op of datasourceOps.filter((o) => o.action !== "delete")) {
11189
+ if (op.action === "create") {
11190
+ const payload = toDatasourceCreate(stripApiFields(op.payload));
11191
+ try {
11192
+ await client.datasources.create({
11193
+ path: { space_id: spaceIdNum },
11194
+ body: { datasource: payload },
11195
+ throwOnError: true
11196
+ });
11197
+ created++;
11198
+ } catch (error) {
11199
+ handleAPIError("push_datasource", error, `Failed to create datasource ${op.name}`);
11200
+ }
11201
+ } else if (op.action === "update") {
11202
+ const existing = remote.datasources.get(op.name);
11203
+ if (existing?.id) {
11204
+ const payload = toDatasourceUpdate(stripApiFields(op.payload), existing);
11205
+ try {
11206
+ await client.datasources.update(existing.id, {
11207
+ path: { space_id: spaceIdNum },
11208
+ body: { datasource: payload },
11209
+ throwOnError: true
11210
+ });
11211
+ updated++;
11212
+ } catch (error) {
11213
+ handleAPIError("update_datasource", error, `Failed to update datasource ${op.name}`);
11214
+ }
11215
+ }
11216
+ }
11217
+ }
11218
+ for (const op of datasourceOps.filter((o) => o.action === "delete")) {
11219
+ const existing = remote.datasources.get(op.name);
11220
+ if (existing?.id) {
11221
+ try {
11222
+ await client.datasources.delete(existing.id, {
11223
+ path: { space_id: spaceIdNum },
11224
+ throwOnError: true
11225
+ });
11226
+ deleted++;
11227
+ } catch (error) {
11228
+ handleAPIError("delete_datasource", error, `Failed to delete datasource ${op.name}`);
11229
+ }
11230
+ }
11231
+ }
11232
+ for (const op of componentOps.filter((o) => o.action === "delete")) {
11233
+ const existing = remote.components.get(op.name);
11234
+ if (existing?.id) {
11235
+ try {
11236
+ await client.components.delete(existing.id, {
11237
+ path: { space_id: spaceIdNum },
11238
+ throwOnError: true
11239
+ });
11240
+ deleted++;
11241
+ } catch (error) {
11242
+ handleAPIError("push_component", error, `Failed to delete component ${op.name}`);
11243
+ }
11244
+ }
11245
+ }
11246
+ for (const op of folderOps.filter((o) => o.action === "delete")) {
11247
+ const existing = remote.componentFolders.get(op.name);
11248
+ if (existing?.id) {
11249
+ try {
11250
+ await client.componentFolders.delete(existing.id, {
11251
+ path: { space_id: spaceIdNum },
11252
+ throwOnError: true
11253
+ });
11254
+ deleted++;
11255
+ } catch (error) {
11256
+ handleAPIError("push_component_group", error, `Failed to delete folder ${op.name}`);
11257
+ }
11258
+ }
11259
+ }
11260
+ return { created, updated, deleted };
11261
+ }
11262
+
11263
+ 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) => {
11264
+ const ui = getUI();
11265
+ const logger = getLogger();
11266
+ const reporter = getReporter();
11267
+ const { space, path: basePath, verbose } = command.optsWithGlobals();
11268
+ const { state } = session();
11269
+ ui.title(commands.SCHEMA, colorPalette.SCHEMA, "Rolling back schema...");
11270
+ logger.info("Schema rollback started", { changesetFile, space });
11271
+ if (!requireAuthentication(state, verbose)) {
11272
+ return;
11273
+ }
11274
+ if (!space) {
11275
+ handleError(new CommandError("Please provide the space as argument --space SPACE_ID."), verbose);
11276
+ return;
11277
+ }
11278
+ const summary = { total: 0, succeeded: 0, failed: 0 };
11279
+ try {
11280
+ const resolvedBase = resolvePath(basePath, "");
11281
+ let resolvedFile;
11282
+ if (changesetFile) {
11283
+ resolvedFile = changesetFile;
11284
+ } else if (options.latest) {
11285
+ const available = await listChangesets(resolvedBase);
11286
+ if (available.length === 0) {
11287
+ ui.warn("No changesets found. Run `schema push` first to create one.");
11288
+ return;
11289
+ }
11290
+ resolvedFile = available[0];
11291
+ ui.info(`Using latest changeset: ${basename(resolvedFile)}`);
11292
+ } else {
11293
+ const available = await listChangesets(resolvedBase);
11294
+ if (available.length === 0) {
11295
+ ui.warn("No changesets found. Run `schema push` first to create one.");
11296
+ return;
11297
+ }
11298
+ resolvedFile = await select({
11299
+ message: "Select a changeset to roll back:",
11300
+ choices: available.map((f) => ({ name: basename(f), value: f }))
11301
+ });
11302
+ }
11303
+ let changeset;
11304
+ try {
11305
+ changeset = await loadChangeset(resolvedFile);
11306
+ } catch (maybeError) {
11307
+ handleError(toError(maybeError), verbose);
11308
+ return;
11309
+ }
11310
+ logger.info("Changeset loaded", { file: resolvedFile, changes: changeset.changes.length });
11311
+ const ops = buildRollbackOps(changeset);
11312
+ if (ops.length === 0) {
11313
+ ui.ok("Changeset has no changes \u2014 nothing to roll back.");
11314
+ return;
11315
+ }
11316
+ ui.br();
11317
+ ui.log(formatRollbackOutput(changeset.changes));
11318
+ if (options.dryRun) {
11319
+ ui.info("Dry run \u2014 no changes applied.");
11320
+ logger.info("Dry run completed", { ops: ops.length });
11321
+ return;
11322
+ }
11323
+ if (!options.yes) {
11324
+ const confirmed = await confirm({
11325
+ message: `Apply rollback of ${ops.length} change(s) from ${basename(resolvedFile)}?`,
11326
+ default: false
11327
+ });
11328
+ if (!confirmed) {
11329
+ ui.info("Rollback cancelled.");
11330
+ return;
11331
+ }
11332
+ }
11333
+ const remoteSpinner = ui.createSpinner(`Fetching current remote state from space ${space}...`);
11334
+ let remoteResult;
11335
+ try {
11336
+ remoteResult = await fetchRemoteSchema(space);
11337
+ } catch (maybeError) {
11338
+ remoteSpinner.failed("Failed to fetch remote schema");
11339
+ handleError(toError(maybeError), verbose);
11340
+ return;
11341
+ }
11342
+ const { remote } = remoteResult;
11343
+ remoteSpinner.succeed(`Remote: ${remote.components.size} components, ${remote.componentFolders.size} component folders, ${remote.datasources.size} datasources`);
11344
+ const rollbackSpinner = ui.createSpinner("Applying rollback...");
11345
+ let result;
11346
+ try {
11347
+ result = await executeRollback(space, ops, remote);
11348
+ } catch (error) {
11349
+ rollbackSpinner.failed("Failed to apply rollback");
11350
+ throw error;
11351
+ }
11352
+ summary.total = result.created + result.updated + result.deleted;
11353
+ summary.succeeded = summary.total;
11354
+ rollbackSpinner.succeed(`Rolled back: ${result.created} created, ${result.updated} updated, ${result.deleted} deleted.`);
11355
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
11356
+ const rollbackChangesetPath = await saveChangeset(resolvedBase, {
11357
+ timestamp,
11358
+ spaceId: Number(space),
11359
+ remote: { components: remoteResult.rawComponents, componentFolders: remoteResult.rawComponentFolders, datasources: remoteResult.rawDatasources },
11360
+ changes: ops.map((op) => ({
11361
+ type: op.type,
11362
+ name: op.name,
11363
+ action: op.action,
11364
+ ...Object.keys(op.payload).length > 0 && { after: op.payload }
11365
+ }))
11366
+ });
11367
+ logger.info("Rollback changeset saved", { path: rollbackChangesetPath });
11368
+ } catch (maybeError) {
11369
+ summary.failed += 1;
11370
+ handleError(toError(maybeError), verbose);
11371
+ } finally {
11372
+ logger.info("Schema rollback finished", { summary });
11373
+ reporter.addSummary("schemaRollbackResults", summary);
11374
+ reporter.finalize();
11375
+ }
11376
+ });
11377
+
9446
11378
  const program = getProgram();
9447
11379
  konsola.br();
9448
11380
  konsola.br();