@kubb/studio 5.11.3 → 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-DpxvWX9E.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-DpxvWX9E.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_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.3";
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 }) {
@@ -1449,38 +1469,11 @@ async function listDisk({ root, outputPath, maxFiles }) {
1449
1469
  paths: new Set(paths)
1450
1470
  };
1451
1471
  }
1452
- async function readDiskFiles({ files: disk, paths, cursor = 0, limit }) {
1453
- const files = {};
1454
- const pages = {};
1455
- await inParallel({
1456
- items: paths.filter((path) => disk.paths.has(path)),
1457
- limit: READ_CONCURRENCY,
1458
- run: async (path) => {
1459
- const content = await disk.storage.readItem(resolve(disk.root, path));
1460
- if (content === null) return;
1461
- if (limit === void 0) {
1462
- files[path] = content;
1463
- return;
1464
- }
1465
- const lines = content.split(/\r\n|\r|\n/);
1466
- const end = Math.min(cursor + limit, lines.length);
1467
- files[path] = lines.slice(cursor, end).join("\n");
1468
- pages[path] = {
1469
- nextCursor: end < lines.length ? end : null,
1470
- totalLines: lines.length
1471
- };
1472
- }
1473
- });
1474
- return limit === void 0 ? { files } : {
1475
- files,
1476
- pages
1477
- };
1478
- }
1479
1472
  /**
1480
1473
  * Keeps recent generations by job id in `storage`, with an index next to them so a store on disk
1481
1474
  * survives a restart. A generation is only ever looked up by its job id, so on a pooled sandbox one
1482
- * tenant never reaches another's output. The oldest go past `maxCount` or `maxMb`, but the newest
1483
- * 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.
1484
1477
  */
1485
1478
  function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now }) {
1486
1479
  let index;
@@ -1499,7 +1492,7 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
1499
1492
  /**
1500
1493
  * Copies `files` into the store as one set of `jobId`. Above `maxSetMb` only the hashes are kept.
1501
1494
  */
1502
- async function keep({ jobId, source, files, maxSetMb, normalize = source === FileSource.OUTPUT }) {
1495
+ async function keep({ jobId, files, maxSetMb }) {
1503
1496
  const maxSetBytes = maxSetMb * MB;
1504
1497
  const hashes = {};
1505
1498
  let bytes = 0;
@@ -1510,14 +1503,13 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
1510
1503
  if (path.split("/").includes("..")) return;
1511
1504
  const content = await files.storage.readItem(resolve(files.root, path));
1512
1505
  if (content === null) return;
1513
- if (normalize && !content.trim()) return;
1514
- hashes[path] = hashOf(normalize ? `${content.trim()}\n` : content);
1506
+ hashes[path] = hashOf(content);
1515
1507
  bytes += Buffer.byteLength(content);
1516
- if (bytes <= maxSetBytes) await storage.writeItem(`${dirOf(jobId)}${source}/${path}`, content);
1508
+ if (bytes <= maxSetBytes) await storage.writeItem(`${dirOf(jobId)}output/${path}`, content);
1517
1509
  }
1518
1510
  });
1519
1511
  if (bytes > maxSetBytes) {
1520
- await storage.empty(`${dirOf(jobId)}${source}/`);
1512
+ await storage.empty(`${dirOf(jobId)}output/`);
1521
1513
  return {
1522
1514
  paths: [],
1523
1515
  hashes,
@@ -1533,10 +1525,50 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
1533
1525
  async function drop(jobId) {
1534
1526
  await storage.empty(dirOf(jobId));
1535
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
+ }
1536
1542
  return {
1537
1543
  keep,
1538
1544
  drop,
1539
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
+ /**
1540
1572
  * Keeps the `InputNode` of `jobId` and returns its size in bytes, or 0 when it keeps none. A ref schema loses
1541
1573
  * the copy of its target, which `input.schemas` holds: with it, a large spec outgrows what `JSON.stringify` can return.
1542
1574
  */
@@ -1564,30 +1596,19 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
1564
1596
  return stored === null ? void 0 : JSON.parse(stored);
1565
1597
  },
1566
1598
  get: async (jobId) => (await load()).find((generation) => generation.jobId === jobId && isLive(generation)),
1567
- async add(generation) {
1568
- const current = await load();
1569
- for (const expired of current.filter((entry) => !isLive(entry))) await drop(expired.jobId);
1570
- const entries = current.filter((entry) => entry.jobId !== generation.jobId && isLive(entry));
1571
- entries.push({
1572
- ...generation,
1573
- keptAt: now()
1574
- });
1575
- while (entries.length > 1 && (entries.length > maxCount || totalBytes(entries) > maxMb * MB)) await drop(entries.shift().jobId);
1576
- index = entries;
1577
- await storage.writeItem(INDEX_KEY, JSON.stringify(entries));
1578
- },
1599
+ add,
1579
1600
  /**
1580
1601
  * Reads the requested paths the set holds, skipping any it does not, one page of lines when `limit` is given.
1581
1602
  */
1582
- async read({ generation, source, paths, cursor = 0, limit }) {
1583
- const kept = new Set(generation[source]?.paths);
1603
+ async read({ generation, paths, cursor = 0, limit }) {
1604
+ const kept = new Set(generation.output.paths);
1584
1605
  const files = {};
1585
1606
  const pages = {};
1586
1607
  await inParallel({
1587
1608
  items: paths.filter((path) => kept.has(path)),
1588
1609
  limit: READ_CONCURRENCY,
1589
1610
  run: async (path) => {
1590
- const content = await storage.readItem(`${dirOf(generation.jobId)}${source}/${path}`);
1611
+ const content = await storage.readItem(`${dirOf(generation.jobId)}output/${path}`);
1591
1612
  if (content === null) return;
1592
1613
  if (limit === void 0) {
1593
1614
  files[path] = content;
@@ -1862,6 +1883,25 @@ function isValidPage({ cursor, limit }) {
1862
1883
  if (cursor !== void 0 && (!Number.isSafeInteger(cursor) || cursor < 0)) return false;
1863
1884
  return Number.isSafeInteger(limit) && limit >= 1 && limit <= 2e3;
1864
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
+ }
1865
1905
  var GenerationRunTarget = class extends RpcTarget {
1866
1906
  #run;
1867
1907
  constructor(run) {
@@ -1886,6 +1926,7 @@ var Agent = class extends RpcTarget {
1886
1926
  #session;
1887
1927
  #limits = resolveGenerationLimits();
1888
1928
  #store;
1929
+ #sandboxCacheRoot = path.join(tmpdir(), `kubb-studio-${randomUUID()}`);
1889
1930
  #diffs = /* @__PURE__ */ new Map();
1890
1931
  constructor({ options, session }) {
1891
1932
  super();
@@ -1907,13 +1948,19 @@ var Agent = class extends RpcTarget {
1907
1948
  }
1908
1949
  get #generations() {
1909
1950
  this.#store ??= createGenerationStore({
1910
- storage: this.#sandbox ? memoryStorage() : cacheStorage({ root: this.#options.root }),
1951
+ storage: cacheStorage({ root: this.#sandbox ? this.#sandboxCacheRoot : this.#options.root }),
1911
1952
  maxCount: this.#limits.maxCount,
1912
1953
  maxMb: this.#limits.maxMb,
1913
1954
  ttlMs: this.#sandbox ? SANDBOX_GENERATION_TTL_MS : void 0
1914
1955
  });
1915
1956
  return this.#store;
1916
1957
  }
1958
+ async [Symbol.asyncDispose]() {
1959
+ if (this.#sandbox) await rm(resolveCacheDir(this.#sandboxCacheRoot), {
1960
+ recursive: true,
1961
+ force: true
1962
+ });
1963
+ }
1917
1964
  #warn({ message, permission }) {
1918
1965
  return emit(this.#options.hooks, "studio:warn", {
1919
1966
  message,
@@ -2025,7 +2072,12 @@ var Agent = class extends RpcTarget {
2025
2072
  maxFiles: DISK_SNAPSHOT_MAX_FILES
2026
2073
  });
2027
2074
  if (!files) return void 0;
2028
- return { files: [...files.paths] };
2075
+ const { jobId, output } = await this.#generations.keepInitial(files);
2076
+ return {
2077
+ jobId,
2078
+ files: output.paths,
2079
+ hashes: output.hashes
2080
+ };
2029
2081
  }
2030
2082
  startGeneration(input) {
2031
2083
  const stream = createGenerationStream(this.#options.hooks, input.jobId);
@@ -2044,7 +2096,9 @@ var Agent = class extends RpcTarget {
2044
2096
  }
2045
2097
  });
2046
2098
  }
2047
- /** 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
+ */
2048
2102
  async #runGeneration(input, controller) {
2049
2103
  this.#assertConnected();
2050
2104
  if (this.#session.closed) throw new Diagnostics.Error({
@@ -2060,11 +2114,14 @@ var Agent = class extends RpcTarget {
2060
2114
  settled: settled.promise
2061
2115
  };
2062
2116
  let root = this.#options.root;
2117
+ let outputRoot;
2063
2118
  try {
2064
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-"));
2065
2121
  return await this.#generate({
2066
2122
  input,
2067
2123
  root,
2124
+ outputRoot,
2068
2125
  signal: controller.signal
2069
2126
  });
2070
2127
  } finally {
@@ -2075,12 +2132,18 @@ var Agent = class extends RpcTarget {
2075
2132
  recursive: true,
2076
2133
  force: true
2077
2134
  })]).catch(() => {});
2135
+ if (outputRoot) await rm(outputRoot, {
2136
+ recursive: true,
2137
+ force: true
2138
+ });
2078
2139
  this.#session.active = void 0;
2079
2140
  settled.resolve();
2080
2141
  }
2081
2142
  }
2082
- /** Merges Studio's config over the disk config, refuses output paths outside the output folder, then generates in `root`. */
2083
- 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 }) {
2084
2147
  const { jobId, config: patch } = input;
2085
2148
  const studioInput = this.#sandbox ? patch?.input : void 0;
2086
2149
  if (this.#sandbox && (typeof studioInput !== "string" || !studioInput.trim() || getInputKind(studioInput) !== "inline")) return this.#refuse({ message: AgentMessage.SANDBOX_INPUT_ONLY });
@@ -2111,37 +2174,28 @@ var Agent = class extends RpcTarget {
2111
2174
  });
2112
2175
  const activePlugins = plugins ?? config.plugins;
2113
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" });
2114
- const diskFiles = !this.#sandbox && this.#permissions.allowRead ? await listDisk({
2115
- root,
2116
- outputPath: config.output.path,
2117
- maxFiles: DISK_SNAPSHOT_MAX_FILES
2118
- }) : void 0;
2119
- const disk = diskFiles ? await this.#generations.keep({
2120
- jobId,
2121
- source: FileSource.DISK,
2122
- files: diskFiles,
2123
- maxSetMb: this.#limits.maxSnapshotMb
2124
- }) : void 0;
2125
2177
  const files = await generate({
2126
2178
  config: {
2127
2179
  ...config,
2128
2180
  root,
2129
2181
  input: studioInput ?? config.input,
2130
- storage: this.#permissions.allowWrite ? fsStorage() : memoryStorage(),
2182
+ storage: outputRoot ? stagingStorage({
2183
+ root,
2184
+ directory: outputRoot
2185
+ }) : fsStorage(),
2131
2186
  plugins: activePlugins,
2132
2187
  adapter,
2133
2188
  parsers
2134
2189
  },
2135
2190
  hooks: this.#options.hooks,
2136
2191
  signal,
2137
- allowExec: this.#permissions.allowExec
2192
+ allowExec: this.#permissions.allowWrite && this.#permissions.allowExec
2138
2193
  }).catch(async (error) => {
2139
2194
  await this.#generations.drop(jobId);
2140
2195
  throw error;
2141
2196
  });
2142
2197
  const output = await this.#generations.keep({
2143
2198
  jobId,
2144
- source: FileSource.OUTPUT,
2145
2199
  files,
2146
2200
  maxSetMb: this.#limits.maxMb
2147
2201
  });
@@ -2154,16 +2208,14 @@ var Agent = class extends RpcTarget {
2154
2208
  await this.#generations.add({
2155
2209
  jobId,
2156
2210
  output,
2157
- disk,
2158
2211
  inputBytes
2159
2212
  });
2160
- 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" : ""}`);
2161
2214
  return {
2162
2215
  status: "success",
2163
2216
  files: [...files.paths],
2164
2217
  fileCount: files.paths.size,
2165
- hashes: output.hashes,
2166
- disk: disk ? { hashes: disk.hashes } : void 0
2218
+ hashes: output.hashes
2167
2219
  };
2168
2220
  }
2169
2221
  async saveConfig(input) {
@@ -2233,35 +2285,14 @@ var Agent = class extends RpcTarget {
2233
2285
  message: AgentMessage.READ_NOT_ALLOWED,
2234
2286
  permission: AgentPermission.READ
2235
2287
  });
2236
- if (!Array.isArray(input.paths) || typeof input.jobId !== "string" || !input.jobId || !["generation", "currentDisk"].includes(input.jobType)) return this.#refuse({ message: AgentMessage.FILE_REQUEST_INVALID });
2288
+ if (!Array.isArray(input.paths) || typeof input.jobId !== "string" || !input.jobId) return this.#refuse({ message: AgentMessage.FILE_REQUEST_INVALID });
2237
2289
  const { paths } = input;
2238
2290
  if (paths.length > 50) return this.#refuse({ message: AgentMessage.TOO_MANY_FILES });
2239
2291
  if (!isValidPage(input)) return this.#refuse({ message: AgentMessage.INVALID_PAGE });
2240
- if (input.jobType === "currentDisk") {
2241
- if (this.#sandbox) return this.#refuse({ message: AgentMessage.SANDBOX_DISK_UNAVAILABLE });
2242
- const config = await this.#options.loadConfig();
2243
- const disk = await listDisk({
2244
- root: this.#options.root,
2245
- outputPath: config.output.path,
2246
- maxFiles: DISK_SNAPSHOT_MAX_FILES
2247
- });
2248
- if (!disk) return this.#refuse({ message: AgentMessage.GENERATION_GONE });
2249
- const result = await readDiskFiles({
2250
- files: disk,
2251
- paths,
2252
- cursor: input.cursor,
2253
- limit: input.limit
2254
- });
2255
- await this.#logCommand("readFiles", `read ${Object.keys(result.files).length}/${paths.length} requested file${paths.length === 1 ? "" : "s"}`);
2256
- return result;
2257
- }
2258
2292
  const generation = await this.#generations.get(input.jobId);
2259
2293
  if (!generation) return this.#refuse({ message: AgentMessage.GENERATION_GONE });
2260
- const source = input.source === FileSource.DISK ? FileSource.DISK : FileSource.OUTPUT;
2261
- if (!generation[source]) return this.#refuse({ message: AgentMessage.NO_DISK_SNAPSHOT });
2262
2294
  const result = await this.#generations.read({
2263
2295
  generation,
2264
- source,
2265
2296
  paths,
2266
2297
  cursor: input.cursor,
2267
2298
  limit: input.limit
@@ -2286,15 +2317,11 @@ var Agent = class extends RpcTarget {
2286
2317
  })) return this.#refuse({ message: AgentMessage.INVALID_PAGE });
2287
2318
  const [generation, baseGeneration] = await Promise.all([this.#generations.get(jobId), this.#generations.get(base.jobId)]);
2288
2319
  if (!generation || !baseGeneration) return this.#refuse({ message: AgentMessage.GENERATION_GONE });
2289
- const baseSource = base.source === FileSource.DISK ? FileSource.DISK : FileSource.OUTPUT;
2290
- if (!baseGeneration[baseSource]) return this.#refuse({ message: AgentMessage.NO_DISK_SNAPSHOT });
2291
2320
  const [after, before] = await Promise.all([this.#generations.read({
2292
2321
  generation,
2293
- source: FileSource.OUTPUT,
2294
2322
  paths: [path]
2295
2323
  }), this.#generations.read({
2296
2324
  generation: baseGeneration,
2297
- source: baseSource,
2298
2325
  paths: [path]
2299
2326
  })]);
2300
2327
  const afterContent = after.files[path];
@@ -2315,7 +2342,6 @@ var Agent = class extends RpcTarget {
2315
2342
  const key = JSON.stringify([
2316
2343
  jobId,
2317
2344
  base.jobId,
2318
- baseSource,
2319
2345
  path
2320
2346
  ]);
2321
2347
  let diff = this.#diffs.get(key);
@@ -2372,14 +2398,15 @@ async function runSession({ options, openRpc, url, instanceId }) {
2372
2398
  connected: Promise.withResolvers(),
2373
2399
  closed: false
2374
2400
  };
2401
+ const agent = new Agent({
2402
+ options,
2403
+ session
2404
+ });
2375
2405
  const rpc = openRpc({
2376
2406
  url,
2377
2407
  token,
2378
2408
  instanceId,
2379
- agent: new Agent({
2380
- options,
2381
- session
2382
- })
2409
+ agent
2383
2410
  });
2384
2411
  signal?.addEventListener("abort", rpc.close, { once: true });
2385
2412
  let timer;
@@ -2414,6 +2441,7 @@ async function runSession({ options, openRpc, url, instanceId }) {
2414
2441
  await emit(hooks, "studio:warn", { message: "Canceled the running generation: the connection to Kubb Studio closed" });
2415
2442
  }
2416
2443
  await active?.settled;
2444
+ await agent[Symbol.asyncDispose]();
2417
2445
  return {
2418
2446
  ...close,
2419
2447
  connected: session.info !== void 0
@@ -2721,10 +2749,9 @@ async function createSnapshotPackage({ files, packageInfo }) {
2721
2749
  const root = await mkdtemp(join(tmpdir(), "kubb-snapshot-"));
2722
2750
  const dist = join(root, "dist");
2723
2751
  try {
2724
- await mkdir(dist);
2752
+ await mkdir$1(dist);
2725
2753
  await Promise.all([...sources].map(async ([path, { content }]) => {
2726
- await mkdir(dirname(join(root, path)), { recursive: true });
2727
- await writeFile(join(root, path), content);
2754
+ await writeRaw(join(root, path), content);
2728
2755
  }));
2729
2756
  const entry = [...sources.keys()].filter((path) => /\.(?:[cm]?[jt]sx?)$/.test(path)).map((path) => join(root, path));
2730
2757
  if (entry.length) {
@@ -2764,7 +2791,7 @@ async function createSnapshotPackage({ files, packageInfo }) {
2764
2791
  const chunks = [...new Map([
2765
2792
  ["package.json", JSON.stringify(manifest, null, 2)],
2766
2793
  ...[...sources].map(([path, { content }]) => [path, content]),
2767
- ...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))]))
2768
2795
  ])].flatMap(([path, content]) => {
2769
2796
  const bytes = Buffer.from(content);
2770
2797
  return [
@@ -2795,7 +2822,10 @@ async function createSnapshot({ studioUrl = defaultStudioUrl, apiKey, config, ho
2795
2822
  path: "/api/snapshots"
2796
2823
  });
2797
2824
  const generated = await generate({
2798
- config,
2825
+ config: {
2826
+ ...config,
2827
+ storage: fsStorage()
2828
+ },
2799
2829
  hooks,
2800
2830
  signal,
2801
2831
  allowExec: true