storyblok 4.17.0 → 4.17.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -68,7 +68,7 @@ interface StoryblokMultilinkUrl {
68
68
  url: string;
69
69
  full_slug: string;
70
70
  }
71
- interface StoryblokMultilink {
71
+ interface StoryblokMultilinkBase {
72
72
  fieldtype: 'multilink';
73
73
  id: string;
74
74
  url: string;
@@ -78,10 +78,18 @@ interface StoryblokMultilink {
78
78
  rel?: string;
79
79
  title?: string;
80
80
  prep?: string;
81
- linktype: 'story' | 'url' | 'email' | 'asset';
82
- story?: StoryblokMultilinkStory | StoryblokMultilinkLink | StoryblokMultilinkUrl;
83
- email?: string;
84
81
  }
82
+ type StoryblokMultilink = (StoryblokMultilinkBase & {
83
+ linktype: 'story';
84
+ story?: StoryblokMultilinkStory | StoryblokMultilinkLink | StoryblokMultilinkUrl;
85
+ }) | (StoryblokMultilinkBase & {
86
+ linktype: 'url';
87
+ }) | (StoryblokMultilinkBase & {
88
+ linktype: 'email';
89
+ email: string;
90
+ }) | (StoryblokMultilinkBase & {
91
+ linktype: 'asset';
92
+ });
85
93
  interface StoryblokTable {
86
94
  fieldtype: 'table';
87
95
  thead: Array<{
package/dist/index.d.ts CHANGED
@@ -68,7 +68,7 @@ interface StoryblokMultilinkUrl {
68
68
  url: string;
69
69
  full_slug: string;
70
70
  }
71
- interface StoryblokMultilink {
71
+ interface StoryblokMultilinkBase {
72
72
  fieldtype: 'multilink';
73
73
  id: string;
74
74
  url: string;
@@ -78,10 +78,18 @@ interface StoryblokMultilink {
78
78
  rel?: string;
79
79
  title?: string;
80
80
  prep?: string;
81
- linktype: 'story' | 'url' | 'email' | 'asset';
82
- story?: StoryblokMultilinkStory | StoryblokMultilinkLink | StoryblokMultilinkUrl;
83
- email?: string;
84
81
  }
82
+ type StoryblokMultilink = (StoryblokMultilinkBase & {
83
+ linktype: 'story';
84
+ story?: StoryblokMultilinkStory | StoryblokMultilinkLink | StoryblokMultilinkUrl;
85
+ }) | (StoryblokMultilinkBase & {
86
+ linktype: 'url';
87
+ }) | (StoryblokMultilinkBase & {
88
+ linktype: 'email';
89
+ email: string;
90
+ }) | (StoryblokMultilinkBase & {
91
+ linktype: 'asset';
92
+ });
85
93
  interface StoryblokTable {
86
94
  fieldtype: 'table';
87
95
  thead: Array<{
package/dist/index.mjs CHANGED
@@ -578,10 +578,12 @@ function setActiveConfig(config) {
578
578
 
579
579
  class FetchError extends Error {
580
580
  response;
581
- constructor(message, response) {
581
+ request;
582
+ constructor(message, response, request = {}) {
582
583
  super(message);
583
584
  this.name = "FetchError";
584
585
  this.response = response;
586
+ this.request = request;
585
587
  }
586
588
  }
587
589
  const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
@@ -589,6 +591,7 @@ async function customFetch(url, options = {}) {
589
591
  const { api } = getActiveConfig();
590
592
  const maxRetries = options.maxRetries ?? api.maxRetries;
591
593
  const baseDelay = options.baseDelay ?? 500;
594
+ const requestContext = { url, method: options.method ?? "GET" };
592
595
  let attempt = 0;
593
596
  while (attempt <= maxRetries) {
594
597
  try {
@@ -612,7 +615,7 @@ async function customFetch(url, options = {}) {
612
615
  status: response.status,
613
616
  statusText: response.statusText,
614
617
  data: null
615
- });
618
+ }, requestContext);
616
619
  }
617
620
  if (!response.ok) {
618
621
  if (response.status === 429 && attempt < maxRetries) {
@@ -625,7 +628,7 @@ async function customFetch(url, options = {}) {
625
628
  status: response.status,
626
629
  statusText: response.statusText,
627
630
  data
628
- });
631
+ }, requestContext);
629
632
  }
630
633
  return {
631
634
  ...data,
@@ -640,14 +643,14 @@ async function customFetch(url, options = {}) {
640
643
  status: 0,
641
644
  statusText: "Network Error",
642
645
  data: null
643
- });
646
+ }, requestContext);
644
647
  }
645
648
  }
646
649
  throw new FetchError("Max retries exceeded", {
647
650
  status: 429,
648
651
  statusText: "Rate Limit Exceeded",
649
652
  data: null
650
- });
653
+ }, requestContext);
651
654
  }
652
655
 
653
656
  const API_ACTIONS = {
@@ -719,9 +722,14 @@ function handleAPIError(action, error, customMessage) {
719
722
  }
720
723
  const response = error?.response;
721
724
  if (response?.status) {
725
+ const reqCandidate = error?.request;
722
726
  const wrappedError = new FetchError(
723
727
  response.statusText ?? error.message,
724
- { status: response.status, statusText: response.statusText ?? "", data: response.data }
728
+ { status: response.status, statusText: response.statusText ?? "", data: response.data },
729
+ {
730
+ url: typeof reqCandidate?.url === "string" ? reqCandidate.url : void 0,
731
+ method: typeof reqCandidate?.method === "string" ? reqCandidate.method : void 0
732
+ }
725
733
  );
726
734
  const errorId = getErrorId(response.status);
727
735
  throw new APIError(errorId, action, wrappedError, customMessage);
@@ -763,6 +771,8 @@ class APIError extends Error {
763
771
  }
764
772
  }
765
773
  getInfo() {
774
+ const request = this.error?.request;
775
+ const hasRequestContext = Boolean(request && (request.url || request.method));
766
776
  return {
767
777
  name: this.name,
768
778
  message: this.message,
@@ -770,7 +780,8 @@ class APIError extends Error {
770
780
  cause: this.cause,
771
781
  errorId: this.errorId,
772
782
  stack: this.stack,
773
- responseData: this.response?.data
783
+ responseData: this.response?.data,
784
+ ...hasRequestContext ? { request: { url: request.url, method: request.method } } : {}
774
785
  };
775
786
  }
776
787
  }
@@ -1106,7 +1117,7 @@ const isVitest = process.env.VITEST === "true";
1106
1117
  const noopProgressBar = {
1107
1118
  increment: () => {
1108
1119
  },
1109
- setTotal: (_n) => {
1120
+ setTotal: () => {
1110
1121
  },
1111
1122
  stop: () => {
1112
1123
  }
@@ -1190,7 +1201,17 @@ class UI {
1190
1201
  }
1191
1202
  }
1192
1203
  createProgressBar(options) {
1193
- return this.multiBar?.create(0, 0, options) || noopProgressBar;
1204
+ const bar = this.multiBar?.create(0, 0, options);
1205
+ if (!bar) {
1206
+ return noopProgressBar;
1207
+ }
1208
+ return {
1209
+ increment: (count = 1) => bar.increment(count),
1210
+ // cli-progress renders `{eta_formatted}` as "LLs" when total is 0.
1211
+ // Floor at 1 so an empty phase stays a clean 0/1 instead.
1212
+ setTotal: (total) => bar.setTotal(Math.max(total, 1)),
1213
+ stop: () => bar.stop()
1214
+ };
1194
1215
  }
1195
1216
  stopAllProgressBars() {
1196
1217
  this.multiBar?.stop();
@@ -2258,16 +2279,40 @@ const DEFAULT_GROUPS_FILENAME = "groups";
2258
2279
  const DEFAULT_PRESETS_FILENAME = "presets";
2259
2280
  const DEFAULT_TAGS_FILENAME = "tags";
2260
2281
 
2282
+ async function fetchAllPages(fetchFunction, extractDataFunction) {
2283
+ const items = [];
2284
+ let page = 1;
2285
+ while (true) {
2286
+ const { data, response } = await fetchFunction(page);
2287
+ const totalHeader = response.headers.get("total");
2288
+ const fetchedItems = extractDataFunction(data);
2289
+ items.push(...fetchedItems);
2290
+ if (!totalHeader) {
2291
+ return items;
2292
+ }
2293
+ const total = Number(totalHeader);
2294
+ if (Number.isNaN(total) || items.length >= total || fetchedItems.length === 0) {
2295
+ return items;
2296
+ }
2297
+ page++;
2298
+ }
2299
+ }
2300
+
2261
2301
  const fetchComponents = async (spaceId) => {
2262
2302
  try {
2263
2303
  const client = getMapiClient();
2264
- const { data } = await client.components.list({
2265
- path: {
2266
- space_id: Number(spaceId)
2267
- },
2268
- throwOnError: true
2269
- });
2270
- return data?.components;
2304
+ return await fetchAllPages(
2305
+ (page) => client.components.list({
2306
+ path: {
2307
+ space_id: Number(spaceId)
2308
+ },
2309
+ query: {
2310
+ page
2311
+ },
2312
+ throwOnError: true
2313
+ }),
2314
+ (data) => data?.components ?? []
2315
+ );
2271
2316
  } catch (error) {
2272
2317
  handleAPIError("pull_components", error);
2273
2318
  }
@@ -2275,16 +2320,20 @@ const fetchComponents = async (spaceId) => {
2275
2320
  const fetchComponent = async (spaceId, componentName) => {
2276
2321
  try {
2277
2322
  const client = getMapiClient();
2278
- const { data } = await client.components.list({
2279
- path: {
2280
- space_id: Number(spaceId)
2281
- },
2282
- query: {
2283
- search: componentName
2284
- },
2285
- throwOnError: true
2286
- });
2287
- return data?.components?.find((c) => c.name === componentName);
2323
+ const matches = await fetchAllPages(
2324
+ (page) => client.components.list({
2325
+ path: {
2326
+ space_id: Number(spaceId)
2327
+ },
2328
+ query: {
2329
+ page,
2330
+ search: componentName
2331
+ },
2332
+ throwOnError: true
2333
+ }),
2334
+ (data) => data?.components ?? []
2335
+ );
2336
+ return matches.find((c) => c.name === componentName);
2288
2337
  } catch (error) {
2289
2338
  handleAPIError("pull_components", error, `Failed to fetch component ${componentName}`);
2290
2339
  }
@@ -2318,12 +2367,19 @@ const fetchComponentPresets = async (spaceId) => {
2318
2367
  const fetchComponentInternalTags = async (spaceId) => {
2319
2368
  try {
2320
2369
  const client = getMapiClient();
2321
- const { data } = await client.internalTags.list({
2322
- path: {
2323
- space_id: Number(spaceId)
2324
- }
2325
- });
2326
- return data?.internal_tags?.filter((tag) => tag.object_type === "component");
2370
+ return await fetchAllPages(
2371
+ (page) => client.internalTags.list({
2372
+ path: {
2373
+ space_id: Number(spaceId)
2374
+ },
2375
+ query: {
2376
+ page,
2377
+ by_object_type: "component"
2378
+ },
2379
+ throwOnError: true
2380
+ }),
2381
+ (data) => data?.internal_tags ?? []
2382
+ );
2327
2383
  } catch (error) {
2328
2384
  handleAPIError("pull_component_internal_tags", error);
2329
2385
  }
@@ -6104,20 +6160,6 @@ generateCmd.action(async (options, command) => {
6104
6160
  const program$6 = getProgram();
6105
6161
  const datasourcesCommand = program$6.command(commands.DATASOURCES).alias("ds").description(`Manage your space's datasources`);
6106
6162
 
6107
- async function fetchAllPages(fetchFunction, extractDataFunction, page = 1, collectedItems = []) {
6108
- const { data, response } = await fetchFunction(page);
6109
- const totalHeader = response.headers.get("total");
6110
- const total = Number(totalHeader);
6111
- const fetchedItems = extractDataFunction(data);
6112
- const allItems = [...collectedItems, ...fetchedItems];
6113
- if (!totalHeader || Number.isNaN(total)) {
6114
- return allItems;
6115
- }
6116
- if (allItems.length < total && fetchedItems.length > 0) {
6117
- return fetchAllPages(fetchFunction, extractDataFunction, page + 1, allItems);
6118
- }
6119
- return allItems;
6120
- }
6121
6163
  const fetchDatasourceEntries = async (spaceId, datasourceId) => {
6122
6164
  try {
6123
6165
  const client = getMapiClient();
@@ -6170,16 +6212,20 @@ const fetchDatasources = async (spaceId) => {
6170
6212
  const fetchDatasource = async (spaceId, datasourceName) => {
6171
6213
  try {
6172
6214
  const client = getMapiClient();
6173
- const { data } = await client.datasources.list({
6174
- path: {
6175
- space_id: Number(spaceId)
6176
- },
6177
- query: {
6178
- search: datasourceName
6179
- },
6180
- throwOnError: true
6181
- });
6182
- const found = data.datasources?.find((d) => d.name === datasourceName);
6215
+ const matches = await fetchAllPages(
6216
+ (page) => client.datasources.list({
6217
+ path: {
6218
+ space_id: Number(spaceId)
6219
+ },
6220
+ query: {
6221
+ page,
6222
+ search: datasourceName
6223
+ },
6224
+ throwOnError: true
6225
+ }),
6226
+ (data) => data.datasources || []
6227
+ );
6228
+ const found = matches.find((d) => d.name === datasourceName);
6183
6229
  if (!found) {
6184
6230
  return void 0;
6185
6231
  }
@@ -8047,13 +8093,6 @@ const fetchStoryStream = ({
8047
8093
  }
8048
8094
  });
8049
8095
  };
8050
- const getUUIDFromFilename = (filename) => {
8051
- const uuid = basename(filename, extname(filename)).split("_").at(-1);
8052
- if (!uuid) {
8053
- throw new Error(`Unable to extract UUID from local story "${filename}"`);
8054
- }
8055
- return uuid;
8056
- };
8057
8096
  const readLocalStoriesStream = ({
8058
8097
  directoryPath,
8059
8098
  fileFilter = () => true,
@@ -8063,7 +8102,7 @@ const readLocalStoriesStream = ({
8063
8102
  onStoryError
8064
8103
  }) => {
8065
8104
  const listGenerator = async function* localStoryIterator() {
8066
- const files = (await readDirectory(directoryPath)).filter((f) => extname(f) === ".json" && fileFilter({ uuid: getUUIDFromFilename(f) }));
8105
+ const files = (await readDirectory(directoryPath)).filter((f) => extname(f) === ".json" && fileFilter({ filename: f }));
8067
8106
  setTotalStories?.(files.length);
8068
8107
  for (const file of files) {
8069
8108
  try {
@@ -8131,10 +8170,13 @@ const scanLocalStoryIndex = async ({
8131
8170
  const filePath = join(directoryPath, file);
8132
8171
  const fileContent = await readFile$1(filePath, "utf-8");
8133
8172
  const story = JSON.parse(fileContent);
8173
+ if (!story.uuid) {
8174
+ throw new Error(`Story "${file}" is missing a uuid and cannot be pushed.`);
8175
+ }
8134
8176
  entries.push({
8135
8177
  filename: file,
8136
8178
  id: story.id,
8137
- uuid: story.uuid ?? "",
8179
+ uuid: story.uuid,
8138
8180
  slug: story.slug ?? "",
8139
8181
  name: story.name ?? "",
8140
8182
  full_slug: story.full_slug ?? "",
@@ -8968,6 +9010,105 @@ pullCmd.action(async (options, command) => {
8968
9010
  }
8969
9011
  });
8970
9012
 
9013
+ function formatStoryIdentifier(record) {
9014
+ const title = record.full_slug ?? record.slug ?? record.uuid ?? record.id?.toString() ?? record.filename ?? "unknown story";
9015
+ const extras = [];
9016
+ if (record.uuid && record.uuid !== title) {
9017
+ extras.push(`uuid: ${record.uuid}`);
9018
+ }
9019
+ if (record.filename && record.filename !== title) {
9020
+ extras.push(`file: ${record.filename}`);
9021
+ }
9022
+ return extras.length > 0 ? `${title} (${extras.join(", ")})` : title;
9023
+ }
9024
+ const ENRICHABLE_FIELDS = ["full_slug", "slug", "uuid", "id", "filename"];
9025
+ function enrichFieldMessage(detail, record) {
9026
+ const match = detail.match(/^(\w+):/);
9027
+ if (!match) {
9028
+ return detail;
9029
+ }
9030
+ const field = match[1];
9031
+ if (!ENRICHABLE_FIELDS.includes(field)) {
9032
+ return detail;
9033
+ }
9034
+ const value = record[field];
9035
+ if (value == null || value === "" || value instanceof Error) {
9036
+ return detail;
9037
+ }
9038
+ return `${field} (${String(value)})${detail.slice(field.length)}`;
9039
+ }
9040
+ function renderFailureReport(ui, records, verbose) {
9041
+ if (records.length === 0) {
9042
+ return;
9043
+ }
9044
+ ui.error(`Failed stories (${records.length}):`, void 0, { header: false, margin: false });
9045
+ const lines = [];
9046
+ for (const record of records) {
9047
+ lines.push(formatStoryIdentifier(record));
9048
+ const messages = record.error instanceof APIError ? record.error.messageStack : [record.error.message];
9049
+ const [primary, ...rest] = messages;
9050
+ lines.push(` \u2022 ${enrichFieldMessage(primary ?? "Unknown error", record)}`);
9051
+ for (const detail of rest) {
9052
+ lines.push(` \u2514\u2500 ${enrichFieldMessage(detail, record)}`);
9053
+ }
9054
+ if (verbose && record.error.stack) {
9055
+ for (const frame of record.error.stack.split("\n")) {
9056
+ lines.push(` ${frame}`);
9057
+ }
9058
+ }
9059
+ }
9060
+ ui.list(lines);
9061
+ ui.br();
9062
+ if (!verbose) {
9063
+ ui.info("Re-run with the `--verbose` flag for full stack traces.", { margin: false });
9064
+ }
9065
+ }
9066
+ class FailureCollector {
9067
+ records = /* @__PURE__ */ new Map();
9068
+ keyFor(story) {
9069
+ return story.full_slug ?? story.uuid ?? story.filename ?? `__unknown_${this.records.size}`;
9070
+ }
9071
+ /**
9072
+ * Record a failure. Returns `true` if this is the first failure seen for
9073
+ * this story's identity, `false` if one was already recorded (in which case
9074
+ * the caller should skip counter updates to avoid double-billing).
9075
+ */
9076
+ record(story, error) {
9077
+ const key = this.keyFor(story);
9078
+ if (this.records.has(key)) {
9079
+ return false;
9080
+ }
9081
+ this.records.set(key, {
9082
+ filename: story.filename,
9083
+ full_slug: story.full_slug,
9084
+ slug: story.slug,
9085
+ uuid: story.uuid,
9086
+ id: story.id,
9087
+ error
9088
+ });
9089
+ return true;
9090
+ }
9091
+ get isEmpty() {
9092
+ return this.records.size === 0;
9093
+ }
9094
+ get size() {
9095
+ return this.records.size;
9096
+ }
9097
+ render(ui, verbose = false) {
9098
+ renderFailureReport(ui, [...this.records.values()], verbose);
9099
+ }
9100
+ toReporterMeta() {
9101
+ return [...this.records.values()].map((r) => ({
9102
+ filename: r.filename,
9103
+ full_slug: r.full_slug,
9104
+ slug: r.slug,
9105
+ uuid: r.uuid,
9106
+ id: r.id,
9107
+ error: r.error.message
9108
+ }));
9109
+ }
9110
+ }
9111
+
8971
9112
  const pushCmd = storiesCommand.command("push").option("-s, --space <space>", "space ID").option("-f, --from <from>", "source space id").option("-d, --dry-run", "Preview changes without applying them to Storyblok").option("--publish", "Publish stories after pushing").option("--cleanup", "delete local stories after a successful push (note: does not cleanup manifests)").description(`Push local stories to a Storyblok space.`);
8972
9113
  pushCmd.action(async (options, command) => {
8973
9114
  const ui = getUI();
@@ -8991,6 +9132,7 @@ pushCmd.action(async (options, command) => {
8991
9132
  return;
8992
9133
  }
8993
9134
  const pendingWarnings = [];
9135
+ const failures = new FailureCollector();
8994
9136
  const summary = {
8995
9137
  creationResults: { total: 0, succeeded: 0, skipped: 0, failed: 0 },
8996
9138
  processResults: { total: 0, succeeded: 0, failed: 0 },
@@ -9064,8 +9206,10 @@ pushCmd.action(async (options, command) => {
9064
9206
  scanProgress.increment();
9065
9207
  },
9066
9208
  onError(error, filename) {
9067
- summary.creationResults.failed += 1;
9068
- handleError(error, verbose, { storyFile: filename });
9209
+ if (failures.record({ filename }, error)) {
9210
+ summary.creationResults.failed += 1;
9211
+ }
9212
+ logOnlyError(error, { storyFile: filename });
9069
9213
  }
9070
9214
  });
9071
9215
  const levels = groupStoriesByDepth(storyIndex);
@@ -9093,8 +9237,11 @@ pushCmd.action(async (options, command) => {
9093
9237
  dryRun: options.dryRun ?? false,
9094
9238
  appendToManifest,
9095
9239
  onStorySuccess(entry, remoteStory) {
9096
- if (!entry.uuid || !remoteStory.uuid) {
9097
- throw new Error("Invalid story provided!");
9240
+ if (!entry.uuid) {
9241
+ throw new Error(`Local story file "${entry.filename}" is missing a "uuid" field. Re-pull the story or add the uuid manually.`);
9242
+ }
9243
+ if (!remoteStory.uuid) {
9244
+ throw new Error(`Storyblok API returned a story without a uuid for slug "${entry.slug}".`);
9098
9245
  }
9099
9246
  maps.stories.set(entry.id, remoteStory.id);
9100
9247
  maps.stories.set(entry.uuid, remoteStory.uuid);
@@ -9103,8 +9250,11 @@ pushCmd.action(async (options, command) => {
9103
9250
  creationProgress.increment();
9104
9251
  },
9105
9252
  onStorySkipped(entry, remoteStory, reason) {
9106
- if (!entry.uuid || !remoteStory.uuid) {
9107
- throw new Error("Invalid story provided!");
9253
+ if (!entry.uuid) {
9254
+ throw new Error(`Local story file "${entry.filename}" is missing a "uuid" field. Re-pull the story or add the uuid manually.`);
9255
+ }
9256
+ if (!remoteStory.uuid) {
9257
+ throw new Error(`Storyblok API returned a story without a uuid for slug "${entry.slug}".`);
9108
9258
  }
9109
9259
  maps.stories.set(entry.id, remoteStory.id);
9110
9260
  maps.stories.set(entry.uuid, remoteStory.uuid);
@@ -9113,13 +9263,15 @@ pushCmd.action(async (options, command) => {
9113
9263
  creationProgress.increment();
9114
9264
  },
9115
9265
  onStoryError(error, entry) {
9116
- summary.creationResults.failed += 1;
9117
- summary.processResults.total -= 1;
9118
- summary.updateResults.total -= 1;
9119
- processProgress.setTotal(summary.processResults.total);
9120
- updateProgress.setTotal(summary.updateResults.total);
9266
+ if (failures.record(entry, error)) {
9267
+ summary.creationResults.failed += 1;
9268
+ summary.processResults.total -= 1;
9269
+ summary.updateResults.total -= 1;
9270
+ processProgress.setTotal(summary.processResults.total);
9271
+ updateProgress.setTotal(summary.updateResults.total);
9272
+ }
9121
9273
  creationProgress.increment();
9122
- handleError(error, verbose, { storyId: entry?.uuid });
9274
+ logOnlyError(error, { storyId: entry.uuid });
9123
9275
  }
9124
9276
  });
9125
9277
  }
@@ -9128,12 +9280,14 @@ pushCmd.action(async (options, command) => {
9128
9280
  pendingWarnings.push(message);
9129
9281
  logger.warn(message);
9130
9282
  }
9283
+ const uuidByFilename = new Map(storyIndex.map((entry) => [entry.filename, entry.uuid]));
9131
9284
  await pipeline$1(
9132
9285
  // Read local stories from `.json` files.
9133
9286
  readLocalStoriesStream({
9134
9287
  directoryPath: storiesDirectoryPath,
9135
- fileFilter({ uuid }) {
9136
- return Boolean(maps.stories.get(uuid));
9288
+ fileFilter({ filename }) {
9289
+ const uuid = uuidByFilename.get(filename);
9290
+ return uuid != null && Boolean(maps.stories.get(uuid));
9137
9291
  },
9138
9292
  setTotalStories(total) {
9139
9293
  summary.processResults.total = total;
@@ -9142,12 +9296,15 @@ pushCmd.action(async (options, command) => {
9142
9296
  updateProgress.setTotal(total);
9143
9297
  },
9144
9298
  onStoryError(error, filename) {
9145
- summary.creationResults.failed += 1;
9146
- summary.processResults.total -= 1;
9299
+ if (failures.record({ filename }, error)) {
9300
+ summary.processResults.failed += 1;
9301
+ } else {
9302
+ summary.processResults.total -= 1;
9303
+ processProgress.setTotal(summary.processResults.total);
9304
+ }
9147
9305
  summary.updateResults.total -= 1;
9148
- processProgress.setTotal(summary.processResults.total);
9149
9306
  updateProgress.setTotal(summary.updateResults.total);
9150
- handleError(error, verbose, { storyFile: filename });
9307
+ logOnlyError(error, { storyFile: filename });
9151
9308
  }
9152
9309
  }),
9153
9310
  // Map all references to numeric ids and uuids.
@@ -9163,10 +9320,15 @@ pushCmd.action(async (options, command) => {
9163
9320
  summary.processResults.succeeded += 1;
9164
9321
  },
9165
9322
  onStoryError(error, localStory) {
9166
- summary.processResults.failed += 1;
9323
+ logOnlyError(error, { storyId: localStory.uuid });
9324
+ if (failures.record(localStory, error)) {
9325
+ summary.processResults.failed += 1;
9326
+ } else {
9327
+ summary.processResults.total -= 1;
9328
+ processProgress.setTotal(summary.processResults.total);
9329
+ }
9167
9330
  summary.updateResults.total -= 1;
9168
9331
  updateProgress.setTotal(summary.updateResults.total);
9169
- handleError(error, verbose, { storyId: localStory.uuid });
9170
9332
  }
9171
9333
  }),
9172
9334
  // Update remote stories with correct references.
@@ -9186,8 +9348,13 @@ pushCmd.action(async (options, command) => {
9186
9348
  summary.updateResults.succeeded += 1;
9187
9349
  },
9188
9350
  onStoryError(error, localStory) {
9189
- summary.updateResults.failed += 1;
9190
- handleError(error, verbose, { storyId: localStory.uuid });
9351
+ logOnlyError(error, { storyId: localStory.uuid });
9352
+ if (failures.record(localStory, error)) {
9353
+ summary.updateResults.failed += 1;
9354
+ } else {
9355
+ summary.updateResults.total -= 1;
9356
+ updateProgress.setTotal(summary.updateResults.total);
9357
+ }
9191
9358
  }
9192
9359
  })
9193
9360
  );
@@ -9196,19 +9363,30 @@ pushCmd.action(async (options, command) => {
9196
9363
  } finally {
9197
9364
  logger.info("Pushing stories finished", summary);
9198
9365
  ui.stopAllProgressBars();
9199
- for (const warning of pendingWarnings) {
9200
- ui.warn(warning);
9201
- }
9202
- const failedStories = Math.max(summary.creationResults.failed, summary.processResults.failed, summary.updateResults.failed);
9203
- ui.info(`Push results: ${summary.creationResults.total} ${summary.creationResults.total === 1 ? "story" : "stories"} pushed, ${failedStories} ${failedStories === 1 ? "story" : "stories"} failed`);
9366
+ ui.br();
9367
+ const failedCount = failures.size;
9368
+ ui.info(`Push results: ${summary.creationResults.total} ${summary.creationResults.total === 1 ? "story" : "stories"} pushed, ${failedCount} ${failedCount === 1 ? "story" : "stories"} failed`);
9204
9369
  ui.list([
9205
9370
  `Creating stories: ${summary.creationResults.succeeded + summary.creationResults.skipped}/${summary.creationResults.total} succeeded, ${summary.creationResults.failed} failed.`,
9206
9371
  `Processing stories: ${summary.processResults.succeeded}/${summary.processResults.total} succeeded, ${summary.processResults.failed} failed.`,
9207
9372
  `Updating stories: ${summary.updateResults.succeeded}/${summary.updateResults.total} succeeded, ${summary.updateResults.failed} failed.`
9208
9373
  ]);
9374
+ if (pendingWarnings.length > 0 || !failures.isEmpty) {
9375
+ ui.br();
9376
+ }
9377
+ for (const warning of pendingWarnings) {
9378
+ ui.warn(warning);
9379
+ }
9380
+ if (pendingWarnings.length > 0 && !failures.isEmpty) {
9381
+ ui.br();
9382
+ }
9383
+ failures.render(ui, verbose);
9209
9384
  reporter.addSummary("creationResults", summary.creationResults);
9210
9385
  reporter.addSummary("processResults", summary.processResults);
9211
9386
  reporter.addSummary("updateResults", summary.updateResults);
9387
+ if (!failures.isEmpty) {
9388
+ reporter.addMeta("failedStories", failures.toReporterMeta());
9389
+ }
9212
9390
  reporter.finalize();
9213
9391
  }
9214
9392
  });