@kubb/studio 5.9.4 → 5.9.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -74,7 +74,7 @@ running in a developer's project and a container running a fixed plugin set.
74
74
  | `connect.ts` | `connectAgent`: the socket loop and the `Agent` that serves Studio's calls. |
75
75
  | `pair.ts` | `pairAgent`: RFC 8628 device pairing. |
76
76
  | `snapshot.ts` | `createSnapshot` for CI, and packing a generation into an npm tarball. |
77
- | `generations.ts` | Running a generation and keeping its files for `readFiles`. |
77
+ | `generations.ts` | Running a generation and keeping its files and AST for `readFiles`/`readInput`. |
78
78
  | `configFile.ts` | Reading and patching plugin options in `kubb.config.ts`. |
79
79
 
80
80
  ## Connection
@@ -107,7 +107,7 @@ Every permission is off by default, and each covers one trust boundary:
107
107
  | `allowWrite` | Generated files are written to disk. Off means they exist only in memory and stream to Studio. |
108
108
  | `allowExec` | The formatter, the linter, and `output.postGenerate` run as child processes. |
109
109
  | `allowConfigEdit` | Studio may change plugin options in `kubb.config.ts`. |
110
- | `allowRead` | Studio may read back the files a generation produced. |
110
+ | `allowRead` | Studio may read back the files a generation produced and its parsed spec. |
111
111
 
112
112
  A sandbox never writes or edits config, and always allows reads.
113
113
 
package/dist/index.cjs CHANGED
@@ -37,6 +37,7 @@ let capnweb = require("capnweb");
37
37
  let magicast = require("magicast");
38
38
  let node_module = require("node:module");
39
39
  let node_util = require("node:util");
40
+ let _kubb_ast = require("@kubb/ast");
40
41
  let ofetch = require("ofetch");
41
42
  let node_zlib = require("node:zlib");
42
43
  //#region ../../internals/utils/src/casing.ts
@@ -176,7 +177,7 @@ function getElapsedMs(hrStart) {
176
177
  }
177
178
  //#endregion
178
179
  //#region package.json
179
- var version = "5.9.4";
180
+ var version = "5.9.5";
180
181
  //#endregion
181
182
  //#region src/resolveConfig.ts
182
183
  /**
@@ -1107,7 +1108,10 @@ function relativeStoragePath({ root, filePath }) {
1107
1108
  * The content fingerprint Studio compares snapshots by. Agent runs and CI uploads must agree on it.
1108
1109
  */
1109
1110
  const hashOf = (content) => (0, node_crypto.createHash)("sha1").update(content).digest("hex").slice(0, 16);
1110
- const totalBytes = (entries) => entries.reduce((sum, { output, disk }) => sum + output.bytes + (disk?.bytes ?? 0), 0);
1111
+ /**
1112
+ * The bytes `entries` keep on disk: output, written files, and kept input.
1113
+ */
1114
+ const totalBytes = (entries) => entries.reduce((sum, { output, disk, inputBytes }) => sum + output.bytes + (disk?.bytes ?? 0) + (inputBytes ?? 0), 0);
1111
1115
  function positiveNumber(value) {
1112
1116
  const parsed = Number(value);
1113
1117
  return value && Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
@@ -1150,7 +1154,11 @@ async function generate({ config, hooks, signal, allowExec }) {
1150
1154
  hooks,
1151
1155
  signal
1152
1156
  });
1153
- const result = await kubb.generate();
1157
+ let input = null;
1158
+ const unhook = kubb.hooks.hook("kubb:build:end", (ctx) => {
1159
+ if (ctx.config === kubb.config) input = kubb.driver.inputNode;
1160
+ });
1161
+ const result = await kubb.generate().finally(unhook);
1154
1162
  if (!result.success) throw generationFailure(result.diagnostics);
1155
1163
  const { storage, root } = kubb.config;
1156
1164
  return {
@@ -1159,7 +1167,8 @@ async function generate({ config, hooks, signal, allowExec }) {
1159
1167
  paths: new Set(result.files.map((file) => relativeStoragePath({
1160
1168
  root,
1161
1169
  filePath: file.path
1162
- })))
1170
+ }))),
1171
+ input
1163
1172
  };
1164
1173
  }
1165
1174
  /**
@@ -1240,6 +1249,33 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
1240
1249
  return {
1241
1250
  keep,
1242
1251
  drop,
1252
+ /**
1253
+ * Keeps the `InputNode` of `jobId` and returns its size in bytes, or 0 when it keeps none. A ref schema loses
1254
+ * the copy of its target, which `input.schemas` holds: with it, a large spec outgrows what `JSON.stringify` can return.
1255
+ */
1256
+ async keepInput({ jobId, input, maxSetMb }) {
1257
+ if (!input) return 0;
1258
+ let json;
1259
+ try {
1260
+ json = JSON.stringify(_kubb_ast.ast.transform(input, { schema: (node) => node.type === "ref" && node.schema ? {
1261
+ ...node,
1262
+ schema: void 0
1263
+ } : void 0 }));
1264
+ } catch {
1265
+ return 0;
1266
+ }
1267
+ const bytes = Buffer.byteLength(json);
1268
+ if (bytes > maxSetMb * MB) return 0;
1269
+ await storage.writeItem(`${dirOf(jobId)}input.json`, json);
1270
+ return bytes;
1271
+ },
1272
+ /**
1273
+ * The input `keepInput` kept for `jobId`, or `undefined` when there is none.
1274
+ */
1275
+ async readInput(jobId) {
1276
+ const stored = await storage.readItem(`${dirOf(jobId)}input.json`);
1277
+ return stored === null ? void 0 : JSON.parse(stored);
1278
+ },
1243
1279
  get: async (jobId) => (await load()).find((generation) => generation.jobId === jobId && isLive(generation)),
1244
1280
  async add(generation) {
1245
1281
  const current = await load();
@@ -1690,6 +1726,7 @@ var Agent = class extends capnweb.RpcTarget {
1690
1726
  }
1691
1727
  });
1692
1728
  }
1729
+ /** Runs one job and keeps its output, and its input with `allowRead`, for later reads. */
1693
1730
  async #runGeneration(input, controller) {
1694
1731
  this.#assertConnected();
1695
1732
  if (this.#session.closed) throw new Error("Connection is closed");
@@ -1777,11 +1814,17 @@ var Agent = class extends capnweb.RpcTarget {
1777
1814
  files,
1778
1815
  maxSetMb: this.#limits.maxMb
1779
1816
  });
1817
+ const inputBytes = await this.#generations.keepInput({
1818
+ jobId,
1819
+ input: this.#permissions.allowRead ? files.input : null,
1820
+ maxSetMb: this.#limits.maxMb
1821
+ });
1780
1822
  if (!output.paths.length && Object.keys(output.hashes).length) await this.#warn({ message: "Kept only the hashes of this generation: its output is too large to keep" });
1781
1823
  await this.#generations.add({
1782
1824
  jobId,
1783
1825
  output,
1784
- disk
1826
+ disk,
1827
+ inputBytes
1785
1828
  });
1786
1829
  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" : ""}`);
1787
1830
  return {
@@ -1833,10 +1876,30 @@ var Agent = class extends capnweb.RpcTarget {
1833
1876
  return refuse(getErrorMessage(error));
1834
1877
  }
1835
1878
  }
1879
+ /**
1880
+ * Returns the Kubb AST a kept job parsed its spec into. Refused without `allowRead`.
1881
+ */
1882
+ async readInput(input) {
1883
+ this.#assertConnected();
1884
+ if (!this.#permissions.allowRead) return this.#refuse({
1885
+ message: require_protocol.READ_NOT_ALLOWED_MESSAGE,
1886
+ permission: "allowRead"
1887
+ });
1888
+ if (typeof input.jobId !== "string" || !input.jobId) return this.#refuse({ message: "The request must name a job" });
1889
+ const generation = await this.#generations.get(input.jobId);
1890
+ if (!generation) return this.#refuse({ message: require_protocol.GENERATION_GONE_MESSAGE });
1891
+ const kept = generation.inputBytes ? await this.#generations.readInput(input.jobId) : void 0;
1892
+ if (!kept) return this.#refuse({ message: require_protocol.NO_INPUT_MESSAGE });
1893
+ await this.#logCommand("readInput", "read the API input");
1894
+ return { input: kept };
1895
+ }
1896
+ /**
1897
+ * Returns the requested files of a kept job, one page of lines at a time. Refused without `allowRead`.
1898
+ */
1836
1899
  async readFiles(input) {
1837
1900
  this.#assertConnected();
1838
1901
  if (!this.#permissions.allowRead) return this.#refuse({
1839
- message: "The agent was not granted permission to read generated files",
1902
+ message: require_protocol.READ_NOT_ALLOWED_MESSAGE,
1840
1903
  permission: "allowRead"
1841
1904
  });
1842
1905
  if (!Array.isArray(input.paths) || typeof input.jobId !== "string" || !input.jobId) return this.#refuse({ message: "The request must name a job and carry paths" });