@kubb/studio 5.10.1 → 5.10.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/README.md CHANGED
@@ -75,6 +75,7 @@ running in a developer's project and a container running a fixed plugin set.
75
75
  | `pair.ts` | `pairAgent`: RFC 8628 device pairing. |
76
76
  | `snapshot.ts` | `createSnapshot` for CI, and packing a generation into an npm tarball. |
77
77
  | `generations.ts` | Running a generation and keeping its files and AST for `readFiles`/`readInput`. |
78
+ | `diff.ts` | The paged line diff `readDiff` serves between two kept sets. |
78
79
  | `configFile.ts` | Reading and patching plugin options in `kubb.config.ts`. |
79
80
 
80
81
  ## Connection
package/dist/index.cjs CHANGED
@@ -36,6 +36,7 @@ let _kubb_core = require("@kubb/core");
36
36
  let capnweb = require("capnweb");
37
37
  let magicast = require("magicast");
38
38
  let node_module = require("node:module");
39
+ let diff = require("diff");
39
40
  let node_util = require("node:util");
40
41
  let _kubb_ast = require("@kubb/ast");
41
42
  let ofetch = require("ofetch");
@@ -177,7 +178,7 @@ function getElapsedMs(hrStart) {
177
178
  }
178
179
  //#endregion
179
180
  //#region package.json
180
- var version = "5.10.1";
181
+ var version = "5.10.2";
181
182
  //#endregion
182
183
  //#region src/resolveConfig.ts
183
184
  /**
@@ -279,22 +280,42 @@ function isAllowedPlugin({ packageName, extensions }) {
279
280
  *
280
281
  * @throws if the package cannot be imported or exports no callable factory.
281
282
  */
282
- async function loadFactory({ packageName, extensions }) {
283
+ async function loadFactory({ packageName, extensions, kind = "plugin" }) {
284
+ const label = {
285
+ plugin: "Plugin",
286
+ adapter: "Adapter",
287
+ parser: "Parser"
288
+ }[kind];
283
289
  let mod;
284
290
  try {
285
291
  mod = await importFromProject(packageName);
286
292
  } catch (cause) {
287
- throw new Error(`Plugin "${packageName}" could not be loaded. Make sure it is installed: \`npm install ${packageName}\``, { cause });
293
+ throw new _kubb_core.Diagnostics.Error({
294
+ code: kind === "plugin" ? _kubb_core.Diagnostics.code.pluginNotFound : _kubb_core.Diagnostics.code.unknown,
295
+ severity: "error",
296
+ message: `${label} "${packageName}" could not be loaded. Make sure it is installed: \`npm install ${packageName}\``,
297
+ cause: toError(cause)
298
+ });
288
299
  }
289
300
  const manifestExport = extensions.get(packageName)?.manifest.export;
290
301
  if (manifestExport) {
291
302
  if (typeof mod[manifestExport] === "function") return mod[manifestExport];
292
- throw new Error(`Plugin "${packageName}" does not export "${manifestExport}", the factory its kubb-extension.json names.`);
303
+ throw new _kubb_core.Diagnostics.Error({
304
+ code: _kubb_core.Diagnostics.code.incompatibleExtension,
305
+ severity: "error",
306
+ message: `${label} "${packageName}" does not export "${manifestExport}", the factory its kubb-extension.json names.`,
307
+ help: "Install a version of the plugin whose exports match its kubb-extension.json."
308
+ });
293
309
  }
294
310
  const exportName = toExportName(packageName);
295
311
  if (typeof mod[exportName] === "function") return mod[exportName];
296
312
  if (typeof mod["default"] === "function") return mod["default"];
297
- throw new Error(`Plugin "${packageName}" does not export a callable factory. Tried the named export "${exportName}" and "default".`);
313
+ throw new _kubb_core.Diagnostics.Error({
314
+ code: _kubb_core.Diagnostics.code.incompatibleExtension,
315
+ severity: "error",
316
+ message: `${label} "${packageName}" does not export a callable factory. Tried the named export "${exportName}" and "default".`,
317
+ help: "Install a Kubb plugin package that exports its factory."
318
+ });
298
319
  }
299
320
  /**
300
321
  * Resolves each plugin entry by dynamically importing its package and calling its factory with the
@@ -315,7 +336,11 @@ async function resolvePlugins({ plugins, extensions = /* @__PURE__ */ new Map()
315
336
  if (!isAllowedPlugin({
316
337
  packageName: name,
317
338
  extensions
318
- })) throw new Error(`Plugin "${name}" is not an installed Kubb plugin. Studio loads @kubb/plugin-* packages and installed packages that ship a kubb-extension.json with kind "plugin".`);
339
+ })) throw new _kubb_core.Diagnostics.Error({
340
+ code: _kubb_core.Diagnostics.code.pluginNotFound,
341
+ severity: "error",
342
+ message: `Plugin "${name}" is not an installed Kubb plugin. Studio loads @kubb/plugin-* packages and installed packages that ship a kubb-extension.json with kind "plugin".`
343
+ });
319
344
  return (await loadFactory({
320
345
  packageName: name,
321
346
  extensions
@@ -442,7 +467,12 @@ async function mergePlugins({ diskPlugins, studioPlugins, extensions = /* @__PUR
442
467
  extensions
443
468
  }).catch((cause) => {
444
469
  if (unchanged) return [diskPlugin];
445
- throw new Error(`Plugin "${diskPlugin.name}" is not an installed @kubb/plugin-* package or Kubb extension, so Studio cannot change its options. Edit it in the config file, or only enable or disable it from Studio.`, { cause });
470
+ throw new _kubb_core.Diagnostics.Error({
471
+ code: _kubb_core.Diagnostics.code.pluginNotFound,
472
+ severity: "error",
473
+ message: `Plugin "${diskPlugin.name}" is not an installed @kubb/plugin-* package or Kubb extension, so Studio cannot change its options. Edit it in the config file, or only enable or disable it from Studio.`,
474
+ cause: toError(cause)
475
+ });
446
476
  });
447
477
  return resolved ?? diskPlugin;
448
478
  }));
@@ -466,9 +496,15 @@ async function rebuildExtension({ disk, studioOptions, kind, extensions }) {
466
496
  const { name } = (0, _kubb_core.resolveMeta)(disk);
467
497
  return (await loadFactory({
468
498
  packageName: [...extensions.values()].find(({ manifest }) => manifest.kind === kind && manifest.name === name)?.package ?? `@kubb/${kind}-${name}`,
469
- extensions
499
+ extensions,
500
+ kind
470
501
  }).catch((cause) => {
471
- throw new Error(`The ${kind} "${name}" is not an installed Kubb extension, so Studio cannot change its options. Edit it in the config file.`, { cause });
502
+ throw new _kubb_core.Diagnostics.Error({
503
+ code: kind === "plugin" ? _kubb_core.Diagnostics.code.pluginNotFound : _kubb_core.Diagnostics.code.unknown,
504
+ severity: "error",
505
+ message: `The ${kind} "${name}" is not an installed Kubb extension, so Studio cannot change its options. Edit it in the config file.`,
506
+ cause: toError(cause)
507
+ });
472
508
  }))(mergeOptions(disk.options ?? {}, studioOptions));
473
509
  }
474
510
  /**
@@ -545,7 +581,11 @@ function invalidEdit(edit) {
545
581
  }
546
582
  /** Where a node sits in the source text. */
547
583
  function rangeOf(node) {
548
- if (typeof node.start !== "number" || typeof node.end !== "number") throw new Error("the config file could not be mapped back to its source text");
584
+ if (typeof node.start !== "number" || typeof node.end !== "number") throw new _kubb_core.Diagnostics.Error({
585
+ code: _kubb_core.Diagnostics.code.internalError,
586
+ severity: "error",
587
+ message: "the config file could not be mapped back to its source text"
588
+ });
549
589
  return {
550
590
  start: node.start,
551
591
  end: node.end
@@ -797,6 +837,44 @@ function disabledBlocks(source, config) {
797
837
  }];
798
838
  });
799
839
  }
840
+ /**
841
+ * Reads an object option that holds some code: its literal members as `value`, and the paths of
842
+ * the members that are code as `locked`. `set` can write any path outside `locked`, so Studio
843
+ * edits those fields. `undefined` for a computed key, since it could set any path.
844
+ */
845
+ function readPartial(node, prefix = []) {
846
+ const value = {};
847
+ const locked = [];
848
+ for (const [index, entry] of node.properties.entries()) {
849
+ if (entry.type === "SpreadElement") continue;
850
+ const key = memberKey(entry);
851
+ if (key === void 0) return void 0;
852
+ if (findMember(node, key)?.index !== index) continue;
853
+ const path = [...prefix, key];
854
+ if (!isSafeKey(key)) {
855
+ locked.push(path);
856
+ continue;
857
+ }
858
+ const member = entry.type === "ObjectProperty" && !isShadowed(node, index) ? entry.value : void 0;
859
+ const literal = readLiteral(member);
860
+ if (literal !== void 0) {
861
+ value[key] = literal;
862
+ continue;
863
+ }
864
+ if (member?.type !== "ObjectExpression") {
865
+ locked.push(path);
866
+ continue;
867
+ }
868
+ const nested = readPartial(member, path);
869
+ if (!nested) return void 0;
870
+ value[key] = nested.value;
871
+ locked.push(...nested.locked);
872
+ }
873
+ return {
874
+ value,
875
+ locked
876
+ };
877
+ }
800
878
  /** The top-level options of a plugin call, each flagged with whether Studio may write it. */
801
879
  function readOptions(call) {
802
880
  const entries = {};
@@ -805,10 +883,15 @@ function readOptions(call) {
805
883
  for (const [index, entry] of options.properties.entries()) {
806
884
  const key = memberKey(entry);
807
885
  if (key === void 0 || !isSafeKey(key)) continue;
808
- const value = entry.type === "ObjectProperty" && !isShadowed(options, index) ? readLiteral(entry.value) : void 0;
809
- entries[key] = value === void 0 ? { literal: false } : {
886
+ const member = entry.type === "ObjectProperty" && !isShadowed(options, index) ? entry.value : void 0;
887
+ const value = readLiteral(member);
888
+ const partial = value === void 0 && member?.type === "ObjectExpression" ? readPartial(member) : void 0;
889
+ entries[key] = value !== void 0 ? {
810
890
  literal: true,
811
891
  value
892
+ } : {
893
+ literal: false,
894
+ ...partial
812
895
  };
813
896
  }
814
897
  return entries;
@@ -1094,6 +1177,38 @@ async function writeConfigEdits({ filePath, edits, extensions }) {
1094
1177
  return result;
1095
1178
  }
1096
1179
  //#endregion
1180
+ //#region src/diff.ts
1181
+ const CONTEXT_LINES = 3;
1182
+ const DIFF_TIMEOUT_MS = 1e4;
1183
+ function diffHunks(before, after) {
1184
+ return new Promise((resolve) => {
1185
+ (0, diff.structuredPatch)("", "", before, after, void 0, void 0, {
1186
+ context: CONTEXT_LINES,
1187
+ timeout: DIFF_TIMEOUT_MS,
1188
+ callback: (patch) => resolve(patch?.hunks)
1189
+ });
1190
+ });
1191
+ }
1192
+ function pageHunks({ hunks, cursor, limit }) {
1193
+ const page = [];
1194
+ const end = cursor + limit;
1195
+ let start = 0;
1196
+ for (const [index, hunk] of hunks.entries()) {
1197
+ const next = start + hunk.lines.length;
1198
+ if (next > cursor && start < end) page.push({
1199
+ index,
1200
+ oldStart: hunk.oldStart,
1201
+ newStart: hunk.newStart,
1202
+ lines: hunk.lines.slice(Math.max(0, cursor - start), end - start)
1203
+ });
1204
+ start = next;
1205
+ }
1206
+ return {
1207
+ hunks: page,
1208
+ nextCursor: end < start ? end : null
1209
+ };
1210
+ }
1211
+ //#endregion
1097
1212
  //#region src/generations.ts
1098
1213
  const READ_CONCURRENCY = 50;
1099
1214
  const MB = 1048576;
@@ -1223,8 +1338,8 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
1223
1338
  if (path.split("/").includes("..")) return;
1224
1339
  const content = await files.storage.readItem((0, node_path.resolve)(files.root, path));
1225
1340
  if (content === null) return;
1226
- if (source === "output" && !content.trim()) return;
1227
- hashes[path] = hashOf(source === "output" ? `${content.trim()}\n` : content);
1341
+ if (source === require_protocol.FileSource.OUTPUT && !content.trim()) return;
1342
+ hashes[path] = hashOf(source === require_protocol.FileSource.OUTPUT ? `${content.trim()}\n` : content);
1228
1343
  bytes += Buffer.byteLength(content);
1229
1344
  if (bytes <= maxSetBytes) await storage.writeItem(`${dirOf(jobId)}${source}/${path}`, content);
1230
1345
  }
@@ -1522,7 +1637,11 @@ function isLoopbackHost(hostname) {
1522
1637
  */
1523
1638
  function secureUrl({ studioUrl, path, socket = false }) {
1524
1639
  const url = new URL(`${studioUrl.replace(/\/+$/, "")}${path}`);
1525
- if (url.protocol !== "https:" && !(url.protocol === "http:" && isLoopbackHost(url.hostname))) throw new Error(`Refusing to reach Kubb Studio at ${url.origin}: use an https:// URL, or a loopback host for local development`);
1640
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && isLoopbackHost(url.hostname))) throw new _kubb_core.Diagnostics.Error({
1641
+ code: _kubb_core.Diagnostics.code.unknown,
1642
+ severity: "error",
1643
+ message: `Refusing to reach Kubb Studio at ${url.origin}: use an https:// URL, or a loopback host for local development`
1644
+ });
1526
1645
  if (socket) url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
1527
1646
  return url;
1528
1647
  }
@@ -1592,6 +1711,7 @@ var Agent = class extends capnweb.RpcTarget {
1592
1711
  #session;
1593
1712
  #limits = resolveGenerationLimits();
1594
1713
  #store;
1714
+ #diffs = /* @__PURE__ */ new Map();
1595
1715
  constructor({ options, session }) {
1596
1716
  super();
1597
1717
  this.#options = options;
@@ -1630,10 +1750,18 @@ var Agent = class extends capnweb.RpcTarget {
1630
1750
  message,
1631
1751
  permission
1632
1752
  });
1633
- throw new Error(message);
1753
+ throw new _kubb_core.Diagnostics.Error({
1754
+ code: _kubb_core.Diagnostics.code.unknown,
1755
+ severity: "error",
1756
+ message
1757
+ });
1634
1758
  }
1635
1759
  #assertConnected() {
1636
- if (!this.#session.info) throw new Error("Studio must call connect() before any other command");
1760
+ if (!this.#session.info) throw new _kubb_core.Diagnostics.Error({
1761
+ code: _kubb_core.Diagnostics.code.internalError,
1762
+ severity: "error",
1763
+ message: "Studio must call connect() before any other command"
1764
+ });
1637
1765
  }
1638
1766
  #logCommand(command, info) {
1639
1767
  return emit(this.#options.hooks, "studio:command", {
@@ -1730,7 +1858,11 @@ var Agent = class extends capnweb.RpcTarget {
1730
1858
  /** Runs one job and keeps its output, and its input with `allowRead`, for later reads. */
1731
1859
  async #runGeneration(input, controller) {
1732
1860
  this.#assertConnected();
1733
- if (this.#session.closed) throw new Error("Connection is closed");
1861
+ if (this.#session.closed) throw new _kubb_core.Diagnostics.Error({
1862
+ code: _kubb_core.Diagnostics.code.internalError,
1863
+ severity: "error",
1864
+ message: "Connection is closed"
1865
+ });
1734
1866
  if (this.#session.active) return this.#refuse({ message: "A generation is already in progress, please wait for it to finish" });
1735
1867
  const settled = Promise.withResolvers();
1736
1868
  this.#session.active = {
@@ -1788,7 +1920,7 @@ var Agent = class extends capnweb.RpcTarget {
1788
1920
  }) : void 0;
1789
1921
  const disk = diskFiles ? await this.#generations.keep({
1790
1922
  jobId,
1791
- source: "disk",
1923
+ source: require_protocol.FileSource.DISK,
1792
1924
  files: diskFiles,
1793
1925
  maxSetMb: this.#limits.maxSnapshotMb
1794
1926
  }) : void 0;
@@ -1811,7 +1943,7 @@ var Agent = class extends capnweb.RpcTarget {
1811
1943
  });
1812
1944
  const output = await this.#generations.keep({
1813
1945
  jobId,
1814
- source: "output",
1946
+ source: require_protocol.FileSource.OUTPUT,
1815
1947
  files,
1816
1948
  maxSetMb: this.#limits.maxMb
1817
1949
  });
@@ -1883,14 +2015,14 @@ var Agent = class extends capnweb.RpcTarget {
1883
2015
  async readInput(input) {
1884
2016
  this.#assertConnected();
1885
2017
  if (!this.#permissions.allowReadAst) return this.#refuse({
1886
- message: require_protocol.READ_AST_NOT_ALLOWED_MESSAGE,
1887
- permission: "allowReadAst"
2018
+ message: require_protocol.AgentMessage.READ_AST_NOT_ALLOWED,
2019
+ permission: require_protocol.AgentPermission.READ_AST
1888
2020
  });
1889
2021
  if (typeof input.jobId !== "string" || !input.jobId) return this.#refuse({ message: "The request must name a job" });
1890
2022
  const generation = await this.#generations.get(input.jobId);
1891
- if (!generation) return this.#refuse({ message: require_protocol.GENERATION_GONE_MESSAGE });
2023
+ if (!generation) return this.#refuse({ message: require_protocol.AgentMessage.GENERATION_GONE });
1892
2024
  const kept = generation.inputBytes ? await this.#generations.readInput(input.jobId) : void 0;
1893
- if (!kept) return this.#refuse({ message: require_protocol.NO_INPUT_MESSAGE });
2025
+ if (!kept) return this.#refuse({ message: require_protocol.AgentMessage.NO_INPUT });
1894
2026
  await this.#logCommand("readInput", "read the API input");
1895
2027
  return { input: kept };
1896
2028
  }
@@ -1900,16 +2032,16 @@ var Agent = class extends capnweb.RpcTarget {
1900
2032
  async readFiles(input) {
1901
2033
  this.#assertConnected();
1902
2034
  if (!this.#permissions.allowRead) return this.#refuse({
1903
- message: require_protocol.READ_NOT_ALLOWED_MESSAGE,
1904
- permission: "allowRead"
2035
+ message: require_protocol.AgentMessage.READ_NOT_ALLOWED,
2036
+ permission: require_protocol.AgentPermission.READ
1905
2037
  });
1906
2038
  if (!Array.isArray(input.paths) || typeof input.jobId !== "string" || !input.jobId) return this.#refuse({ message: "The request must name a job and carry paths" });
1907
2039
  const { paths } = input;
1908
2040
  if (paths.length > 50) return this.#refuse({ message: `At most 50 paths may be requested at once` });
1909
2041
  const generation = await this.#generations.get(input.jobId);
1910
- if (!generation) return this.#refuse({ message: require_protocol.GENERATION_GONE_MESSAGE });
2042
+ if (!generation) return this.#refuse({ message: require_protocol.AgentMessage.GENERATION_GONE });
1911
2043
  if (!isValidPage(input)) return this.#refuse({ message: `The cursor must be a whole line from 0 and come with a limit of 1 to ${require_protocol.MAX_PAGE_LINES} lines` });
1912
- const source = input.source === "disk" ? "disk" : "output";
2044
+ const source = input.source === require_protocol.FileSource.DISK ? require_protocol.FileSource.DISK : require_protocol.FileSource.OUTPUT;
1913
2045
  if (!generation[source]) return this.#refuse({ message: "This agent kept no snapshot of the files on disk for that job" });
1914
2046
  const result = await this.#generations.read({
1915
2047
  generation,
@@ -1921,6 +2053,69 @@ var Agent = class extends capnweb.RpcTarget {
1921
2053
  await this.#logCommand("readFiles", `read ${Object.keys(result.files).length}/${paths.length} requested file${paths.length === 1 ? "" : "s"}`);
1922
2054
  return result;
1923
2055
  }
2056
+ /**
2057
+ * Returns one page of the line diff of a file between two kept sets. Refused without `allowRead`.
2058
+ */
2059
+ async readDiff(input) {
2060
+ this.#assertConnected();
2061
+ if (!this.#permissions.allowRead) return this.#refuse({
2062
+ message: require_protocol.AgentMessage.READ_NOT_ALLOWED,
2063
+ permission: require_protocol.AgentPermission.READ
2064
+ });
2065
+ const { jobId, path, base, cursor = 0, limit } = input;
2066
+ if (typeof jobId !== "string" || !jobId || typeof path !== "string" || !path || typeof base?.jobId !== "string" || !base.jobId) return this.#refuse({ message: "The request must name a job, a base, and a path" });
2067
+ if (!isValidPage({
2068
+ cursor,
2069
+ limit
2070
+ })) return this.#refuse({ message: `The cursor must be a whole line from 0 and come with a limit of 1 to ${require_protocol.MAX_PAGE_LINES} lines` });
2071
+ const [generation, baseGeneration] = await Promise.all([this.#generations.get(jobId), this.#generations.get(base.jobId)]);
2072
+ if (!generation || !baseGeneration) return this.#refuse({ message: require_protocol.AgentMessage.GENERATION_GONE });
2073
+ const baseSource = base.source === require_protocol.FileSource.DISK ? require_protocol.FileSource.DISK : require_protocol.FileSource.OUTPUT;
2074
+ if (!baseGeneration[baseSource]) return this.#refuse({ message: "This agent kept no snapshot of the files on disk for that job" });
2075
+ const [after, before] = await Promise.all([this.#generations.read({
2076
+ generation,
2077
+ source: require_protocol.FileSource.OUTPUT,
2078
+ paths: [path]
2079
+ }), this.#generations.read({
2080
+ generation: baseGeneration,
2081
+ source: baseSource,
2082
+ paths: [path]
2083
+ })]);
2084
+ const afterContent = after.files[path];
2085
+ if (afterContent === void 0 && path in generation.output.hashes) return this.#refuse({ message: "This agent kept only the hashes of that job" });
2086
+ const beforeContent = before.files[path];
2087
+ if (beforeContent === void 0) return {
2088
+ hunks: [],
2089
+ nextCursor: null,
2090
+ hasBase: false
2091
+ };
2092
+ const key = JSON.stringify([
2093
+ jobId,
2094
+ base.jobId,
2095
+ baseSource,
2096
+ path
2097
+ ]);
2098
+ let hunks = this.#diffs.get(key);
2099
+ if (!hunks) {
2100
+ hunks = diffHunks(beforeContent, afterContent ?? "");
2101
+ this.#diffs.set(key, hunks);
2102
+ if (this.#diffs.size > 4) this.#diffs.delete(this.#diffs.keys().next().value);
2103
+ }
2104
+ const all = await hunks;
2105
+ if (!all) {
2106
+ this.#diffs.delete(key);
2107
+ return this.#refuse({ message: "The two versions differ too much to diff" });
2108
+ }
2109
+ await this.#logCommand("readDiff", `read the diff of ${path}`);
2110
+ return {
2111
+ ...pageHunks({
2112
+ hunks: all,
2113
+ cursor,
2114
+ limit
2115
+ }),
2116
+ hasBase: true
2117
+ };
2118
+ }
1924
2119
  };
1925
2120
  const openWebSocketRpc = ({ url, token, instanceId, agent }) => {
1926
2121
  const socket = new WebSocket(url, { headers: {
@@ -2129,7 +2324,12 @@ async function startPairing({ studioUrl = defaultStudioUrl, type, name, hostname
2129
2324
  },
2130
2325
  signal
2131
2326
  });
2132
- if (typeof pairing?.device_code !== "string") throw new Error("Kubb Studio returned no pairing code");
2327
+ if (typeof pairing?.device_code !== "string") throw new _kubb_core.Diagnostics.Error({
2328
+ code: _kubb_core.Diagnostics.code.unknown,
2329
+ severity: "error",
2330
+ message: "Kubb Studio returned no pairing code",
2331
+ help: "Try again later. If it keeps failing, check the Kubb Studio status."
2332
+ });
2133
2333
  return pairing;
2134
2334
  } catch (error) {
2135
2335
  if (signal?.aborted) throw new PairingCanceledError();
@@ -2170,7 +2370,12 @@ async function pollForPairingToken({ studioUrl = defaultStudioUrl, session, sign
2170
2370
  signal,
2171
2371
  ignoreResponseError: true
2172
2372
  });
2173
- if (status >= 500 || !_data || typeof _data !== "object") throw new Error(`Kubb Studio answered with status ${status}`);
2373
+ if (status >= 500 || !_data || typeof _data !== "object") throw new _kubb_core.Diagnostics.Error({
2374
+ code: _kubb_core.Diagnostics.code.inputRequestFailed,
2375
+ severity: "error",
2376
+ message: `Kubb Studio answered with status ${status}`,
2377
+ help: "Try again later."
2378
+ });
2174
2379
  response = _data;
2175
2380
  } catch (error) {
2176
2381
  if (signal?.aborted) throw new PairingCanceledError();
@@ -2178,7 +2383,11 @@ async function pollForPairingToken({ studioUrl = defaultStudioUrl, session, sign
2178
2383
  continue;
2179
2384
  }
2180
2385
  if (isPairingResult(response)) return response;
2181
- if (!("error" in response) || typeof response.error !== "string") throw new Error("Kubb Studio returned an empty pairing response, pair again");
2386
+ if (!("error" in response) || typeof response.error !== "string") throw new _kubb_core.Diagnostics.Error({
2387
+ code: _kubb_core.Diagnostics.code.unknown,
2388
+ severity: "error",
2389
+ message: "Kubb Studio returned an empty pairing response, pair again"
2390
+ });
2182
2391
  if (response.error === "authorization_pending") continue;
2183
2392
  if (response.error === "slow_down") {
2184
2393
  intervalMs += 5e3;
@@ -2186,7 +2395,12 @@ async function pollForPairingToken({ studioUrl = defaultStudioUrl, session, sign
2186
2395
  }
2187
2396
  if (response.error === "access_denied") throw new PairingDeniedError(response.error_description);
2188
2397
  if (response.error === "expired_token" || response.error === "invalid_grant") throw new PairingExpiredError(response.error_description);
2189
- throw new Error(response.error_description ?? `Pairing failed (${response.error})`);
2398
+ throw new _kubb_core.Diagnostics.Error({
2399
+ code: _kubb_core.Diagnostics.code.unknown,
2400
+ severity: "error",
2401
+ message: response.error_description ?? `Pairing failed (${response.error})`,
2402
+ help: "Pair again with `kubb studio login`."
2403
+ });
2190
2404
  }
2191
2405
  throw new PairingExpiredError();
2192
2406
  }
@@ -2236,7 +2450,12 @@ function splitEntryPath(path) {
2236
2450
  prefix
2237
2451
  };
2238
2452
  }
2239
- throw new Error(`Snapshot path is too long for a tar entry: ${path}`);
2453
+ throw new _kubb_core.Diagnostics.Error({
2454
+ code: _kubb_core.Diagnostics.code.unknown,
2455
+ severity: "error",
2456
+ message: `Snapshot path is too long for a tar entry: ${path}`,
2457
+ help: "Shorten the output path or the generated file names."
2458
+ });
2240
2459
  }
2241
2460
  function header(path, size) {
2242
2461
  const { name, prefix } = splitEntryPath(path);
@@ -2269,7 +2488,12 @@ async function createSnapshotPackage({ files, packageInfo }) {
2269
2488
  for (const [name, content] of Object.entries(files)) {
2270
2489
  const path = packagePath(name);
2271
2490
  const owner = sources.get(path)?.name;
2272
- if (owner) throw new Error(`Snapshot has two generated files that sanitize to the same path "package/${path}": "${owner}" and "${name}"`);
2491
+ if (owner) throw new _kubb_core.Diagnostics.Error({
2492
+ code: _kubb_core.Diagnostics.code.unknown,
2493
+ severity: "error",
2494
+ message: `Snapshot has two generated files that sanitize to the same path "package/${path}": "${owner}" and "${name}"`,
2495
+ help: "Rename one of the generated files, for example with a plugin resolver."
2496
+ });
2273
2497
  sources.set(path, {
2274
2498
  name,
2275
2499
  content
@@ -2359,7 +2583,11 @@ async function createSnapshot({ studioUrl = defaultStudioUrl, apiKey, config, ho
2359
2583
  });
2360
2584
  const files = Object.fromEntries(await Promise.all([...generated.paths].map(async (path) => {
2361
2585
  const content = await generated.storage.readItem((0, node_path.resolve)(generated.root, path));
2362
- if (content === null) throw new Error(`The generation listed ${path} but did not keep its content`);
2586
+ if (content === null) throw new _kubb_core.Diagnostics.Error({
2587
+ code: _kubb_core.Diagnostics.code.internalError,
2588
+ severity: "error",
2589
+ message: `The generation listed ${path} but did not keep its content`
2590
+ });
2363
2591
  return [path, content];
2364
2592
  })));
2365
2593
  const pluginNames = config.plugins.map((plugin) => (0, _kubb_core.resolveMeta)(plugin).name);
@@ -2372,7 +2600,12 @@ async function createSnapshot({ studioUrl = defaultStudioUrl, apiKey, config, ho
2372
2600
  const names = [...new Set(pluginNames.map(packageOf))];
2373
2601
  const peerDependencies = Object.fromEntries(installed.flatMap(({ package: name, version }) => names.includes(name) && version ? [[name, version]] : []));
2374
2602
  const missing = names.filter((name) => !(name in peerDependencies) && !BUNDLED_DEPENDENCIES.includes(name));
2375
- if (missing.length) throw new Error(`Missing dependencies: ${missing.join(", ")}`);
2603
+ if (missing.length) throw new _kubb_core.Diagnostics.Error({
2604
+ code: _kubb_core.Diagnostics.code.missingDependency,
2605
+ severity: "error",
2606
+ message: `Missing dependencies: ${missing.join(", ")}`,
2607
+ help: "Install the listed plugin packages in the project."
2608
+ });
2376
2609
  const bytes = await createSnapshotPackage({
2377
2610
  files,
2378
2611
  packageInfo: {
@@ -2381,7 +2614,12 @@ async function createSnapshot({ studioUrl = defaultStudioUrl, apiKey, config, ho
2381
2614
  peerDependencies
2382
2615
  }
2383
2616
  });
2384
- if (bytes.length > 52428800) throw new Error(`The snapshot is ${Math.ceil(bytes.length / 1024 / 1024)} MB, over the ${require_protocol.MAX_SNAPSHOT_BYTES / 1024 / 1024} MB limit`);
2617
+ if (bytes.length > 52428800) throw new _kubb_core.Diagnostics.Error({
2618
+ code: _kubb_core.Diagnostics.code.unknown,
2619
+ severity: "error",
2620
+ message: `The snapshot is ${Math.ceil(bytes.length / 1024 / 1024)} MB, over the ${require_protocol.MAX_SNAPSHOT_BYTES / 1024 / 1024} MB limit`,
2621
+ help: "Generate fewer files, for example with `include` or `exclude` on the plugins."
2622
+ });
2385
2623
  const hashes = Object.fromEntries(Object.entries(files).map(([path, content]) => [path, hashOf(content)]));
2386
2624
  const body = new FormData();
2387
2625
  body.set("metadata", JSON.stringify({
@@ -2401,9 +2639,19 @@ async function createSnapshot({ studioUrl = defaultStudioUrl, apiKey, config, ho
2401
2639
  });
2402
2640
  if (!ok) {
2403
2641
  const upgrade = _data?.data?.upgradeUrl ? ` Upgrade at ${_data.data.upgradeUrl}.` : "";
2404
- throw new Error(`Kubb Studio refused the snapshot (status ${status})${_data?.message ? `: ${_data.message}` : ""}.${upgrade}`);
2642
+ throw new _kubb_core.Diagnostics.Error({
2643
+ code: _kubb_core.Diagnostics.code.inputRequestFailed,
2644
+ severity: "error",
2645
+ message: `Kubb Studio refused the snapshot (status ${status})${_data?.message ? `: ${_data.message}` : ""}.${upgrade}`,
2646
+ help: "Check the API key and your plan, then try again."
2647
+ });
2405
2648
  }
2406
- if (!_data?.snapshot) throw new Error(`Kubb Studio returned no snapshot (status ${status})`);
2649
+ if (!_data?.snapshot) throw new _kubb_core.Diagnostics.Error({
2650
+ code: _kubb_core.Diagnostics.code.inputRequestFailed,
2651
+ severity: "error",
2652
+ message: `Kubb Studio returned no snapshot (status ${status})`,
2653
+ help: "Try again later."
2654
+ });
2407
2655
  return _data.snapshot;
2408
2656
  }
2409
2657
  //#endregion