@kubb/studio 5.10.1 → 5.10.3

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.js CHANGED
@@ -1,5 +1,4 @@
1
- import "./rolldown-runtime-CRm0XQPb.js";
2
- import { AGENT_INSTANCE_HEADER, AGENT_SOCKET_PATH, AgentCloseCode, GENERATION_GONE_MESSAGE, MAX_PAGE_LINES, MAX_SNAPSHOT_BYTES, NO_INPUT_MESSAGE, READ_AST_NOT_ALLOWED_MESSAGE, READ_NOT_ALLOWED_MESSAGE, generationEventTypes } from "./protocol.js";
1
+ import { AGENT_INSTANCE_HEADER, AGENT_SOCKET_PATH, AgentCloseCode, AgentMessage, AgentPermission, FileSource, MAX_PAGE_LINES, MAX_SNAPSHOT_BYTES, generationEventTypes } from "./protocol.js";
3
2
  import { createRequire } from "node:module";
4
3
  import { createHash, hash, randomUUID } from "node:crypto";
5
4
  import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
@@ -12,10 +11,12 @@ import { existsSync } from "node:fs";
12
11
  import { Diagnostics, Hookable, cacheStorage, createKubb, fsStorage, getInputKind, isPackageName, memoryStorage, readInstalledPackages, resolveCacheDir, resolveMeta } from "@kubb/core";
13
12
  import { RpcTarget, newWebSocketRpcSession } from "capnweb";
14
13
  import { detectCodeFormat, parseExpression, parseModule } from "magicast";
14
+ import { structuredPatch } from "diff";
15
15
  import { stripVTControlCharacters } from "node:util";
16
16
  import { ast } from "@kubb/ast";
17
17
  import { ofetch } from "ofetch";
18
18
  import { gzipSync } from "node:zlib";
19
+ //#endregion
19
20
  //#region ../../internals/utils/src/casing.ts
20
21
  /**
21
22
  * Shared implementation for camelCase and PascalCase conversion.
@@ -153,7 +154,7 @@ function getElapsedMs(hrStart) {
153
154
  }
154
155
  //#endregion
155
156
  //#region package.json
156
- var version = "5.10.1";
157
+ var version = "5.10.3";
157
158
  //#endregion
158
159
  //#region src/resolveConfig.ts
159
160
  /**
@@ -255,22 +256,42 @@ function isAllowedPlugin({ packageName, extensions }) {
255
256
  *
256
257
  * @throws if the package cannot be imported or exports no callable factory.
257
258
  */
258
- async function loadFactory({ packageName, extensions }) {
259
+ async function loadFactory({ packageName, extensions, kind = "plugin" }) {
260
+ const label = {
261
+ plugin: "Plugin",
262
+ adapter: "Adapter",
263
+ parser: "Parser"
264
+ }[kind];
259
265
  let mod;
260
266
  try {
261
267
  mod = await importFromProject(packageName);
262
268
  } catch (cause) {
263
- throw new Error(`Plugin "${packageName}" could not be loaded. Make sure it is installed: \`npm install ${packageName}\``, { cause });
269
+ throw new Diagnostics.Error({
270
+ code: kind === "plugin" ? Diagnostics.code.pluginNotFound : Diagnostics.code.unknown,
271
+ severity: "error",
272
+ message: `${label} "${packageName}" could not be loaded. Make sure it is installed: \`npm install ${packageName}\``,
273
+ cause: toError(cause)
274
+ });
264
275
  }
265
276
  const manifestExport = extensions.get(packageName)?.manifest.export;
266
277
  if (manifestExport) {
267
278
  if (typeof mod[manifestExport] === "function") return mod[manifestExport];
268
- throw new Error(`Plugin "${packageName}" does not export "${manifestExport}", the factory its kubb-extension.json names.`);
279
+ throw new Diagnostics.Error({
280
+ code: Diagnostics.code.incompatibleExtension,
281
+ severity: "error",
282
+ message: `${label} "${packageName}" does not export "${manifestExport}", the factory its kubb-extension.json names.`,
283
+ help: "Install a version of the plugin whose exports match its kubb-extension.json."
284
+ });
269
285
  }
270
286
  const exportName = toExportName(packageName);
271
287
  if (typeof mod[exportName] === "function") return mod[exportName];
272
288
  if (typeof mod["default"] === "function") return mod["default"];
273
- throw new Error(`Plugin "${packageName}" does not export a callable factory. Tried the named export "${exportName}" and "default".`);
289
+ throw new Diagnostics.Error({
290
+ code: Diagnostics.code.incompatibleExtension,
291
+ severity: "error",
292
+ message: `${label} "${packageName}" does not export a callable factory. Tried the named export "${exportName}" and "default".`,
293
+ help: "Install a Kubb plugin package that exports its factory."
294
+ });
274
295
  }
275
296
  /**
276
297
  * Resolves each plugin entry by dynamically importing its package and calling its factory with the
@@ -291,7 +312,11 @@ async function resolvePlugins({ plugins, extensions = /* @__PURE__ */ new Map()
291
312
  if (!isAllowedPlugin({
292
313
  packageName: name,
293
314
  extensions
294
- })) 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".`);
315
+ })) throw new Diagnostics.Error({
316
+ code: Diagnostics.code.pluginNotFound,
317
+ severity: "error",
318
+ 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".`
319
+ });
295
320
  return (await loadFactory({
296
321
  packageName: name,
297
322
  extensions
@@ -418,7 +443,12 @@ async function mergePlugins({ diskPlugins, studioPlugins, extensions = /* @__PUR
418
443
  extensions
419
444
  }).catch((cause) => {
420
445
  if (unchanged) return [diskPlugin];
421
- 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 });
446
+ throw new Diagnostics.Error({
447
+ code: Diagnostics.code.pluginNotFound,
448
+ severity: "error",
449
+ 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.`,
450
+ cause: toError(cause)
451
+ });
422
452
  });
423
453
  return resolved ?? diskPlugin;
424
454
  }));
@@ -442,9 +472,15 @@ async function rebuildExtension({ disk, studioOptions, kind, extensions }) {
442
472
  const { name } = resolveMeta(disk);
443
473
  return (await loadFactory({
444
474
  packageName: [...extensions.values()].find(({ manifest }) => manifest.kind === kind && manifest.name === name)?.package ?? `@kubb/${kind}-${name}`,
445
- extensions
475
+ extensions,
476
+ kind
446
477
  }).catch((cause) => {
447
- 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 });
478
+ throw new Diagnostics.Error({
479
+ code: kind === "plugin" ? Diagnostics.code.pluginNotFound : Diagnostics.code.unknown,
480
+ severity: "error",
481
+ message: `The ${kind} "${name}" is not an installed Kubb extension, so Studio cannot change its options. Edit it in the config file.`,
482
+ cause: toError(cause)
483
+ });
448
484
  }))(mergeOptions(disk.options ?? {}, studioOptions));
449
485
  }
450
486
  /**
@@ -521,7 +557,11 @@ function invalidEdit(edit) {
521
557
  }
522
558
  /** Where a node sits in the source text. */
523
559
  function rangeOf(node) {
524
- if (typeof node.start !== "number" || typeof node.end !== "number") throw new Error("the config file could not be mapped back to its source text");
560
+ if (typeof node.start !== "number" || typeof node.end !== "number") throw new Diagnostics.Error({
561
+ code: Diagnostics.code.internalError,
562
+ severity: "error",
563
+ message: "the config file could not be mapped back to its source text"
564
+ });
525
565
  return {
526
566
  start: node.start,
527
567
  end: node.end
@@ -773,6 +813,44 @@ function disabledBlocks(source, config) {
773
813
  }];
774
814
  });
775
815
  }
816
+ /**
817
+ * Reads an object option that holds some code: its literal members as `value`, and the paths of
818
+ * the members that are code as `locked`. `set` can write any path outside `locked`, so Studio
819
+ * edits those fields. `undefined` for a computed key, since it could set any path.
820
+ */
821
+ function readPartial(node, prefix = []) {
822
+ const value = {};
823
+ const locked = [];
824
+ for (const [index, entry] of node.properties.entries()) {
825
+ if (entry.type === "SpreadElement") continue;
826
+ const key = memberKey(entry);
827
+ if (key === void 0) return void 0;
828
+ if (findMember(node, key)?.index !== index) continue;
829
+ const path = [...prefix, key];
830
+ if (!isSafeKey(key)) {
831
+ locked.push(path);
832
+ continue;
833
+ }
834
+ const member = entry.type === "ObjectProperty" && !isShadowed(node, index) ? entry.value : void 0;
835
+ const literal = readLiteral(member);
836
+ if (literal !== void 0) {
837
+ value[key] = literal;
838
+ continue;
839
+ }
840
+ if (member?.type !== "ObjectExpression") {
841
+ locked.push(path);
842
+ continue;
843
+ }
844
+ const nested = readPartial(member, path);
845
+ if (!nested) return void 0;
846
+ value[key] = nested.value;
847
+ locked.push(...nested.locked);
848
+ }
849
+ return {
850
+ value,
851
+ locked
852
+ };
853
+ }
776
854
  /** The top-level options of a plugin call, each flagged with whether Studio may write it. */
777
855
  function readOptions(call) {
778
856
  const entries = {};
@@ -781,10 +859,15 @@ function readOptions(call) {
781
859
  for (const [index, entry] of options.properties.entries()) {
782
860
  const key = memberKey(entry);
783
861
  if (key === void 0 || !isSafeKey(key)) continue;
784
- const value = entry.type === "ObjectProperty" && !isShadowed(options, index) ? readLiteral(entry.value) : void 0;
785
- entries[key] = value === void 0 ? { literal: false } : {
862
+ const member = entry.type === "ObjectProperty" && !isShadowed(options, index) ? entry.value : void 0;
863
+ const value = readLiteral(member);
864
+ const partial = value === void 0 && member?.type === "ObjectExpression" ? readPartial(member) : void 0;
865
+ entries[key] = value !== void 0 ? {
786
866
  literal: true,
787
867
  value
868
+ } : {
869
+ literal: false,
870
+ ...partial
788
871
  };
789
872
  }
790
873
  return entries;
@@ -1070,6 +1153,159 @@ async function writeConfigEdits({ filePath, edits, extensions }) {
1070
1153
  return result;
1071
1154
  }
1072
1155
  //#endregion
1156
+ //#region src/diff.ts
1157
+ const CONTEXT_LINES = 3;
1158
+ const DIFF_TIMEOUT_MS = 1e3;
1159
+ const splitLines = (text) => text === "" ? [] : text.split(/(?<=\n)/);
1160
+ function longestIncreasing(values) {
1161
+ const tails = [];
1162
+ const prev = [];
1163
+ for (const [index, value] of values.entries()) {
1164
+ let low = 0;
1165
+ let high = tails.length;
1166
+ while (low < high) {
1167
+ const mid = low + high >> 1;
1168
+ if (values[tails[mid]] < value) low = mid + 1;
1169
+ else high = mid;
1170
+ }
1171
+ prev[index] = low > 0 ? tails[low - 1] : -1;
1172
+ tails[low] = index;
1173
+ }
1174
+ const result = [];
1175
+ for (let index = tails.at(-1) ?? -1; index !== -1; index = prev[index]) result.push(index);
1176
+ return result.reverse();
1177
+ }
1178
+ function cutPoints(before, after) {
1179
+ const counts = /* @__PURE__ */ new Map();
1180
+ for (const [index, line] of before.entries()) {
1181
+ const count = counts.get(line);
1182
+ if (count) count.before++;
1183
+ else counts.set(line, {
1184
+ before: 1,
1185
+ after: 0,
1186
+ index
1187
+ });
1188
+ }
1189
+ const afterIndex = /* @__PURE__ */ new Map();
1190
+ for (const [index, line] of after.entries()) {
1191
+ const count = counts.get(line);
1192
+ if (!count) continue;
1193
+ count.after++;
1194
+ afterIndex.set(line, index);
1195
+ }
1196
+ const unique = [...counts].filter(([, count]) => count.before === 1 && count.after === 1).map(([line, count]) => [count.index, afterIndex.get(line)]);
1197
+ const anchors = longestIncreasing(unique.map(([, index]) => index)).map((index) => unique[index]);
1198
+ const cuts = [[0, 0]];
1199
+ let run = {
1200
+ before: 0,
1201
+ after: 0,
1202
+ length: 0
1203
+ };
1204
+ const closeRun = () => {
1205
+ if (run.length > 6) cuts.push([run.before + CONTEXT_LINES, run.after + CONTEXT_LINES]);
1206
+ };
1207
+ let i = 0;
1208
+ let j = 0;
1209
+ for (const [anchorBefore, anchorAfter] of anchors) {
1210
+ const unchanged = anchorBefore - i === anchorAfter - j && before.slice(i, anchorBefore).every((line, offset) => line === after[j + offset]);
1211
+ if (unchanged && run.length && run.before + run.length === i) run.length += anchorBefore - i + 1;
1212
+ else {
1213
+ closeRun();
1214
+ run = unchanged ? {
1215
+ before: i,
1216
+ after: j,
1217
+ length: anchorBefore - i + 1
1218
+ } : {
1219
+ before: anchorBefore,
1220
+ after: anchorAfter,
1221
+ length: 1
1222
+ };
1223
+ }
1224
+ i = anchorBefore + 1;
1225
+ j = anchorAfter + 1;
1226
+ }
1227
+ closeRun();
1228
+ cuts.push([before.length, after.length]);
1229
+ return cuts;
1230
+ }
1231
+ function replaceHunk(before, after) {
1232
+ let start = 0;
1233
+ while (start < before.length && start < after.length && before[start] === after[start]) start++;
1234
+ let end = 0;
1235
+ while (end < before.length - start && end < after.length - start && before[before.length - 1 - end] === after[after.length - 1 - end]) end++;
1236
+ const removed = before.slice(start, before.length - end);
1237
+ const added = after.slice(start, after.length - end);
1238
+ if (!removed.length && !added.length) return [];
1239
+ const lines = (prefix, rows) => rows.flatMap((row) => row.endsWith("\n") ? [`${prefix}${row.slice(0, -1)}`] : [`${prefix}${row}`, "\"]);
1240
+ return [{
1241
+ oldStart: start + 1,
1242
+ oldLines: removed.length,
1243
+ newStart: start + 1,
1244
+ newLines: added.length,
1245
+ lines: [...lines("-", removed), ...lines("+", added)]
1246
+ }];
1247
+ }
1248
+ function diffHunks(before, after, timeout = DIFF_TIMEOUT_MS) {
1249
+ const beforeLines = splitLines(before);
1250
+ const afterLines = splitLines(after);
1251
+ const cuts = cutPoints(beforeLines, afterLines);
1252
+ const deadline = Date.now() + timeout;
1253
+ const hunks = [];
1254
+ let coarse = false;
1255
+ for (const [index, [beforeStart, afterStart]] of cuts.slice(0, -1).entries()) {
1256
+ const [beforeEnd, afterEnd] = cuts[index + 1];
1257
+ const segmentBefore = beforeLines.slice(beforeStart, beforeEnd);
1258
+ const segmentAfter = afterLines.slice(afterStart, afterEnd);
1259
+ const patch = structuredPatch("", "", segmentBefore.join(""), segmentAfter.join(""), void 0, void 0, {
1260
+ context: CONTEXT_LINES,
1261
+ timeout: Math.max(0, deadline - Date.now())
1262
+ });
1263
+ if (!patch) coarse = true;
1264
+ for (const hunk of patch?.hunks ?? replaceHunk(segmentBefore, segmentAfter)) hunks.push({
1265
+ ...hunk,
1266
+ oldStart: hunk.oldStart + beforeStart,
1267
+ newStart: hunk.newStart + afterStart
1268
+ });
1269
+ }
1270
+ const lines = hunks.flatMap((hunk) => hunk.lines);
1271
+ return {
1272
+ hunks,
1273
+ stats: {
1274
+ additions: lines.filter((line) => line[0] === "+").length,
1275
+ deletions: lines.filter((line) => line[0] === "-").length,
1276
+ hunks: hunks.length,
1277
+ lines: lines.length,
1278
+ coarse
1279
+ }
1280
+ };
1281
+ }
1282
+ function pageHunks({ hunks, cursor, limit }) {
1283
+ const page = [];
1284
+ let used = 0;
1285
+ let start = 0;
1286
+ for (const [index, hunk] of hunks.entries()) {
1287
+ const offset = Math.max(0, cursor - start);
1288
+ start += hunk.lines.length;
1289
+ const remaining = hunk.lines.length - offset;
1290
+ if (remaining <= 0) continue;
1291
+ if (used > 0 && remaining > limit - used) break;
1292
+ let take = Math.min(remaining, limit - used);
1293
+ if (hunk.lines[offset + take]?.startsWith("\\")) take++;
1294
+ page.push({
1295
+ index,
1296
+ ...hunk,
1297
+ lines: hunk.lines.slice(offset, offset + take)
1298
+ });
1299
+ used += take;
1300
+ if (used >= limit) break;
1301
+ }
1302
+ const total = hunks.reduce((sum, hunk) => sum + hunk.lines.length, 0);
1303
+ return {
1304
+ hunks: page,
1305
+ nextCursor: cursor + used < total ? cursor + used : null
1306
+ };
1307
+ }
1308
+ //#endregion
1073
1309
  //#region src/generations.ts
1074
1310
  const READ_CONCURRENCY = 50;
1075
1311
  const MB = 1048576;
@@ -1199,8 +1435,8 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
1199
1435
  if (path.split("/").includes("..")) return;
1200
1436
  const content = await files.storage.readItem(resolve(files.root, path));
1201
1437
  if (content === null) return;
1202
- if (source === "output" && !content.trim()) return;
1203
- hashes[path] = hashOf(source === "output" ? `${content.trim()}\n` : content);
1438
+ if (source === FileSource.OUTPUT && !content.trim()) return;
1439
+ hashes[path] = hashOf(source === FileSource.OUTPUT ? `${content.trim()}\n` : content);
1204
1440
  bytes += Buffer.byteLength(content);
1205
1441
  if (bytes <= maxSetBytes) await storage.writeItem(`${dirOf(jobId)}${source}/${path}`, content);
1206
1442
  }
@@ -1302,7 +1538,7 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
1302
1538
  //#region src/generationEvents.ts
1303
1539
  const MAX_QUEUED_EVENTS = 1024;
1304
1540
  const RESERVED_EVENTS = 64;
1305
- const isDiscardable = (event) => event.type === "kubb:files:processing:update" || event.type === "kubb:info" || event.type === "kubb:success";
1541
+ const isDiscardable = (type) => type === "kubb:files:processing:update" || type === "kubb:info" || type === "kubb:success";
1306
1542
  /** Forwards selected Kubb lifecycle events to a native Cap'n Web stream. */
1307
1543
  function createGenerationStream(hooks, jobId) {
1308
1544
  const unhooks = [];
@@ -1334,7 +1570,7 @@ function createGenerationStream(hooks, jobId) {
1334
1570
  version: 1,
1335
1571
  timestamp: Date.now()
1336
1572
  };
1337
- if (controller.desiredSize <= RESERVED_EVENTS && isDiscardable(event)) return;
1573
+ if (controller.desiredSize <= RESERVED_EVENTS && isDiscardable(type)) return;
1338
1574
  if (controller.desiredSize <= 0) {
1339
1575
  fail(/* @__PURE__ */ new Error(`Generation event stream exceeded ${MAX_QUEUED_EVENTS} queued events`));
1340
1576
  return;
@@ -1438,7 +1674,10 @@ function createGenerationStream(hooks, jobId) {
1438
1674
  }]
1439
1675
  };
1440
1676
  function forward(type) {
1441
- on(type, (...args) => emitEvent(type, projections[type](...args)));
1677
+ on(type, (...args) => {
1678
+ emitEvent(type, projections[type](...args));
1679
+ if (!isDiscardable(type)) return new Promise((resolve) => setImmediate(resolve));
1680
+ });
1442
1681
  }
1443
1682
  on("kubb:build:start", ({ config }) => {
1444
1683
  root = config.root;
@@ -1498,7 +1737,11 @@ function isLoopbackHost(hostname) {
1498
1737
  */
1499
1738
  function secureUrl({ studioUrl, path, socket = false }) {
1500
1739
  const url = new URL(`${studioUrl.replace(/\/+$/, "")}${path}`);
1501
- 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`);
1740
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && isLoopbackHost(url.hostname))) throw new Diagnostics.Error({
1741
+ code: Diagnostics.code.unknown,
1742
+ severity: "error",
1743
+ message: `Refusing to reach Kubb Studio at ${url.origin}: use an https:// URL, or a loopback host for local development`
1744
+ });
1502
1745
  if (socket) url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
1503
1746
  return url;
1504
1747
  }
@@ -1568,6 +1811,7 @@ var Agent = class extends RpcTarget {
1568
1811
  #session;
1569
1812
  #limits = resolveGenerationLimits();
1570
1813
  #store;
1814
+ #diffs = /* @__PURE__ */ new Map();
1571
1815
  constructor({ options, session }) {
1572
1816
  super();
1573
1817
  this.#options = options;
@@ -1606,10 +1850,18 @@ var Agent = class extends RpcTarget {
1606
1850
  message,
1607
1851
  permission
1608
1852
  });
1609
- throw new Error(message);
1853
+ throw new Diagnostics.Error({
1854
+ code: Diagnostics.code.unknown,
1855
+ severity: "error",
1856
+ message
1857
+ });
1610
1858
  }
1611
1859
  #assertConnected() {
1612
- if (!this.#session.info) throw new Error("Studio must call connect() before any other command");
1860
+ if (!this.#session.info) throw new Diagnostics.Error({
1861
+ code: Diagnostics.code.internalError,
1862
+ severity: "error",
1863
+ message: "Studio must call connect() before any other command"
1864
+ });
1613
1865
  }
1614
1866
  #logCommand(command, info) {
1615
1867
  return emit(this.#options.hooks, "studio:command", {
@@ -1706,7 +1958,11 @@ var Agent = class extends RpcTarget {
1706
1958
  /** Runs one job and keeps its output, and its input with `allowRead`, for later reads. */
1707
1959
  async #runGeneration(input, controller) {
1708
1960
  this.#assertConnected();
1709
- if (this.#session.closed) throw new Error("Connection is closed");
1961
+ if (this.#session.closed) throw new Diagnostics.Error({
1962
+ code: Diagnostics.code.internalError,
1963
+ severity: "error",
1964
+ message: "Connection is closed"
1965
+ });
1710
1966
  if (this.#session.active) return this.#refuse({ message: "A generation is already in progress, please wait for it to finish" });
1711
1967
  const settled = Promise.withResolvers();
1712
1968
  this.#session.active = {
@@ -1764,7 +2020,7 @@ var Agent = class extends RpcTarget {
1764
2020
  }) : void 0;
1765
2021
  const disk = diskFiles ? await this.#generations.keep({
1766
2022
  jobId,
1767
- source: "disk",
2023
+ source: FileSource.DISK,
1768
2024
  files: diskFiles,
1769
2025
  maxSetMb: this.#limits.maxSnapshotMb
1770
2026
  }) : void 0;
@@ -1787,7 +2043,7 @@ var Agent = class extends RpcTarget {
1787
2043
  });
1788
2044
  const output = await this.#generations.keep({
1789
2045
  jobId,
1790
- source: "output",
2046
+ source: FileSource.OUTPUT,
1791
2047
  files,
1792
2048
  maxSetMb: this.#limits.maxMb
1793
2049
  });
@@ -1859,14 +2115,14 @@ var Agent = class extends RpcTarget {
1859
2115
  async readInput(input) {
1860
2116
  this.#assertConnected();
1861
2117
  if (!this.#permissions.allowReadAst) return this.#refuse({
1862
- message: READ_AST_NOT_ALLOWED_MESSAGE,
1863
- permission: "allowReadAst"
2118
+ message: AgentMessage.READ_AST_NOT_ALLOWED,
2119
+ permission: AgentPermission.READ_AST
1864
2120
  });
1865
2121
  if (typeof input.jobId !== "string" || !input.jobId) return this.#refuse({ message: "The request must name a job" });
1866
2122
  const generation = await this.#generations.get(input.jobId);
1867
- if (!generation) return this.#refuse({ message: GENERATION_GONE_MESSAGE });
2123
+ if (!generation) return this.#refuse({ message: AgentMessage.GENERATION_GONE });
1868
2124
  const kept = generation.inputBytes ? await this.#generations.readInput(input.jobId) : void 0;
1869
- if (!kept) return this.#refuse({ message: NO_INPUT_MESSAGE });
2125
+ if (!kept) return this.#refuse({ message: AgentMessage.NO_INPUT });
1870
2126
  await this.#logCommand("readInput", "read the API input");
1871
2127
  return { input: kept };
1872
2128
  }
@@ -1876,16 +2132,16 @@ var Agent = class extends RpcTarget {
1876
2132
  async readFiles(input) {
1877
2133
  this.#assertConnected();
1878
2134
  if (!this.#permissions.allowRead) return this.#refuse({
1879
- message: READ_NOT_ALLOWED_MESSAGE,
1880
- permission: "allowRead"
2135
+ message: AgentMessage.READ_NOT_ALLOWED,
2136
+ permission: AgentPermission.READ
1881
2137
  });
1882
2138
  if (!Array.isArray(input.paths) || typeof input.jobId !== "string" || !input.jobId) return this.#refuse({ message: "The request must name a job and carry paths" });
1883
2139
  const { paths } = input;
1884
2140
  if (paths.length > 50) return this.#refuse({ message: `At most 50 paths may be requested at once` });
1885
2141
  const generation = await this.#generations.get(input.jobId);
1886
- if (!generation) return this.#refuse({ message: GENERATION_GONE_MESSAGE });
2142
+ if (!generation) return this.#refuse({ message: AgentMessage.GENERATION_GONE });
1887
2143
  if (!isValidPage(input)) return this.#refuse({ message: `The cursor must be a whole line from 0 and come with a limit of 1 to ${MAX_PAGE_LINES} lines` });
1888
- const source = input.source === "disk" ? "disk" : "output";
2144
+ const source = input.source === FileSource.DISK ? FileSource.DISK : FileSource.OUTPUT;
1889
2145
  if (!generation[source]) return this.#refuse({ message: "This agent kept no snapshot of the files on disk for that job" });
1890
2146
  const result = await this.#generations.read({
1891
2147
  generation,
@@ -1897,6 +2153,72 @@ var Agent = class extends RpcTarget {
1897
2153
  await this.#logCommand("readFiles", `read ${Object.keys(result.files).length}/${paths.length} requested file${paths.length === 1 ? "" : "s"}`);
1898
2154
  return result;
1899
2155
  }
2156
+ /**
2157
+ * Returns one page of the line diff of a file between two kept sets. Refused without `allowRead`.
2158
+ */
2159
+ async readDiff(input) {
2160
+ this.#assertConnected();
2161
+ if (!this.#permissions.allowRead) return this.#refuse({
2162
+ message: AgentMessage.READ_NOT_ALLOWED,
2163
+ permission: AgentPermission.READ
2164
+ });
2165
+ const { jobId, path, base, cursor = 0, limit } = input;
2166
+ 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" });
2167
+ if (!isValidPage({
2168
+ cursor,
2169
+ limit
2170
+ })) return this.#refuse({ message: `The cursor must be a whole line from 0 and come with a limit of 1 to ${MAX_PAGE_LINES} lines` });
2171
+ const [generation, baseGeneration] = await Promise.all([this.#generations.get(jobId), this.#generations.get(base.jobId)]);
2172
+ if (!generation || !baseGeneration) return this.#refuse({ message: AgentMessage.GENERATION_GONE });
2173
+ const baseSource = base.source === FileSource.DISK ? FileSource.DISK : FileSource.OUTPUT;
2174
+ if (!baseGeneration[baseSource]) return this.#refuse({ message: "This agent kept no snapshot of the files on disk for that job" });
2175
+ const [after, before] = await Promise.all([this.#generations.read({
2176
+ generation,
2177
+ source: FileSource.OUTPUT,
2178
+ paths: [path]
2179
+ }), this.#generations.read({
2180
+ generation: baseGeneration,
2181
+ source: baseSource,
2182
+ paths: [path]
2183
+ })]);
2184
+ const afterContent = after.files[path];
2185
+ if (afterContent === void 0 && path in generation.output.hashes) return this.#refuse({ message: "This agent kept only the hashes of that job" });
2186
+ const beforeContent = before.files[path];
2187
+ if (beforeContent === void 0) return {
2188
+ hunks: [],
2189
+ nextCursor: null,
2190
+ hasBase: false,
2191
+ stats: {
2192
+ additions: 0,
2193
+ deletions: 0,
2194
+ hunks: 0,
2195
+ lines: 0,
2196
+ coarse: false
2197
+ }
2198
+ };
2199
+ const key = JSON.stringify([
2200
+ jobId,
2201
+ base.jobId,
2202
+ baseSource,
2203
+ path
2204
+ ]);
2205
+ let diff = this.#diffs.get(key);
2206
+ if (!diff) {
2207
+ diff = diffHunks(beforeContent, afterContent ?? "");
2208
+ this.#diffs.set(key, diff);
2209
+ if (this.#diffs.size > 4) this.#diffs.delete(this.#diffs.keys().next().value);
2210
+ }
2211
+ await this.#logCommand("readDiff", `read the diff of ${path}`);
2212
+ return {
2213
+ ...pageHunks({
2214
+ hunks: diff.hunks,
2215
+ cursor,
2216
+ limit
2217
+ }),
2218
+ hasBase: true,
2219
+ stats: diff.stats
2220
+ };
2221
+ }
1900
2222
  };
1901
2223
  const openWebSocketRpc = ({ url, token, instanceId, agent }) => {
1902
2224
  const socket = new WebSocket(url, { headers: {
@@ -2105,7 +2427,12 @@ async function startPairing({ studioUrl = defaultStudioUrl, type, name, hostname
2105
2427
  },
2106
2428
  signal
2107
2429
  });
2108
- if (typeof pairing?.device_code !== "string") throw new Error("Kubb Studio returned no pairing code");
2430
+ if (typeof pairing?.device_code !== "string") throw new Diagnostics.Error({
2431
+ code: Diagnostics.code.unknown,
2432
+ severity: "error",
2433
+ message: "Kubb Studio returned no pairing code",
2434
+ help: "Try again later. If it keeps failing, check the Kubb Studio status."
2435
+ });
2109
2436
  return pairing;
2110
2437
  } catch (error) {
2111
2438
  if (signal?.aborted) throw new PairingCanceledError();
@@ -2146,7 +2473,12 @@ async function pollForPairingToken({ studioUrl = defaultStudioUrl, session, sign
2146
2473
  signal,
2147
2474
  ignoreResponseError: true
2148
2475
  });
2149
- if (status >= 500 || !_data || typeof _data !== "object") throw new Error(`Kubb Studio answered with status ${status}`);
2476
+ if (status >= 500 || !_data || typeof _data !== "object") throw new Diagnostics.Error({
2477
+ code: Diagnostics.code.inputRequestFailed,
2478
+ severity: "error",
2479
+ message: `Kubb Studio answered with status ${status}`,
2480
+ help: "Try again later."
2481
+ });
2150
2482
  response = _data;
2151
2483
  } catch (error) {
2152
2484
  if (signal?.aborted) throw new PairingCanceledError();
@@ -2154,7 +2486,11 @@ async function pollForPairingToken({ studioUrl = defaultStudioUrl, session, sign
2154
2486
  continue;
2155
2487
  }
2156
2488
  if (isPairingResult(response)) return response;
2157
- if (!("error" in response) || typeof response.error !== "string") throw new Error("Kubb Studio returned an empty pairing response, pair again");
2489
+ if (!("error" in response) || typeof response.error !== "string") throw new Diagnostics.Error({
2490
+ code: Diagnostics.code.unknown,
2491
+ severity: "error",
2492
+ message: "Kubb Studio returned an empty pairing response, pair again"
2493
+ });
2158
2494
  if (response.error === "authorization_pending") continue;
2159
2495
  if (response.error === "slow_down") {
2160
2496
  intervalMs += 5e3;
@@ -2162,7 +2498,12 @@ async function pollForPairingToken({ studioUrl = defaultStudioUrl, session, sign
2162
2498
  }
2163
2499
  if (response.error === "access_denied") throw new PairingDeniedError(response.error_description);
2164
2500
  if (response.error === "expired_token" || response.error === "invalid_grant") throw new PairingExpiredError(response.error_description);
2165
- throw new Error(response.error_description ?? `Pairing failed (${response.error})`);
2501
+ throw new Diagnostics.Error({
2502
+ code: Diagnostics.code.unknown,
2503
+ severity: "error",
2504
+ message: response.error_description ?? `Pairing failed (${response.error})`,
2505
+ help: "Pair again with `kubb studio login`."
2506
+ });
2166
2507
  }
2167
2508
  throw new PairingExpiredError();
2168
2509
  }
@@ -2212,7 +2553,12 @@ function splitEntryPath(path) {
2212
2553
  prefix
2213
2554
  };
2214
2555
  }
2215
- throw new Error(`Snapshot path is too long for a tar entry: ${path}`);
2556
+ throw new Diagnostics.Error({
2557
+ code: Diagnostics.code.unknown,
2558
+ severity: "error",
2559
+ message: `Snapshot path is too long for a tar entry: ${path}`,
2560
+ help: "Shorten the output path or the generated file names."
2561
+ });
2216
2562
  }
2217
2563
  function header(path, size) {
2218
2564
  const { name, prefix } = splitEntryPath(path);
@@ -2245,7 +2591,12 @@ async function createSnapshotPackage({ files, packageInfo }) {
2245
2591
  for (const [name, content] of Object.entries(files)) {
2246
2592
  const path = packagePath(name);
2247
2593
  const owner = sources.get(path)?.name;
2248
- if (owner) throw new Error(`Snapshot has two generated files that sanitize to the same path "package/${path}": "${owner}" and "${name}"`);
2594
+ if (owner) throw new Diagnostics.Error({
2595
+ code: Diagnostics.code.unknown,
2596
+ severity: "error",
2597
+ message: `Snapshot has two generated files that sanitize to the same path "package/${path}": "${owner}" and "${name}"`,
2598
+ help: "Rename one of the generated files, for example with a plugin resolver."
2599
+ });
2249
2600
  sources.set(path, {
2250
2601
  name,
2251
2602
  content
@@ -2335,7 +2686,11 @@ async function createSnapshot({ studioUrl = defaultStudioUrl, apiKey, config, ho
2335
2686
  });
2336
2687
  const files = Object.fromEntries(await Promise.all([...generated.paths].map(async (path) => {
2337
2688
  const content = await generated.storage.readItem(resolve(generated.root, path));
2338
- if (content === null) throw new Error(`The generation listed ${path} but did not keep its content`);
2689
+ if (content === null) throw new Diagnostics.Error({
2690
+ code: Diagnostics.code.internalError,
2691
+ severity: "error",
2692
+ message: `The generation listed ${path} but did not keep its content`
2693
+ });
2339
2694
  return [path, content];
2340
2695
  })));
2341
2696
  const pluginNames = config.plugins.map((plugin) => resolveMeta(plugin).name);
@@ -2348,7 +2703,12 @@ async function createSnapshot({ studioUrl = defaultStudioUrl, apiKey, config, ho
2348
2703
  const names = [...new Set(pluginNames.map(packageOf))];
2349
2704
  const peerDependencies = Object.fromEntries(installed.flatMap(({ package: name, version }) => names.includes(name) && version ? [[name, version]] : []));
2350
2705
  const missing = names.filter((name) => !(name in peerDependencies) && !BUNDLED_DEPENDENCIES.includes(name));
2351
- if (missing.length) throw new Error(`Missing dependencies: ${missing.join(", ")}`);
2706
+ if (missing.length) throw new Diagnostics.Error({
2707
+ code: Diagnostics.code.missingDependency,
2708
+ severity: "error",
2709
+ message: `Missing dependencies: ${missing.join(", ")}`,
2710
+ help: "Install the listed plugin packages in the project."
2711
+ });
2352
2712
  const bytes = await createSnapshotPackage({
2353
2713
  files,
2354
2714
  packageInfo: {
@@ -2357,7 +2717,12 @@ async function createSnapshot({ studioUrl = defaultStudioUrl, apiKey, config, ho
2357
2717
  peerDependencies
2358
2718
  }
2359
2719
  });
2360
- if (bytes.length > 52428800) throw new Error(`The snapshot is ${Math.ceil(bytes.length / 1024 / 1024)} MB, over the ${MAX_SNAPSHOT_BYTES / 1024 / 1024} MB limit`);
2720
+ if (bytes.length > 52428800) throw new Diagnostics.Error({
2721
+ code: Diagnostics.code.unknown,
2722
+ severity: "error",
2723
+ message: `The snapshot is ${Math.ceil(bytes.length / 1024 / 1024)} MB, over the ${MAX_SNAPSHOT_BYTES / 1024 / 1024} MB limit`,
2724
+ help: "Generate fewer files, for example with `include` or `exclude` on the plugins."
2725
+ });
2361
2726
  const hashes = Object.fromEntries(Object.entries(files).map(([path, content]) => [path, hashOf(content)]));
2362
2727
  const body = new FormData();
2363
2728
  body.set("metadata", JSON.stringify({
@@ -2377,9 +2742,19 @@ async function createSnapshot({ studioUrl = defaultStudioUrl, apiKey, config, ho
2377
2742
  });
2378
2743
  if (!ok) {
2379
2744
  const upgrade = _data?.data?.upgradeUrl ? ` Upgrade at ${_data.data.upgradeUrl}.` : "";
2380
- throw new Error(`Kubb Studio refused the snapshot (status ${status})${_data?.message ? `: ${_data.message}` : ""}.${upgrade}`);
2745
+ throw new Diagnostics.Error({
2746
+ code: Diagnostics.code.inputRequestFailed,
2747
+ severity: "error",
2748
+ message: `Kubb Studio refused the snapshot (status ${status})${_data?.message ? `: ${_data.message}` : ""}.${upgrade}`,
2749
+ help: "Check the API key and your plan, then try again."
2750
+ });
2381
2751
  }
2382
- if (!_data?.snapshot) throw new Error(`Kubb Studio returned no snapshot (status ${status})`);
2752
+ if (!_data?.snapshot) throw new Diagnostics.Error({
2753
+ code: Diagnostics.code.inputRequestFailed,
2754
+ severity: "error",
2755
+ message: `Kubb Studio returned no snapshot (status ${status})`,
2756
+ help: "Try again later."
2757
+ });
2383
2758
  return _data.snapshot;
2384
2759
  }
2385
2760
  //#endregion