@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.cjs CHANGED
@@ -23,7 +23,6 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
23
23
  //#endregion
24
24
  const require_protocol = require("./protocol.cjs");
25
25
  let node_crypto = require("node:crypto");
26
- let node_fs_promises = require("node:fs/promises");
27
26
  let node_os = require("node:os");
28
27
  let node_path = require("node:path");
29
28
  node_path = __toESM(node_path, 1);
@@ -32,6 +31,7 @@ node_process = __toESM(node_process, 1);
32
31
  let node_timers_promises = require("node:timers/promises");
33
32
  let node_url = require("node:url");
34
33
  let node_fs = require("node:fs");
34
+ let node_fs_promises = require("node:fs/promises");
35
35
  let _kubb_core = require("@kubb/core");
36
36
  let capnweb = require("capnweb");
37
37
  let magicast = require("magicast");
@@ -141,6 +141,27 @@ async function read(path) {
141
141
  return (0, node_fs_promises.readFile)(path, { encoding: "utf8" });
142
142
  }
143
143
  /**
144
+ * Writes UTF-8 content verbatim, including empty files and surrounding whitespace.
145
+ * Uses Bun.write under Bun and creates missing parent directories under either runtime.
146
+ *
147
+ * @example
148
+ * await writeRaw('./cache/output.ts', ' // keep these bytes\n\n')
149
+ */
150
+ async function writeRaw(path, content) {
151
+ const resolved = (0, node_path.resolve)(path);
152
+ if (runtime.isBun) {
153
+ await Bun.write(resolved, content);
154
+ return;
155
+ }
156
+ try {
157
+ await (0, node_fs_promises.writeFile)(resolved, content, { encoding: "utf-8" });
158
+ } catch (error) {
159
+ if (error.code !== "ENOENT") throw error;
160
+ await (0, node_fs_promises.mkdir)((0, node_path.dirname)(resolved), { recursive: true });
161
+ await (0, node_fs_promises.writeFile)(resolved, content, { encoding: "utf-8" });
162
+ }
163
+ }
164
+ /**
144
165
  * Resolves to `true` when `path` is `parent` itself or nested inside it. Both sides are resolved
145
166
  * to absolute paths first, so relative and `..`-containing inputs compare correctly.
146
167
  *
@@ -199,7 +220,7 @@ function getElapsedMs(hrStart) {
199
220
  }
200
221
  //#endregion
201
222
  //#region package.json
202
- var version = "5.11.3";
223
+ var version = "5.11.4";
203
224
  //#endregion
204
225
  //#region src/resolveConfig.ts
205
226
  /**
@@ -1395,21 +1416,20 @@ const hashOf = (content) => (0, node_crypto.createHash)("sha1").update(content).
1395
1416
  /**
1396
1417
  * The bytes `entries` keep on disk: output, written files, and kept input.
1397
1418
  */
1398
- const totalBytes = (entries) => entries.reduce((sum, { output, disk, inputBytes }) => sum + output.bytes + (disk?.bytes ?? 0) + (inputBytes ?? 0), 0);
1419
+ const totalBytes = (entries) => entries.reduce((sum, { output, inputBytes }) => sum + output.bytes + (inputBytes ?? 0), 0);
1399
1420
  function positiveNumber(value) {
1400
1421
  const parsed = Number(value);
1401
1422
  return value && Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
1402
1423
  }
1403
1424
  /**
1404
1425
  * How many generations an agent keeps and how large they may get, read from
1405
- * `KUBB_AGENT_MAX_GENERATIONS`, `KUBB_AGENT_MAX_GENERATIONS_MB` and `KUBB_AGENT_MAX_SNAPSHOT_MB`.
1426
+ * `KUBB_AGENT_MAX_GENERATIONS` and `KUBB_AGENT_MAX_GENERATIONS_MB`.
1406
1427
  * An unset or invalid value keeps the default.
1407
1428
  */
1408
1429
  function resolveGenerationLimits() {
1409
1430
  return {
1410
1431
  maxCount: Math.max(1, Math.floor(positiveNumber(node_process.default.env.KUBB_AGENT_MAX_GENERATIONS) ?? 8)),
1411
- maxMb: positiveNumber(node_process.default.env.KUBB_AGENT_MAX_GENERATIONS_MB) ?? 100,
1412
- maxSnapshotMb: positiveNumber(node_process.default.env.KUBB_AGENT_MAX_SNAPSHOT_MB) ?? 50
1432
+ maxMb: positiveNumber(node_process.default.env.KUBB_AGENT_MAX_GENERATIONS_MB) ?? 100
1413
1433
  };
1414
1434
  }
1415
1435
  /**
@@ -1456,7 +1476,7 @@ async function generate({ config, hooks, signal, allowExec }) {
1456
1476
  };
1457
1477
  }
1458
1478
  /**
1459
- * What the output directory holds on disk before a run. `undefined` when `outputPath` is not a real
1479
+ * What the output directory holds when the editor connects. `undefined` when `outputPath` is not a real
1460
1480
  * subdirectory of `root` (listing the root would take in every source file) or holds more than `maxFiles`.
1461
1481
  */
1462
1482
  async function listDisk({ root, outputPath, maxFiles }) {
@@ -1473,38 +1493,11 @@ async function listDisk({ root, outputPath, maxFiles }) {
1473
1493
  paths: new Set(paths)
1474
1494
  };
1475
1495
  }
1476
- async function readDiskFiles({ files: disk, paths, cursor = 0, limit }) {
1477
- const files = {};
1478
- const pages = {};
1479
- await inParallel({
1480
- items: paths.filter((path) => disk.paths.has(path)),
1481
- limit: READ_CONCURRENCY,
1482
- run: async (path) => {
1483
- const content = await disk.storage.readItem((0, node_path.resolve)(disk.root, path));
1484
- if (content === null) return;
1485
- if (limit === void 0) {
1486
- files[path] = content;
1487
- return;
1488
- }
1489
- const lines = content.split(/\r\n|\r|\n/);
1490
- const end = Math.min(cursor + limit, lines.length);
1491
- files[path] = lines.slice(cursor, end).join("\n");
1492
- pages[path] = {
1493
- nextCursor: end < lines.length ? end : null,
1494
- totalLines: lines.length
1495
- };
1496
- }
1497
- });
1498
- return limit === void 0 ? { files } : {
1499
- files,
1500
- pages
1501
- };
1502
- }
1503
1496
  /**
1504
1497
  * Keeps recent generations by job id in `storage`, with an index next to them so a store on disk
1505
1498
  * survives a restart. A generation is only ever looked up by its job id, so on a pooled sandbox one
1506
- * tenant never reaches another's output. The oldest go past `maxCount` or `maxMb`, but the newest
1507
- * always stays.
1499
+ * tenant never reaches another's output. Initial disk output and generated jobs each get their
1500
+ * own `maxCount` and `maxMb` budget. Each budget always keeps its newest entry.
1508
1501
  */
1509
1502
  function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now }) {
1510
1503
  let index;
@@ -1523,7 +1516,7 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
1523
1516
  /**
1524
1517
  * Copies `files` into the store as one set of `jobId`. Above `maxSetMb` only the hashes are kept.
1525
1518
  */
1526
- async function keep({ jobId, source, files, maxSetMb, normalize = source === require_protocol.FileSource.OUTPUT }) {
1519
+ async function keep({ jobId, files, maxSetMb }) {
1527
1520
  const maxSetBytes = maxSetMb * MB;
1528
1521
  const hashes = {};
1529
1522
  let bytes = 0;
@@ -1534,14 +1527,13 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
1534
1527
  if (path.split("/").includes("..")) return;
1535
1528
  const content = await files.storage.readItem((0, node_path.resolve)(files.root, path));
1536
1529
  if (content === null) return;
1537
- if (normalize && !content.trim()) return;
1538
- hashes[path] = hashOf(normalize ? `${content.trim()}\n` : content);
1530
+ hashes[path] = hashOf(content);
1539
1531
  bytes += Buffer.byteLength(content);
1540
- if (bytes <= maxSetBytes) await storage.writeItem(`${dirOf(jobId)}${source}/${path}`, content);
1532
+ if (bytes <= maxSetBytes) await storage.writeItem(`${dirOf(jobId)}output/${path}`, content);
1541
1533
  }
1542
1534
  });
1543
1535
  if (bytes > maxSetBytes) {
1544
- await storage.empty(`${dirOf(jobId)}${source}/`);
1536
+ await storage.empty(`${dirOf(jobId)}output/`);
1545
1537
  return {
1546
1538
  paths: [],
1547
1539
  hashes,
@@ -1557,10 +1549,50 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
1557
1549
  async function drop(jobId) {
1558
1550
  await storage.empty(dirOf(jobId));
1559
1551
  }
1552
+ async function add(generation) {
1553
+ const current = await load();
1554
+ for (const expired of current.filter((entry) => !isLive(entry))) await drop(expired.jobId);
1555
+ const live = current.filter((entry) => entry.jobId !== generation.jobId && isLive(entry));
1556
+ const sharesBudget = (entry) => Boolean(entry.initial) === Boolean(generation.initial);
1557
+ const entries = live.filter(sharesBudget);
1558
+ entries.push({
1559
+ ...generation,
1560
+ keptAt: now()
1561
+ });
1562
+ while (entries.length > 1 && (entries.length > maxCount || totalBytes(entries) > maxMb * MB)) await drop(entries.shift().jobId);
1563
+ index = [...live.filter((entry) => !sharesBudget(entry)), ...entries];
1564
+ await storage.writeItem(INDEX_KEY, JSON.stringify(index));
1565
+ }
1560
1566
  return {
1561
1567
  keep,
1562
1568
  drop,
1563
1569
  /**
1570
+ * Captures initial disk output, reusing a retained ID when its hashes and readable paths match.
1571
+ * A temporary candidate is discarded on reuse, so reconnects do not consume retention slots.
1572
+ */
1573
+ async keepInitial(files) {
1574
+ const jobId = (0, node_crypto.randomUUID)();
1575
+ const output = await keep({
1576
+ jobId,
1577
+ files,
1578
+ maxSetMb: maxMb
1579
+ });
1580
+ const hashes = Object.entries(output.hashes);
1581
+ 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));
1582
+ if (previous) {
1583
+ await drop(jobId);
1584
+ await add(previous);
1585
+ return previous;
1586
+ }
1587
+ const generation = {
1588
+ jobId,
1589
+ output,
1590
+ initial: true
1591
+ };
1592
+ await add(generation);
1593
+ return generation;
1594
+ },
1595
+ /**
1564
1596
  * Keeps the `InputNode` of `jobId` and returns its size in bytes, or 0 when it keeps none. A ref schema loses
1565
1597
  * the copy of its target, which `input.schemas` holds: with it, a large spec outgrows what `JSON.stringify` can return.
1566
1598
  */
@@ -1588,30 +1620,19 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
1588
1620
  return stored === null ? void 0 : JSON.parse(stored);
1589
1621
  },
1590
1622
  get: async (jobId) => (await load()).find((generation) => generation.jobId === jobId && isLive(generation)),
1591
- async add(generation) {
1592
- const current = await load();
1593
- for (const expired of current.filter((entry) => !isLive(entry))) await drop(expired.jobId);
1594
- const entries = current.filter((entry) => entry.jobId !== generation.jobId && isLive(entry));
1595
- entries.push({
1596
- ...generation,
1597
- keptAt: now()
1598
- });
1599
- while (entries.length > 1 && (entries.length > maxCount || totalBytes(entries) > maxMb * MB)) await drop(entries.shift().jobId);
1600
- index = entries;
1601
- await storage.writeItem(INDEX_KEY, JSON.stringify(entries));
1602
- },
1623
+ add,
1603
1624
  /**
1604
1625
  * Reads the requested paths the set holds, skipping any it does not, one page of lines when `limit` is given.
1605
1626
  */
1606
- async read({ generation, source, paths, cursor = 0, limit }) {
1607
- const kept = new Set(generation[source]?.paths);
1627
+ async read({ generation, paths, cursor = 0, limit }) {
1628
+ const kept = new Set(generation.output.paths);
1608
1629
  const files = {};
1609
1630
  const pages = {};
1610
1631
  await inParallel({
1611
1632
  items: paths.filter((path) => kept.has(path)),
1612
1633
  limit: READ_CONCURRENCY,
1613
1634
  run: async (path) => {
1614
- const content = await storage.readItem(`${dirOf(generation.jobId)}${source}/${path}`);
1635
+ const content = await storage.readItem(`${dirOf(generation.jobId)}output/${path}`);
1615
1636
  if (content === null) return;
1616
1637
  if (limit === void 0) {
1617
1638
  files[path] = content;
@@ -1886,6 +1907,25 @@ function isValidPage({ cursor, limit }) {
1886
1907
  if (cursor !== void 0 && (!Number.isSafeInteger(cursor) || cursor < 0)) return false;
1887
1908
  return Number.isSafeInteger(limit) && limit >= 1 && limit <= 2e3;
1888
1909
  }
1910
+ /**
1911
+ * Maps generated paths into a temporary directory while keeping the project root for input resolution.
1912
+ */
1913
+ function stagingStorage({ root, directory }) {
1914
+ const storage = (0, _kubb_core.fsStorage)();
1915
+ const target = (key) => {
1916
+ const relative = node_path.default.relative(root, node_path.default.resolve(root, key));
1917
+ if (relative === ".." || relative.startsWith(`..${node_path.default.sep}`) || node_path.default.isAbsolute(relative)) throw new Error("Output path escapes the project root");
1918
+ return node_path.default.join(directory, relative);
1919
+ };
1920
+ return {
1921
+ name: "fs",
1922
+ readItem: (key) => storage.readItem(target(key)),
1923
+ writeItem: (key, value) => storage.writeItem(target(key), value),
1924
+ removeItem: (key) => storage.removeItem(target(key)),
1925
+ readKeys: (base) => storage.readKeys(base ? target(base) : directory),
1926
+ empty: (base) => base ? storage.empty(target(base)) : Promise.resolve()
1927
+ };
1928
+ }
1889
1929
  var GenerationRunTarget = class extends capnweb.RpcTarget {
1890
1930
  #run;
1891
1931
  constructor(run) {
@@ -1910,6 +1950,7 @@ var Agent = class extends capnweb.RpcTarget {
1910
1950
  #session;
1911
1951
  #limits = resolveGenerationLimits();
1912
1952
  #store;
1953
+ #sandboxCacheRoot = node_path.default.join((0, node_os.tmpdir)(), `kubb-studio-${(0, node_crypto.randomUUID)()}`);
1913
1954
  #diffs = /* @__PURE__ */ new Map();
1914
1955
  constructor({ options, session }) {
1915
1956
  super();
@@ -1931,13 +1972,19 @@ var Agent = class extends capnweb.RpcTarget {
1931
1972
  }
1932
1973
  get #generations() {
1933
1974
  this.#store ??= createGenerationStore({
1934
- storage: this.#sandbox ? (0, _kubb_core.memoryStorage)() : (0, _kubb_core.cacheStorage)({ root: this.#options.root }),
1975
+ storage: (0, _kubb_core.cacheStorage)({ root: this.#sandbox ? this.#sandboxCacheRoot : this.#options.root }),
1935
1976
  maxCount: this.#limits.maxCount,
1936
1977
  maxMb: this.#limits.maxMb,
1937
1978
  ttlMs: this.#sandbox ? SANDBOX_GENERATION_TTL_MS : void 0
1938
1979
  });
1939
1980
  return this.#store;
1940
1981
  }
1982
+ async [Symbol.asyncDispose]() {
1983
+ if (this.#sandbox) await (0, node_fs_promises.rm)((0, _kubb_core.resolveCacheDir)(this.#sandboxCacheRoot), {
1984
+ recursive: true,
1985
+ force: true
1986
+ });
1987
+ }
1941
1988
  #warn({ message, permission }) {
1942
1989
  return emit(this.#options.hooks, "studio:warn", {
1943
1990
  message,
@@ -2049,7 +2096,12 @@ var Agent = class extends capnweb.RpcTarget {
2049
2096
  maxFiles: DISK_SNAPSHOT_MAX_FILES
2050
2097
  });
2051
2098
  if (!files) return void 0;
2052
- return { files: [...files.paths] };
2099
+ const { jobId, output } = await this.#generations.keepInitial(files);
2100
+ return {
2101
+ jobId,
2102
+ files: output.paths,
2103
+ hashes: output.hashes
2104
+ };
2053
2105
  }
2054
2106
  startGeneration(input) {
2055
2107
  const stream = createGenerationStream(this.#options.hooks, input.jobId);
@@ -2068,7 +2120,9 @@ var Agent = class extends capnweb.RpcTarget {
2068
2120
  }
2069
2121
  });
2070
2122
  }
2071
- /** Runs one job and keeps its output, and its input with `allowRead`, for later reads. */
2123
+ /**
2124
+ * Keeps a job's output and permitted parsed input, then cleans up its temporary generation directories.
2125
+ */
2072
2126
  async #runGeneration(input, controller) {
2073
2127
  this.#assertConnected();
2074
2128
  if (this.#session.closed) throw new _kubb_core.Diagnostics.Error({
@@ -2084,11 +2138,14 @@ var Agent = class extends capnweb.RpcTarget {
2084
2138
  settled: settled.promise
2085
2139
  };
2086
2140
  let root = this.#options.root;
2141
+ let outputRoot;
2087
2142
  try {
2088
2143
  if (this.#sandbox) root = await (0, node_fs_promises.mkdtemp)(node_path.default.join((0, node_os.tmpdir)(), "kubb-job-"));
2144
+ if (!this.#sandbox && !this.#permissions.allowWrite) outputRoot = await (0, node_fs_promises.mkdtemp)(node_path.default.join((0, node_os.tmpdir)(), "kubb-output-"));
2089
2145
  return await this.#generate({
2090
2146
  input,
2091
2147
  root,
2148
+ outputRoot,
2092
2149
  signal: controller.signal
2093
2150
  });
2094
2151
  } finally {
@@ -2099,12 +2156,18 @@ var Agent = class extends capnweb.RpcTarget {
2099
2156
  recursive: true,
2100
2157
  force: true
2101
2158
  })]).catch(() => {});
2159
+ if (outputRoot) await (0, node_fs_promises.rm)(outputRoot, {
2160
+ recursive: true,
2161
+ force: true
2162
+ });
2102
2163
  this.#session.active = void 0;
2103
2164
  settled.resolve();
2104
2165
  }
2105
2166
  }
2106
- /** Merges Studio's config over the disk config, refuses output paths outside the output folder, then generates in `root`. */
2107
- async #generate({ input, root, signal }) {
2167
+ /**
2168
+ * Merges Studio's config over the project config and confines generation to the configured output folder.
2169
+ */
2170
+ async #generate({ input, root, outputRoot, signal }) {
2108
2171
  const { jobId, config: patch } = input;
2109
2172
  const studioInput = this.#sandbox ? patch?.input : void 0;
2110
2173
  if (this.#sandbox && (typeof studioInput !== "string" || !studioInput.trim() || (0, _kubb_core.getInputKind)(studioInput) !== "inline")) return this.#refuse({ message: require_protocol.AgentMessage.SANDBOX_INPUT_ONLY });
@@ -2135,37 +2198,28 @@ var Agent = class extends capnweb.RpcTarget {
2135
2198
  });
2136
2199
  const activePlugins = plugins ?? config.plugins;
2137
2200
  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" });
2138
- const diskFiles = !this.#sandbox && this.#permissions.allowRead ? await listDisk({
2139
- root,
2140
- outputPath: config.output.path,
2141
- maxFiles: DISK_SNAPSHOT_MAX_FILES
2142
- }) : void 0;
2143
- const disk = diskFiles ? await this.#generations.keep({
2144
- jobId,
2145
- source: require_protocol.FileSource.DISK,
2146
- files: diskFiles,
2147
- maxSetMb: this.#limits.maxSnapshotMb
2148
- }) : void 0;
2149
2201
  const files = await generate({
2150
2202
  config: {
2151
2203
  ...config,
2152
2204
  root,
2153
2205
  input: studioInput ?? config.input,
2154
- storage: this.#permissions.allowWrite ? (0, _kubb_core.fsStorage)() : (0, _kubb_core.memoryStorage)(),
2206
+ storage: outputRoot ? stagingStorage({
2207
+ root,
2208
+ directory: outputRoot
2209
+ }) : (0, _kubb_core.fsStorage)(),
2155
2210
  plugins: activePlugins,
2156
2211
  adapter,
2157
2212
  parsers
2158
2213
  },
2159
2214
  hooks: this.#options.hooks,
2160
2215
  signal,
2161
- allowExec: this.#permissions.allowExec
2216
+ allowExec: this.#permissions.allowWrite && this.#permissions.allowExec
2162
2217
  }).catch(async (error) => {
2163
2218
  await this.#generations.drop(jobId);
2164
2219
  throw error;
2165
2220
  });
2166
2221
  const output = await this.#generations.keep({
2167
2222
  jobId,
2168
- source: require_protocol.FileSource.OUTPUT,
2169
2223
  files,
2170
2224
  maxSetMb: this.#limits.maxMb
2171
2225
  });
@@ -2178,16 +2232,14 @@ var Agent = class extends capnweb.RpcTarget {
2178
2232
  await this.#generations.add({
2179
2233
  jobId,
2180
2234
  output,
2181
- disk,
2182
2235
  inputBytes
2183
2236
  });
2184
- 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" : ""}`);
2237
+ 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" : ""}`);
2185
2238
  return {
2186
2239
  status: "success",
2187
2240
  files: [...files.paths],
2188
2241
  fileCount: files.paths.size,
2189
- hashes: output.hashes,
2190
- disk: disk ? { hashes: disk.hashes } : void 0
2242
+ hashes: output.hashes
2191
2243
  };
2192
2244
  }
2193
2245
  async saveConfig(input) {
@@ -2257,35 +2309,14 @@ var Agent = class extends capnweb.RpcTarget {
2257
2309
  message: require_protocol.AgentMessage.READ_NOT_ALLOWED,
2258
2310
  permission: require_protocol.AgentPermission.READ
2259
2311
  });
2260
- if (!Array.isArray(input.paths) || typeof input.jobId !== "string" || !input.jobId || !["generation", "currentDisk"].includes(input.jobType)) return this.#refuse({ message: require_protocol.AgentMessage.FILE_REQUEST_INVALID });
2312
+ if (!Array.isArray(input.paths) || typeof input.jobId !== "string" || !input.jobId) return this.#refuse({ message: require_protocol.AgentMessage.FILE_REQUEST_INVALID });
2261
2313
  const { paths } = input;
2262
2314
  if (paths.length > 50) return this.#refuse({ message: require_protocol.AgentMessage.TOO_MANY_FILES });
2263
2315
  if (!isValidPage(input)) return this.#refuse({ message: require_protocol.AgentMessage.INVALID_PAGE });
2264
- if (input.jobType === "currentDisk") {
2265
- if (this.#sandbox) return this.#refuse({ message: require_protocol.AgentMessage.SANDBOX_DISK_UNAVAILABLE });
2266
- const config = await this.#options.loadConfig();
2267
- const disk = await listDisk({
2268
- root: this.#options.root,
2269
- outputPath: config.output.path,
2270
- maxFiles: DISK_SNAPSHOT_MAX_FILES
2271
- });
2272
- if (!disk) return this.#refuse({ message: require_protocol.AgentMessage.GENERATION_GONE });
2273
- const result = await readDiskFiles({
2274
- files: disk,
2275
- paths,
2276
- cursor: input.cursor,
2277
- limit: input.limit
2278
- });
2279
- await this.#logCommand("readFiles", `read ${Object.keys(result.files).length}/${paths.length} requested file${paths.length === 1 ? "" : "s"}`);
2280
- return result;
2281
- }
2282
2316
  const generation = await this.#generations.get(input.jobId);
2283
2317
  if (!generation) return this.#refuse({ message: require_protocol.AgentMessage.GENERATION_GONE });
2284
- const source = input.source === require_protocol.FileSource.DISK ? require_protocol.FileSource.DISK : require_protocol.FileSource.OUTPUT;
2285
- if (!generation[source]) return this.#refuse({ message: require_protocol.AgentMessage.NO_DISK_SNAPSHOT });
2286
2318
  const result = await this.#generations.read({
2287
2319
  generation,
2288
- source,
2289
2320
  paths,
2290
2321
  cursor: input.cursor,
2291
2322
  limit: input.limit
@@ -2310,15 +2341,11 @@ var Agent = class extends capnweb.RpcTarget {
2310
2341
  })) return this.#refuse({ message: require_protocol.AgentMessage.INVALID_PAGE });
2311
2342
  const [generation, baseGeneration] = await Promise.all([this.#generations.get(jobId), this.#generations.get(base.jobId)]);
2312
2343
  if (!generation || !baseGeneration) return this.#refuse({ message: require_protocol.AgentMessage.GENERATION_GONE });
2313
- const baseSource = base.source === require_protocol.FileSource.DISK ? require_protocol.FileSource.DISK : require_protocol.FileSource.OUTPUT;
2314
- if (!baseGeneration[baseSource]) return this.#refuse({ message: require_protocol.AgentMessage.NO_DISK_SNAPSHOT });
2315
2344
  const [after, before] = await Promise.all([this.#generations.read({
2316
2345
  generation,
2317
- source: require_protocol.FileSource.OUTPUT,
2318
2346
  paths: [path]
2319
2347
  }), this.#generations.read({
2320
2348
  generation: baseGeneration,
2321
- source: baseSource,
2322
2349
  paths: [path]
2323
2350
  })]);
2324
2351
  const afterContent = after.files[path];
@@ -2339,7 +2366,6 @@ var Agent = class extends capnweb.RpcTarget {
2339
2366
  const key = JSON.stringify([
2340
2367
  jobId,
2341
2368
  base.jobId,
2342
- baseSource,
2343
2369
  path
2344
2370
  ]);
2345
2371
  let diff = this.#diffs.get(key);
@@ -2396,14 +2422,15 @@ async function runSession({ options, openRpc, url, instanceId }) {
2396
2422
  connected: Promise.withResolvers(),
2397
2423
  closed: false
2398
2424
  };
2425
+ const agent = new Agent({
2426
+ options,
2427
+ session
2428
+ });
2399
2429
  const rpc = openRpc({
2400
2430
  url,
2401
2431
  token,
2402
2432
  instanceId,
2403
- agent: new Agent({
2404
- options,
2405
- session
2406
- })
2433
+ agent
2407
2434
  });
2408
2435
  signal?.addEventListener("abort", rpc.close, { once: true });
2409
2436
  let timer;
@@ -2438,6 +2465,7 @@ async function runSession({ options, openRpc, url, instanceId }) {
2438
2465
  await emit(hooks, "studio:warn", { message: "Canceled the running generation: the connection to Kubb Studio closed" });
2439
2466
  }
2440
2467
  await active?.settled;
2468
+ await agent[Symbol.asyncDispose]();
2441
2469
  return {
2442
2470
  ...close,
2443
2471
  connected: session.info !== void 0
@@ -2747,8 +2775,7 @@ async function createSnapshotPackage({ files, packageInfo }) {
2747
2775
  try {
2748
2776
  await (0, node_fs_promises.mkdir)(dist);
2749
2777
  await Promise.all([...sources].map(async ([path, { content }]) => {
2750
- await (0, node_fs_promises.mkdir)((0, node_path.dirname)((0, node_path.join)(root, path)), { recursive: true });
2751
- await (0, node_fs_promises.writeFile)((0, node_path.join)(root, path), content);
2778
+ await writeRaw((0, node_path.join)(root, path), content);
2752
2779
  }));
2753
2780
  const entry = [...sources.keys()].filter((path) => /\.(?:[cm]?[jt]sx?)$/.test(path)).map((path) => (0, node_path.join)(root, path));
2754
2781
  if (entry.length) {
@@ -2788,7 +2815,7 @@ async function createSnapshotPackage({ files, packageInfo }) {
2788
2815
  const chunks = [...new Map([
2789
2816
  ["package.json", JSON.stringify(manifest, null, 2)],
2790
2817
  ...[...sources].map(([path, { content }]) => [path, content]),
2791
- ...await Promise.all(built.map(async (key) => [`dist/${key}`, await (0, node_fs_promises.readFile)((0, node_path.join)(dist, key), "utf8")]))
2818
+ ...await Promise.all(built.map(async (key) => [`dist/${key}`, await read((0, node_path.join)(dist, key))]))
2792
2819
  ])].flatMap(([path, content]) => {
2793
2820
  const bytes = Buffer.from(content);
2794
2821
  return [
@@ -2819,7 +2846,10 @@ async function createSnapshot({ studioUrl = defaultStudioUrl, apiKey, config, ho
2819
2846
  path: "/api/snapshots"
2820
2847
  });
2821
2848
  const generated = await generate({
2822
- config,
2849
+ config: {
2850
+ ...config,
2851
+ storage: (0, _kubb_core.fsStorage)()
2852
+ },
2823
2853
  hooks,
2824
2854
  signal,
2825
2855
  allowExec: true