@kubb/studio 5.11.2 → 5.11.4

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.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { D as InstalledExtensions, P as PublishedSnapshot, W as SnapshotUploadMetadata, a as AgentConnectInfo, c as AgentPermissions, d as ConfigEditOutcome, s as AgentPermission, u as ConfigEdit } from "./protocol-BUZ91Gd4.cjs";
1
+ import { E as InstalledExtensions, N as PublishedSnapshot, U as SnapshotUploadMetadata, a as AgentConnectInfo, c as AgentPermissions, d as ConfigEditOutcome, s as AgentPermission, u as ConfigEdit } from "./protocol-a_DcTFba.cjs";
2
2
  import { Config, Hookable, KubbHooks } from "@kubb/core";
3
3
  //#region src/connect.d.ts
4
4
  /**
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { D as InstalledExtensions, P as PublishedSnapshot, W as SnapshotUploadMetadata, a as AgentConnectInfo, c as AgentPermissions, d as ConfigEditOutcome, s as AgentPermission, u as ConfigEdit } from "./protocol-BUZ91Gd4.js";
1
+ import { E as InstalledExtensions, N as PublishedSnapshot, U as SnapshotUploadMetadata, a as AgentConnectInfo, c as AgentPermissions, d as ConfigEditOutcome, s as AgentPermission, u as ConfigEdit } from "./protocol-a_DcTFba.js";
2
2
  import { Config, Hookable, KubbHooks } from "@kubb/core";
3
3
  //#region src/connect.d.ts
4
4
  /**
package/dist/index.js CHANGED
@@ -1,14 +1,14 @@
1
- import { AGENT_INSTANCE_HEADER, AGENT_SOCKET_PATH, AgentCloseCode, AgentMessage, AgentPermission, FileSource, MAX_PAGE_LINES, MAX_SNAPSHOT_BYTES, generationEventTypes } from "./protocol.js";
1
+ import { AGENT_INSTANCE_HEADER, AGENT_SOCKET_PATH, AgentCloseCode, AgentMessage, AgentPermission, MAX_SNAPSHOT_BYTES, generationEventTypes } from "./protocol.js";
2
2
  import { createRequire } from "node:module";
3
3
  import { createHash, hash, randomUUID } from "node:crypto";
4
- import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
5
4
  import { tmpdir } from "node:os";
6
5
  import path, { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
7
6
  import process$1 from "node:process";
8
7
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
9
8
  import { pathToFileURL } from "node:url";
10
9
  import { existsSync } from "node:fs";
11
- import { Diagnostics, Hookable, cacheStorage, createKubb, fsStorage, getInputKind, isPackageName, memoryStorage, readInstalledPackages, resolveCacheDir, resolveMeta } from "@kubb/core";
10
+ import { mkdir, mkdir as mkdir$1, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
11
+ import { Diagnostics, Hookable, cacheStorage, createKubb, fsStorage, getInputKind, isPackageName, readInstalledPackages, resolveCacheDir, resolveMeta } from "@kubb/core";
12
12
  import { RpcTarget, newWebSocketRpcSession } from "capnweb";
13
13
  import { detectCodeFormat, parseExpression, parseModule } from "magicast";
14
14
  import { structuredPatch } from "diff";
@@ -117,6 +117,27 @@ async function read(path) {
117
117
  return readFile(path, { encoding: "utf8" });
118
118
  }
119
119
  /**
120
+ * Writes UTF-8 content verbatim, including empty files and surrounding whitespace.
121
+ * Uses Bun.write under Bun and creates missing parent directories under either runtime.
122
+ *
123
+ * @example
124
+ * await writeRaw('./cache/output.ts', ' // keep these bytes\n\n')
125
+ */
126
+ async function writeRaw(path, content) {
127
+ const resolved = resolve(path);
128
+ if (runtime.isBun) {
129
+ await Bun.write(resolved, content);
130
+ return;
131
+ }
132
+ try {
133
+ await writeFile(resolved, content, { encoding: "utf-8" });
134
+ } catch (error) {
135
+ if (error.code !== "ENOENT") throw error;
136
+ await mkdir(dirname(resolved), { recursive: true });
137
+ await writeFile(resolved, content, { encoding: "utf-8" });
138
+ }
139
+ }
140
+ /**
120
141
  * Resolves to `true` when `path` is `parent` itself or nested inside it. Both sides are resolved
121
142
  * to absolute paths first, so relative and `..`-containing inputs compare correctly.
122
143
  *
@@ -175,7 +196,7 @@ function getElapsedMs(hrStart) {
175
196
  }
176
197
  //#endregion
177
198
  //#region package.json
178
- var version = "5.11.2";
199
+ var version = "5.11.4";
179
200
  //#endregion
180
201
  //#region src/resolveConfig.ts
181
202
  /**
@@ -1371,21 +1392,20 @@ const hashOf = (content) => createHash("sha1").update(content).digest("hex").sli
1371
1392
  /**
1372
1393
  * The bytes `entries` keep on disk: output, written files, and kept input.
1373
1394
  */
1374
- const totalBytes = (entries) => entries.reduce((sum, { output, disk, inputBytes }) => sum + output.bytes + (disk?.bytes ?? 0) + (inputBytes ?? 0), 0);
1395
+ const totalBytes = (entries) => entries.reduce((sum, { output, inputBytes }) => sum + output.bytes + (inputBytes ?? 0), 0);
1375
1396
  function positiveNumber(value) {
1376
1397
  const parsed = Number(value);
1377
1398
  return value && Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
1378
1399
  }
1379
1400
  /**
1380
1401
  * How many generations an agent keeps and how large they may get, read from
1381
- * `KUBB_AGENT_MAX_GENERATIONS`, `KUBB_AGENT_MAX_GENERATIONS_MB` and `KUBB_AGENT_MAX_SNAPSHOT_MB`.
1402
+ * `KUBB_AGENT_MAX_GENERATIONS` and `KUBB_AGENT_MAX_GENERATIONS_MB`.
1382
1403
  * An unset or invalid value keeps the default.
1383
1404
  */
1384
1405
  function resolveGenerationLimits() {
1385
1406
  return {
1386
1407
  maxCount: Math.max(1, Math.floor(positiveNumber(process$1.env.KUBB_AGENT_MAX_GENERATIONS) ?? 8)),
1387
- maxMb: positiveNumber(process$1.env.KUBB_AGENT_MAX_GENERATIONS_MB) ?? 100,
1388
- maxSnapshotMb: positiveNumber(process$1.env.KUBB_AGENT_MAX_SNAPSHOT_MB) ?? 50
1408
+ maxMb: positiveNumber(process$1.env.KUBB_AGENT_MAX_GENERATIONS_MB) ?? 100
1389
1409
  };
1390
1410
  }
1391
1411
  /**
@@ -1432,7 +1452,7 @@ async function generate({ config, hooks, signal, allowExec }) {
1432
1452
  };
1433
1453
  }
1434
1454
  /**
1435
- * What the output directory holds on disk before a run. `undefined` when `outputPath` is not a real
1455
+ * What the output directory holds when the editor connects. `undefined` when `outputPath` is not a real
1436
1456
  * subdirectory of `root` (listing the root would take in every source file) or holds more than `maxFiles`.
1437
1457
  */
1438
1458
  async function listDisk({ root, outputPath, maxFiles }) {
@@ -1452,8 +1472,8 @@ async function listDisk({ root, outputPath, maxFiles }) {
1452
1472
  /**
1453
1473
  * Keeps recent generations by job id in `storage`, with an index next to them so a store on disk
1454
1474
  * survives a restart. A generation is only ever looked up by its job id, so on a pooled sandbox one
1455
- * tenant never reaches another's output. The oldest go past `maxCount` or `maxMb`, but the newest
1456
- * always stays.
1475
+ * tenant never reaches another's output. Initial disk output and generated jobs each get their
1476
+ * own `maxCount` and `maxMb` budget. Each budget always keeps its newest entry.
1457
1477
  */
1458
1478
  function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now }) {
1459
1479
  let index;
@@ -1472,7 +1492,7 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
1472
1492
  /**
1473
1493
  * Copies `files` into the store as one set of `jobId`. Above `maxSetMb` only the hashes are kept.
1474
1494
  */
1475
- async function keep({ jobId, source, files, maxSetMb, normalize = source === FileSource.OUTPUT }) {
1495
+ async function keep({ jobId, files, maxSetMb }) {
1476
1496
  const maxSetBytes = maxSetMb * MB;
1477
1497
  const hashes = {};
1478
1498
  let bytes = 0;
@@ -1483,14 +1503,13 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
1483
1503
  if (path.split("/").includes("..")) return;
1484
1504
  const content = await files.storage.readItem(resolve(files.root, path));
1485
1505
  if (content === null) return;
1486
- if (normalize && !content.trim()) return;
1487
- hashes[path] = hashOf(normalize ? `${content.trim()}\n` : content);
1506
+ hashes[path] = hashOf(content);
1488
1507
  bytes += Buffer.byteLength(content);
1489
- if (bytes <= maxSetBytes) await storage.writeItem(`${dirOf(jobId)}${source}/${path}`, content);
1508
+ if (bytes <= maxSetBytes) await storage.writeItem(`${dirOf(jobId)}output/${path}`, content);
1490
1509
  }
1491
1510
  });
1492
1511
  if (bytes > maxSetBytes) {
1493
- await storage.empty(`${dirOf(jobId)}${source}/`);
1512
+ await storage.empty(`${dirOf(jobId)}output/`);
1494
1513
  return {
1495
1514
  paths: [],
1496
1515
  hashes,
@@ -1506,10 +1525,50 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
1506
1525
  async function drop(jobId) {
1507
1526
  await storage.empty(dirOf(jobId));
1508
1527
  }
1528
+ async function add(generation) {
1529
+ const current = await load();
1530
+ for (const expired of current.filter((entry) => !isLive(entry))) await drop(expired.jobId);
1531
+ const live = current.filter((entry) => entry.jobId !== generation.jobId && isLive(entry));
1532
+ const sharesBudget = (entry) => Boolean(entry.initial) === Boolean(generation.initial);
1533
+ const entries = live.filter(sharesBudget);
1534
+ entries.push({
1535
+ ...generation,
1536
+ keptAt: now()
1537
+ });
1538
+ while (entries.length > 1 && (entries.length > maxCount || totalBytes(entries) > maxMb * MB)) await drop(entries.shift().jobId);
1539
+ index = [...live.filter((entry) => !sharesBudget(entry)), ...entries];
1540
+ await storage.writeItem(INDEX_KEY, JSON.stringify(index));
1541
+ }
1509
1542
  return {
1510
1543
  keep,
1511
1544
  drop,
1512
1545
  /**
1546
+ * Captures initial disk output, reusing a retained ID when its hashes and readable paths match.
1547
+ * A temporary candidate is discarded on reuse, so reconnects do not consume retention slots.
1548
+ */
1549
+ async keepInitial(files) {
1550
+ const jobId = randomUUID();
1551
+ const output = await keep({
1552
+ jobId,
1553
+ files,
1554
+ maxSetMb: maxMb
1555
+ });
1556
+ const hashes = Object.entries(output.hashes);
1557
+ const previous = (await load()).findLast((entry) => entry.initial && isLive(entry) && entry.output.paths.length === output.paths.length && Object.keys(entry.output.hashes).length === hashes.length && hashes.every(([path, hash]) => entry.output.hashes[path] === hash));
1558
+ if (previous) {
1559
+ await drop(jobId);
1560
+ await add(previous);
1561
+ return previous;
1562
+ }
1563
+ const generation = {
1564
+ jobId,
1565
+ output,
1566
+ initial: true
1567
+ };
1568
+ await add(generation);
1569
+ return generation;
1570
+ },
1571
+ /**
1513
1572
  * Keeps the `InputNode` of `jobId` and returns its size in bytes, or 0 when it keeps none. A ref schema loses
1514
1573
  * the copy of its target, which `input.schemas` holds: with it, a large spec outgrows what `JSON.stringify` can return.
1515
1574
  */
@@ -1537,30 +1596,19 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
1537
1596
  return stored === null ? void 0 : JSON.parse(stored);
1538
1597
  },
1539
1598
  get: async (jobId) => (await load()).find((generation) => generation.jobId === jobId && isLive(generation)),
1540
- async add(generation) {
1541
- const current = await load();
1542
- for (const expired of current.filter((entry) => !isLive(entry))) await drop(expired.jobId);
1543
- const entries = current.filter((entry) => entry.jobId !== generation.jobId && isLive(entry));
1544
- entries.push({
1545
- ...generation,
1546
- keptAt: now()
1547
- });
1548
- while (entries.length > 1 && (entries.length > maxCount || totalBytes(entries) > maxMb * MB)) await drop(entries.shift().jobId);
1549
- index = entries;
1550
- await storage.writeItem(INDEX_KEY, JSON.stringify(entries));
1551
- },
1599
+ add,
1552
1600
  /**
1553
1601
  * Reads the requested paths the set holds, skipping any it does not, one page of lines when `limit` is given.
1554
1602
  */
1555
- async read({ generation, source, paths, cursor = 0, limit }) {
1556
- const kept = new Set(generation[source]?.paths);
1603
+ async read({ generation, paths, cursor = 0, limit }) {
1604
+ const kept = new Set(generation.output.paths);
1557
1605
  const files = {};
1558
1606
  const pages = {};
1559
1607
  await inParallel({
1560
1608
  items: paths.filter((path) => kept.has(path)),
1561
1609
  limit: READ_CONCURRENCY,
1562
1610
  run: async (path) => {
1563
- const content = await storage.readItem(`${dirOf(generation.jobId)}${source}/${path}`);
1611
+ const content = await storage.readItem(`${dirOf(generation.jobId)}output/${path}`);
1564
1612
  if (content === null) return;
1565
1613
  if (limit === void 0) {
1566
1614
  files[path] = content;
@@ -1835,6 +1883,25 @@ function isValidPage({ cursor, limit }) {
1835
1883
  if (cursor !== void 0 && (!Number.isSafeInteger(cursor) || cursor < 0)) return false;
1836
1884
  return Number.isSafeInteger(limit) && limit >= 1 && limit <= 2e3;
1837
1885
  }
1886
+ /**
1887
+ * Maps generated paths into a temporary directory while keeping the project root for input resolution.
1888
+ */
1889
+ function stagingStorage({ root, directory }) {
1890
+ const storage = fsStorage();
1891
+ const target = (key) => {
1892
+ const relative = path.relative(root, path.resolve(root, key));
1893
+ if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) throw new Error("Output path escapes the project root");
1894
+ return path.join(directory, relative);
1895
+ };
1896
+ return {
1897
+ name: "fs",
1898
+ readItem: (key) => storage.readItem(target(key)),
1899
+ writeItem: (key, value) => storage.writeItem(target(key), value),
1900
+ removeItem: (key) => storage.removeItem(target(key)),
1901
+ readKeys: (base) => storage.readKeys(base ? target(base) : directory),
1902
+ empty: (base) => base ? storage.empty(target(base)) : Promise.resolve()
1903
+ };
1904
+ }
1838
1905
  var GenerationRunTarget = class extends RpcTarget {
1839
1906
  #run;
1840
1907
  constructor(run) {
@@ -1859,6 +1926,7 @@ var Agent = class extends RpcTarget {
1859
1926
  #session;
1860
1927
  #limits = resolveGenerationLimits();
1861
1928
  #store;
1929
+ #sandboxCacheRoot = path.join(tmpdir(), `kubb-studio-${randomUUID()}`);
1862
1930
  #diffs = /* @__PURE__ */ new Map();
1863
1931
  constructor({ options, session }) {
1864
1932
  super();
@@ -1880,13 +1948,19 @@ var Agent = class extends RpcTarget {
1880
1948
  }
1881
1949
  get #generations() {
1882
1950
  this.#store ??= createGenerationStore({
1883
- storage: this.#sandbox ? memoryStorage() : cacheStorage({ root: this.#options.root }),
1951
+ storage: cacheStorage({ root: this.#sandbox ? this.#sandboxCacheRoot : this.#options.root }),
1884
1952
  maxCount: this.#limits.maxCount,
1885
1953
  maxMb: this.#limits.maxMb,
1886
1954
  ttlMs: this.#sandbox ? SANDBOX_GENERATION_TTL_MS : void 0
1887
1955
  });
1888
1956
  return this.#store;
1889
1957
  }
1958
+ async [Symbol.asyncDispose]() {
1959
+ if (this.#sandbox) await rm(resolveCacheDir(this.#sandboxCacheRoot), {
1960
+ recursive: true,
1961
+ force: true
1962
+ });
1963
+ }
1890
1964
  #warn({ message, permission }) {
1891
1965
  return emit(this.#options.hooks, "studio:warn", {
1892
1966
  message,
@@ -1998,18 +2072,7 @@ var Agent = class extends RpcTarget {
1998
2072
  maxFiles: DISK_SNAPSHOT_MAX_FILES
1999
2073
  });
2000
2074
  if (!files) return void 0;
2001
- const jobId = randomUUID();
2002
- const output = await this.#generations.keep({
2003
- jobId,
2004
- source: FileSource.OUTPUT,
2005
- files,
2006
- maxSetMb: this.#limits.maxSnapshotMb,
2007
- normalize: false
2008
- });
2009
- await this.#generations.add({
2010
- jobId,
2011
- output
2012
- });
2075
+ const { jobId, output } = await this.#generations.keepInitial(files);
2013
2076
  return {
2014
2077
  jobId,
2015
2078
  files: output.paths,
@@ -2033,7 +2096,9 @@ var Agent = class extends RpcTarget {
2033
2096
  }
2034
2097
  });
2035
2098
  }
2036
- /** Runs one job and keeps its output, and its input with `allowRead`, for later reads. */
2099
+ /**
2100
+ * Keeps a job's output and permitted parsed input, then cleans up its temporary generation directories.
2101
+ */
2037
2102
  async #runGeneration(input, controller) {
2038
2103
  this.#assertConnected();
2039
2104
  if (this.#session.closed) throw new Diagnostics.Error({
@@ -2041,7 +2106,7 @@ var Agent = class extends RpcTarget {
2041
2106
  severity: "error",
2042
2107
  message: "Connection is closed"
2043
2108
  });
2044
- if (this.#session.active) return this.#refuse({ message: "A generation is already in progress, please wait for it to finish" });
2109
+ if (this.#session.active) return this.#refuse({ message: AgentMessage.GENERATION_IN_PROGRESS });
2045
2110
  const settled = Promise.withResolvers();
2046
2111
  this.#session.active = {
2047
2112
  jobId: input.jobId,
@@ -2049,11 +2114,14 @@ var Agent = class extends RpcTarget {
2049
2114
  settled: settled.promise
2050
2115
  };
2051
2116
  let root = this.#options.root;
2117
+ let outputRoot;
2052
2118
  try {
2053
2119
  if (this.#sandbox) root = await mkdtemp(path.join(tmpdir(), "kubb-job-"));
2120
+ if (!this.#sandbox && !this.#permissions.allowWrite) outputRoot = await mkdtemp(path.join(tmpdir(), "kubb-output-"));
2054
2121
  return await this.#generate({
2055
2122
  input,
2056
2123
  root,
2124
+ outputRoot,
2057
2125
  signal: controller.signal
2058
2126
  });
2059
2127
  } finally {
@@ -2064,15 +2132,21 @@ var Agent = class extends RpcTarget {
2064
2132
  recursive: true,
2065
2133
  force: true
2066
2134
  })]).catch(() => {});
2135
+ if (outputRoot) await rm(outputRoot, {
2136
+ recursive: true,
2137
+ force: true
2138
+ });
2067
2139
  this.#session.active = void 0;
2068
2140
  settled.resolve();
2069
2141
  }
2070
2142
  }
2071
- /** Merges Studio's config over the disk config, refuses output paths outside the output folder, then generates in `root`. */
2072
- async #generate({ input, root, signal }) {
2143
+ /**
2144
+ * Merges Studio's config over the project config and confines generation to the configured output folder.
2145
+ */
2146
+ async #generate({ input, root, outputRoot, signal }) {
2073
2147
  const { jobId, config: patch } = input;
2074
2148
  const studioInput = this.#sandbox ? patch?.input : void 0;
2075
- if (this.#sandbox && (typeof studioInput !== "string" || !studioInput.trim() || getInputKind(studioInput) !== "inline")) return this.#refuse({ message: "A sandbox agent generates only from the OpenAPI document content Studio sends, not from a file path or URL" });
2149
+ if (this.#sandbox && (typeof studioInput !== "string" || !studioInput.trim() || getInputKind(studioInput) !== "inline")) return this.#refuse({ message: AgentMessage.SANDBOX_INPUT_ONLY });
2076
2150
  const [config, extensions] = await Promise.all([this.#options.loadConfig(), this.#installedExtensions()]);
2077
2151
  const [plugins, adapter, parsers] = await Promise.all([
2078
2152
  mergePlugins({
@@ -2100,37 +2174,28 @@ var Agent = class extends RpcTarget {
2100
2174
  });
2101
2175
  const activePlugins = plugins ?? config.plugins;
2102
2176
  if (patch?.input && !this.#sandbox) await emit(this.#options.hooks, "kubb:warn", { message: "Ignored the spec from Studio: generating from a Studio spec is only available to a sandbox agent" });
2103
- const diskFiles = !this.#sandbox && this.#permissions.allowRead ? await listDisk({
2104
- root,
2105
- outputPath: config.output.path,
2106
- maxFiles: DISK_SNAPSHOT_MAX_FILES
2107
- }) : void 0;
2108
- const disk = diskFiles ? await this.#generations.keep({
2109
- jobId,
2110
- source: FileSource.DISK,
2111
- files: diskFiles,
2112
- maxSetMb: this.#limits.maxSnapshotMb
2113
- }) : void 0;
2114
2177
  const files = await generate({
2115
2178
  config: {
2116
2179
  ...config,
2117
2180
  root,
2118
2181
  input: studioInput ?? config.input,
2119
- storage: this.#permissions.allowWrite ? fsStorage() : memoryStorage(),
2182
+ storage: outputRoot ? stagingStorage({
2183
+ root,
2184
+ directory: outputRoot
2185
+ }) : fsStorage(),
2120
2186
  plugins: activePlugins,
2121
2187
  adapter,
2122
2188
  parsers
2123
2189
  },
2124
2190
  hooks: this.#options.hooks,
2125
2191
  signal,
2126
- allowExec: this.#permissions.allowExec
2192
+ allowExec: this.#permissions.allowWrite && this.#permissions.allowExec
2127
2193
  }).catch(async (error) => {
2128
2194
  await this.#generations.drop(jobId);
2129
2195
  throw error;
2130
2196
  });
2131
2197
  const output = await this.#generations.keep({
2132
2198
  jobId,
2133
- source: FileSource.OUTPUT,
2134
2199
  files,
2135
2200
  maxSetMb: this.#limits.maxMb
2136
2201
  });
@@ -2143,16 +2208,14 @@ var Agent = class extends RpcTarget {
2143
2208
  await this.#generations.add({
2144
2209
  jobId,
2145
2210
  output,
2146
- disk,
2147
2211
  inputBytes
2148
2212
  });
2149
- await this.#logCommand("generate", `${activePlugins.length} plugin${activePlugins.length === 1 ? "" : "s"}, ${this.#permissions.allowWrite ? "written to disk" : "in memory"}${studioInput !== void 0 ? ", from a Studio spec" : ""}`);
2213
+ await this.#logCommand("generate", `${activePlugins.length} plugin${activePlugins.length === 1 ? "" : "s"}, ${this.#permissions.allowWrite ? "written to disk" : "staged on disk"}${studioInput !== void 0 ? ", from a Studio spec" : ""}`);
2150
2214
  return {
2151
2215
  status: "success",
2152
2216
  files: [...files.paths],
2153
2217
  fileCount: files.paths.size,
2154
- hashes: output.hashes,
2155
- disk: disk ? { hashes: disk.hashes } : void 0
2218
+ hashes: output.hashes
2156
2219
  };
2157
2220
  }
2158
2221
  async saveConfig(input) {
@@ -2205,7 +2268,7 @@ var Agent = class extends RpcTarget {
2205
2268
  message: AgentMessage.READ_AST_NOT_ALLOWED,
2206
2269
  permission: AgentPermission.READ_AST
2207
2270
  });
2208
- if (typeof input.jobId !== "string" || !input.jobId) return this.#refuse({ message: "The request must name a job" });
2271
+ if (typeof input.jobId !== "string" || !input.jobId) return this.#refuse({ message: AgentMessage.JOB_REQUIRED });
2209
2272
  const generation = await this.#generations.get(input.jobId);
2210
2273
  if (!generation) return this.#refuse({ message: AgentMessage.GENERATION_GONE });
2211
2274
  const kept = generation.inputBytes ? await this.#generations.readInput(input.jobId) : void 0;
@@ -2222,17 +2285,14 @@ var Agent = class extends RpcTarget {
2222
2285
  message: AgentMessage.READ_NOT_ALLOWED,
2223
2286
  permission: AgentPermission.READ
2224
2287
  });
2225
- if (!Array.isArray(input.paths) || typeof input.jobId !== "string" || !input.jobId) return this.#refuse({ message: "The request must name a job and carry paths" });
2288
+ if (!Array.isArray(input.paths) || typeof input.jobId !== "string" || !input.jobId) return this.#refuse({ message: AgentMessage.FILE_REQUEST_INVALID });
2226
2289
  const { paths } = input;
2227
- if (paths.length > 50) return this.#refuse({ message: `At most 50 paths may be requested at once` });
2290
+ if (paths.length > 50) return this.#refuse({ message: AgentMessage.TOO_MANY_FILES });
2291
+ if (!isValidPage(input)) return this.#refuse({ message: AgentMessage.INVALID_PAGE });
2228
2292
  const generation = await this.#generations.get(input.jobId);
2229
2293
  if (!generation) return this.#refuse({ message: AgentMessage.GENERATION_GONE });
2230
- 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` });
2231
- const source = input.source === FileSource.DISK ? FileSource.DISK : FileSource.OUTPUT;
2232
- if (!generation[source]) return this.#refuse({ message: "This agent kept no snapshot of the files on disk for that job" });
2233
2294
  const result = await this.#generations.read({
2234
2295
  generation,
2235
- source,
2236
2296
  paths,
2237
2297
  cursor: input.cursor,
2238
2298
  limit: input.limit
@@ -2250,26 +2310,22 @@ var Agent = class extends RpcTarget {
2250
2310
  permission: AgentPermission.READ
2251
2311
  });
2252
2312
  const { jobId, path, base, cursor = 0, limit } = input;
2253
- 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" });
2313
+ if (typeof jobId !== "string" || !jobId || typeof path !== "string" || !path || typeof base?.jobId !== "string" || !base.jobId) return this.#refuse({ message: AgentMessage.DIFF_REQUEST_INVALID });
2254
2314
  if (!isValidPage({
2255
2315
  cursor,
2256
2316
  limit
2257
- })) 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` });
2317
+ })) return this.#refuse({ message: AgentMessage.INVALID_PAGE });
2258
2318
  const [generation, baseGeneration] = await Promise.all([this.#generations.get(jobId), this.#generations.get(base.jobId)]);
2259
2319
  if (!generation || !baseGeneration) return this.#refuse({ message: AgentMessage.GENERATION_GONE });
2260
- const baseSource = base.source === FileSource.DISK ? FileSource.DISK : FileSource.OUTPUT;
2261
- if (!baseGeneration[baseSource]) return this.#refuse({ message: "This agent kept no snapshot of the files on disk for that job" });
2262
2320
  const [after, before] = await Promise.all([this.#generations.read({
2263
2321
  generation,
2264
- source: FileSource.OUTPUT,
2265
2322
  paths: [path]
2266
2323
  }), this.#generations.read({
2267
2324
  generation: baseGeneration,
2268
- source: baseSource,
2269
2325
  paths: [path]
2270
2326
  })]);
2271
2327
  const afterContent = after.files[path];
2272
- if (afterContent === void 0 && path in generation.output.hashes) return this.#refuse({ message: "This agent kept only the hashes of that job" });
2328
+ if (afterContent === void 0 && path in generation.output.hashes) return this.#refuse({ message: AgentMessage.HASHES_ONLY });
2273
2329
  const beforeContent = before.files[path];
2274
2330
  if (beforeContent === void 0) return {
2275
2331
  hunks: [],
@@ -2286,7 +2342,6 @@ var Agent = class extends RpcTarget {
2286
2342
  const key = JSON.stringify([
2287
2343
  jobId,
2288
2344
  base.jobId,
2289
- baseSource,
2290
2345
  path
2291
2346
  ]);
2292
2347
  let diff = this.#diffs.get(key);
@@ -2343,14 +2398,15 @@ async function runSession({ options, openRpc, url, instanceId }) {
2343
2398
  connected: Promise.withResolvers(),
2344
2399
  closed: false
2345
2400
  };
2401
+ const agent = new Agent({
2402
+ options,
2403
+ session
2404
+ });
2346
2405
  const rpc = openRpc({
2347
2406
  url,
2348
2407
  token,
2349
2408
  instanceId,
2350
- agent: new Agent({
2351
- options,
2352
- session
2353
- })
2409
+ agent
2354
2410
  });
2355
2411
  signal?.addEventListener("abort", rpc.close, { once: true });
2356
2412
  let timer;
@@ -2385,6 +2441,7 @@ async function runSession({ options, openRpc, url, instanceId }) {
2385
2441
  await emit(hooks, "studio:warn", { message: "Canceled the running generation: the connection to Kubb Studio closed" });
2386
2442
  }
2387
2443
  await active?.settled;
2444
+ await agent[Symbol.asyncDispose]();
2388
2445
  return {
2389
2446
  ...close,
2390
2447
  connected: session.info !== void 0
@@ -2692,10 +2749,9 @@ async function createSnapshotPackage({ files, packageInfo }) {
2692
2749
  const root = await mkdtemp(join(tmpdir(), "kubb-snapshot-"));
2693
2750
  const dist = join(root, "dist");
2694
2751
  try {
2695
- await mkdir(dist);
2752
+ await mkdir$1(dist);
2696
2753
  await Promise.all([...sources].map(async ([path, { content }]) => {
2697
- await mkdir(dirname(join(root, path)), { recursive: true });
2698
- await writeFile(join(root, path), content);
2754
+ await writeRaw(join(root, path), content);
2699
2755
  }));
2700
2756
  const entry = [...sources.keys()].filter((path) => /\.(?:[cm]?[jt]sx?)$/.test(path)).map((path) => join(root, path));
2701
2757
  if (entry.length) {
@@ -2735,7 +2791,7 @@ async function createSnapshotPackage({ files, packageInfo }) {
2735
2791
  const chunks = [...new Map([
2736
2792
  ["package.json", JSON.stringify(manifest, null, 2)],
2737
2793
  ...[...sources].map(([path, { content }]) => [path, content]),
2738
- ...await Promise.all(built.map(async (key) => [`dist/${key}`, await readFile(join(dist, key), "utf8")]))
2794
+ ...await Promise.all(built.map(async (key) => [`dist/${key}`, await read(join(dist, key))]))
2739
2795
  ])].flatMap(([path, content]) => {
2740
2796
  const bytes = Buffer.from(content);
2741
2797
  return [
@@ -2766,7 +2822,10 @@ async function createSnapshot({ studioUrl = defaultStudioUrl, apiKey, config, ho
2766
2822
  path: "/api/snapshots"
2767
2823
  });
2768
2824
  const generated = await generate({
2769
- config,
2825
+ config: {
2826
+ ...config,
2827
+ storage: fsStorage()
2828
+ },
2770
2829
  hooks,
2771
2830
  signal,
2772
2831
  allowExec: true