@kubb/studio 5.3.5 → 5.3.6

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
@@ -1,19 +1,44 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_rolldown_runtime = require("./rolldown-runtime-qbf5tadS.cjs");
2
+ //#region \0rolldown/runtime.js
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __name = (target, value) => __defProp(target, "name", {
6
+ value,
7
+ configurable: true
8
+ });
9
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
10
+ var __getOwnPropNames = Object.getOwnPropertyNames;
11
+ var __getProtoOf = Object.getPrototypeOf;
12
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
15
+ key = keys[i];
16
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
17
+ get: ((k) => from[k]).bind(null, key),
18
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
19
+ });
20
+ }
21
+ return to;
22
+ };
23
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
24
+ value: mod,
25
+ enumerable: true
26
+ }) : target, mod));
27
+ //#endregion
3
28
  const require_protocol = require("./protocol.cjs");
4
29
  let node_util = require("node:util");
5
30
  let node_process = require("node:process");
6
- node_process = require_rolldown_runtime.__toESM(node_process, 1);
31
+ node_process = __toESM(node_process, 1);
7
32
  let node_child_process = require("node:child_process");
8
33
  let node_fs_promises = require("node:fs/promises");
9
34
  let node_path = require("node:path");
10
- node_path = require_rolldown_runtime.__toESM(node_path, 1);
35
+ node_path = __toESM(node_path, 1);
11
36
  let _kubb_core = require("@kubb/core");
12
37
  let ofetch = require("ofetch");
13
38
  let node_crypto = require("node:crypto");
14
39
  let unstorage = require("unstorage");
15
40
  let unstorage_drivers_fs = require("unstorage/drivers/fs");
16
- unstorage_drivers_fs = require_rolldown_runtime.__toESM(unstorage_drivers_fs, 1);
41
+ unstorage_drivers_fs = __toESM(unstorage_drivers_fs, 1);
17
42
  let tinyexec = require("tinyexec");
18
43
  let magicast = require("magicast");
19
44
  let node_fs = require("node:fs");
@@ -23,8 +48,9 @@ let remeda = require("remeda");
23
48
  let node_os = require("node:os");
24
49
  let node_zlib = require("node:zlib");
25
50
  let tsdown = require("tsdown");
51
+ let capnweb = require("capnweb");
26
52
  let ws = require("ws");
27
- ws = require_rolldown_runtime.__toESM(ws, 1);
53
+ ws = __toESM(ws, 1);
28
54
  let node_timers_promises = require("node:timers/promises");
29
55
  //#region src/constants.ts
30
56
  /**
@@ -46,6 +72,8 @@ const agentDefaults = {
46
72
  * a slower cadence would make a healthy agent look dead after a single missed ping.
47
73
  */
48
74
  maxHeartbeatIntervalMs: 6e4,
75
+ /** How long a heartbeat ping may take before the session is treated as dead. */
76
+ heartbeatTimeoutMs: 1e4,
49
77
  poolSize: 1
50
78
  };
51
79
  //#endregion
@@ -339,7 +367,7 @@ async function detectTool$1(candidates) {
339
367
  for (const candidate of candidates) if (await isToolAvailable(candidate)) return candidate;
340
368
  return null;
341
369
  }
342
- require_rolldown_runtime.__name(detectTool$1, "detectTool");
370
+ __name(detectTool$1, "detectTool");
343
371
  /**
344
372
  * Tokenizes a shell command string, respecting single and double quotes.
345
373
  *
@@ -623,7 +651,7 @@ const INITIAL_POLL_DELAY_MS = 2e3;
623
651
  */
624
652
  const MAX_POLL_INTERVAL_MS = 3e4;
625
653
  /**
626
- * Polls `GET /api/jobs/{id}` until the job reaches `success` or `failed`, waiting
654
+ * Polls `GET /api/jobs/{id}` until the job reaches a terminal status, waiting
627
655
  * {@link INITIAL_POLL_DELAY_MS} first and doubling up to {@link MAX_POLL_INTERVAL_MS} so a long
628
656
  * job stays inside the API key's rate limit.
629
657
  *
@@ -642,7 +670,7 @@ async function waitForJob({ studioUrl, token, id, timeoutMs = 6e4 }) {
642
670
  headers: { "x-api-key": token },
643
671
  retry: false
644
672
  });
645
- if (job.status === "success" || job.status === "failed") return job;
673
+ if (job.status === "success" || job.status === "failed" || job.status === "canceled") return job;
646
674
  } catch (error) {
647
675
  const response = error.response;
648
676
  if (response?.status !== 429) throw error;
@@ -678,7 +706,7 @@ async function createAgent({ studioUrl, token, name, machineToken }) {
678
706
  }
679
707
  //#endregion
680
708
  //#region package.json
681
- var version = "5.3.5";
709
+ var version = "5.3.6";
682
710
  //#endregion
683
711
  //#region src/hooks.ts
684
712
  /**
@@ -689,16 +717,19 @@ var version = "5.3.5";
689
717
  * Returns a remover, so a session that runs one generation after another on the same emitter does
690
718
  * not stack a listener per run.
691
719
  */
692
- function setupHookListener(hooks, root) {
720
+ function setupHookListener(hooks, root, signal) {
693
721
  return hooks.hook("kubb:hook:start", async (ctx) => {
694
722
  const { id, command, args } = ctx;
695
723
  if (!id) return;
696
724
  const commandWithArgs = args?.length ? `${command} ${args.join(" ")}` : command;
697
725
  try {
698
- const proc = (0, tinyexec.x)(command, [...args ?? []], { nodeOptions: {
699
- cwd: root,
700
- detached: true
701
- } });
726
+ const proc = (0, tinyexec.x)(command, [...args ?? []], {
727
+ signal,
728
+ nodeOptions: {
729
+ cwd: root,
730
+ detached: true
731
+ }
732
+ });
702
733
  for await (const line of proc) await hooks.callHook("kubb:hook:line", {
703
734
  id,
704
735
  line
@@ -747,8 +778,11 @@ function waitForHookEnd(hooks, hookId) {
747
778
  const handleHookEnd = (ctx) => {
748
779
  if (ctx.id !== hookId) return;
749
780
  hooks.removeHook("kubb:hook:end", handleHookEnd);
750
- if (ctx.success) resolve();
751
- else reject(ctx.error);
781
+ if (ctx.success) {
782
+ resolve();
783
+ return;
784
+ }
785
+ reject(ctx.error);
752
786
  };
753
787
  hooks.hook("kubb:hook:end", handleHookEnd);
754
788
  });
@@ -1287,14 +1321,14 @@ function applyRemove(call, path) {
1287
1321
  });
1288
1322
  }
1289
1323
  /**
1290
- * Adds a `pluginX(...)` call to a config's plugins array. Refuses when the plugin is already
1291
- * present, or when its import name collides with an unrelated existing import.
1324
+ * Adds a `pluginX(...)` call to a config's plugins array. Replaying the same add is a no-op, while
1325
+ * an import name collision with an unrelated package remains an error.
1292
1326
  */
1293
1327
  function applyAddPlugin(mod, config, edit) {
1294
1328
  if (!isKubbPluginSpecifier(edit.plugin)) return { reason: `"${edit.plugin}" is not a @kubb/plugin-* package` };
1295
1329
  const importName = edit.importName ?? toExportName(edit.plugin);
1296
1330
  if (!IDENTIFIER.test(importName)) return { reason: `"${importName}" is not a valid import name` };
1297
- if (pluginCalls(mod, config).some((plugin) => plugin.packageName === edit.plugin)) return { reason: `${edit.plugin} is already in the plugins array` };
1331
+ if (pluginCalls(mod, config).some((plugin) => plugin.packageName === edit.plugin)) return { noop: true };
1298
1332
  const taken = importedFrom(mod).get(importName);
1299
1333
  if (taken && taken !== edit.plugin) return { reason: `${importName} is already imported from ${taken}` };
1300
1334
  const options = edit.options ?? {};
@@ -1415,6 +1449,10 @@ function applyConfigEdits(source, edits) {
1415
1449
  applied: false,
1416
1450
  reason: result.reason
1417
1451
  };
1452
+ if ("noop" in result) return {
1453
+ edit,
1454
+ applied: true
1455
+ };
1418
1456
  const afterLine = lastImportEndLine(mod);
1419
1457
  let next = (0, magicast.generateCode)(mod, { format }).code;
1420
1458
  if (result.addImport) next = insertImportLine({
@@ -1548,14 +1586,19 @@ function formatGenerationFailure(diagnostics) {
1548
1586
  * can forward progress to connected clients. After a successful build, auto-formatting and
1549
1587
  * linting are applied when configured, followed by any user-defined `hooks.done` commands.
1550
1588
  */
1551
- async function generate({ config, hooks }) {
1589
+ async function generate({ config, hooks, signal }) {
1590
+ signal?.throwIfAborted();
1552
1591
  const hrStart = node_process.default.hrtime();
1553
1592
  await hooks.callHook("kubb:generation:start", { config });
1554
1593
  await hooks.callHook("kubb:info", { message: config.name ? `Setup generation ${config.name}` : "Setup generation" });
1555
- const kubb = (0, _kubb_core.createKubb)(config, { hooks });
1594
+ const kubb = (0, _kubb_core.createKubb)(config, {
1595
+ hooks,
1596
+ signal
1597
+ });
1556
1598
  await kubb.setup();
1557
1599
  await hooks.callHook("kubb:info", { message: config.name ? `Build generation ${config.name}` : "Build generation" });
1558
1600
  const { files, diagnostics, storage } = await kubb.safeBuild();
1601
+ signal?.throwIfAborted();
1559
1602
  await hooks.callHook("kubb:info", { message: "Load summary" });
1560
1603
  for (const diagnostic of diagnostics.filter(isProblemErrorDiagnostic)) await hooks.callHook("kubb:error", { error: new Error(diagnostic.plugin ? `${diagnostic.plugin}: ${diagnostic.message}` : diagnostic.message) });
1561
1604
  const status = _kubb_core.Diagnostics.hasError(diagnostics) ? "failed" : "success";
@@ -1590,6 +1633,7 @@ async function generate({ config, hooks }) {
1590
1633
  await hooks.callHook("kubb:success", { message: `${step.verbing} with ${tool} successfully` });
1591
1634
  } catch (caughtError) {
1592
1635
  await hooks.callHook("kubb:error", { error: new Error(command.errorMessage, { cause: caughtError }) });
1636
+ signal?.throwIfAborted();
1593
1637
  }
1594
1638
  await hooks.callHook(`kubb:${step.kind}:end`);
1595
1639
  }
@@ -1745,13 +1789,6 @@ async function createSnapshotPackage(files, packageInfo) {
1745
1789
  * takes over.
1746
1790
  */
1747
1791
  const CONNECT_TIMEOUT_MS = 5e3;
1748
- /**
1749
- * Per-socket event counter. Every data message carries the next value so Studio can restore the
1750
- * agent's emission order even when the relay delivers frames out of order. Keyed by the socket so
1751
- * the count stays monotonic across every generation run on one connection, and is dropped
1752
- * automatically once the socket is collected.
1753
- */
1754
- const eventSeqCounters = /* @__PURE__ */ new WeakMap();
1755
1792
  const require$1 = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href);
1756
1793
  function relativeStoragePath(root, filePath) {
1757
1794
  return ((0, node_path.isAbsolute)(filePath) ? (0, node_path.relative)((0, node_path.resolve)(root), filePath) : filePath).replaceAll("\\", "/");
@@ -1776,66 +1813,38 @@ async function resolvePeerDependencies(names) {
1776
1813
  }));
1777
1814
  for (const [index, name] of uniqueNames.entries()) {
1778
1815
  const version = versions[index];
1779
- if (version) peerDependencies[name] = version;
1780
- else missingDependencies.push(name);
1816
+ if (version) {
1817
+ peerDependencies[name] = version;
1818
+ continue;
1819
+ }
1820
+ missingDependencies.push(name);
1781
1821
  }
1782
1822
  return {
1783
1823
  peerDependencies,
1784
1824
  missingDependencies
1785
1825
  };
1786
1826
  }
1787
- function nextEventSeq(ws$1) {
1788
- const seq = eventSeqCounters.get(ws$1) ?? 0;
1789
- eventSeqCounters.set(ws$1, seq + 1);
1790
- return seq;
1791
- }
1792
1827
  /**
1793
1828
  * Opens a Studio WebSocket connection and closes it when the initial handshake exceeds the configured timeout.
1794
1829
  */
1795
1830
  function createWebsocket(url, options) {
1796
- const ws$2 = new ws.default(url, options);
1831
+ const ws$1 = new ws.default(url, options);
1797
1832
  const timer = setTimeout(() => {
1798
- if (ws$2.readyState === ws.default.CONNECTING) ws$2.close(3008, "Connection timeout");
1833
+ if (ws$1.readyState === ws.default.CONNECTING) ws$1.close(3008, "Connection timeout");
1799
1834
  }, CONNECT_TIMEOUT_MS);
1800
- ws$2.once("open", () => clearTimeout(timer));
1801
- ws$2.once("close", () => clearTimeout(timer));
1802
- return ws$2;
1803
- }
1804
- /**
1805
- * Sends a serialized agent message when the Studio socket is ready to accept frames.
1806
- */
1807
- function sendAgentMessage(ws$3, message) {
1808
- try {
1809
- if (ws$3.readyState !== ws.default.OPEN) return;
1810
- ws$3.send(JSON.stringify(message));
1811
- } catch (error) {
1812
- throw new Error("Failed to send message to Kubb Studio", { cause: error });
1813
- }
1814
- }
1815
- /**
1816
- * Sends a single `kubb:error` payload to Studio, stamped from the same per-socket counter the event stream
1817
- * uses so Studio can still order it against the generation events around it.
1818
- */
1819
- function sendErrorMessage(ws$4, error, jobId) {
1820
- sendAgentMessage(ws$4, {
1821
- type: "agent:data",
1822
- jobId,
1823
- payload: {
1824
- type: "kubb:error",
1825
- data: [{
1826
- message: error.message,
1827
- stack: error.stack
1828
- }],
1829
- timestamp: Date.now(),
1830
- seq: nextEventSeq(ws$4)
1831
- }
1832
- });
1835
+ ws$1.once("open", () => clearTimeout(timer));
1836
+ ws$1.once("close", () => clearTimeout(timer));
1837
+ return ws$1;
1833
1838
  }
1834
- /**
1835
- * Forwards selected Kubb lifecycle events to Studio as data messages for the active session.
1836
- */
1837
- function setupEventsStream(ws$5, hooks, jobId, options = {}) {
1839
+ /** Forwards selected Kubb lifecycle events to a native Cap'n Web stream. */
1840
+ function createGenerationStream(hooks, jobId, options = {}) {
1838
1841
  const unhooks = [];
1842
+ let root = "";
1843
+ const transform = new TransformStream(void 0, void 0, { highWaterMark: Infinity });
1844
+ const writer = transform.writable.getWriter();
1845
+ let writes = Promise.resolve();
1846
+ let closed = false;
1847
+ let streamError;
1839
1848
  /**
1840
1849
  * Registers a listener and keeps its remover, so one generation's listeners come off the session
1841
1850
  * emitter again when that generation ends.
@@ -1843,98 +1852,73 @@ function setupEventsStream(ws$5, hooks, jobId, options = {}) {
1843
1852
  function on(name, handler) {
1844
1853
  unhooks.push(hooks.hook(name, handler));
1845
1854
  }
1846
- function sendDataMessage(payload) {
1847
- sendAgentMessage(ws$5, {
1848
- type: "agent:data",
1855
+ function emitEvent(type, data) {
1856
+ const event = {
1849
1857
  jobId,
1850
- payload: {
1851
- ...payload,
1852
- timestamp: Date.now(),
1853
- seq: nextEventSeq(ws$5)
1854
- }
1858
+ type,
1859
+ data,
1860
+ version: 1,
1861
+ timestamp: Date.now()
1862
+ };
1863
+ writes = writes.then(() => writer.write(event)).catch((error) => {
1864
+ streamError = error;
1855
1865
  });
1856
1866
  }
1857
1867
  on("kubb:plugin:start", (ctx) => {
1858
- sendDataMessage({
1859
- type: "kubb:plugin:start",
1860
- data: [{ plugin: ctx.plugin }]
1861
- });
1868
+ emitEvent("kubb:plugin:start", [{ plugin: { name: ctx.plugin.name } }]);
1862
1869
  });
1863
1870
  on("kubb:plugin:end", (ctx) => {
1864
- sendDataMessage({
1865
- type: "kubb:plugin:end",
1866
- data: [{
1867
- plugin: ctx.plugin,
1868
- duration: ctx.duration,
1869
- success: ctx.success
1870
- }]
1871
- });
1871
+ emitEvent("kubb:plugin:end", [{
1872
+ plugin: { name: ctx.plugin.name },
1873
+ duration: ctx.duration,
1874
+ success: ctx.success
1875
+ }]);
1872
1876
  });
1873
1877
  on("kubb:build:start", ({ config, adapter }) => {
1874
- sendDataMessage({
1875
- type: "kubb:build:start",
1876
- data: [{
1877
- config: { name: config.name },
1878
- adapter: { name: adapter.name }
1879
- }]
1880
- });
1878
+ root = config.root;
1879
+ emitEvent("kubb:build:start", [{
1880
+ config: { name: config.name },
1881
+ adapter: { name: adapter.name }
1882
+ }]);
1881
1883
  });
1882
1884
  on("kubb:build:end", ({ files, config, outputDir }) => {
1883
- sendDataMessage({
1884
- type: "kubb:build:end",
1885
- data: [{
1886
- files: files.map((file) => ({
1887
- path: relativeStoragePath(config.root, file.path),
1888
- name: file.name
1889
- })),
1890
- outputDir
1891
- }]
1892
- });
1885
+ emitEvent("kubb:build:end", [{
1886
+ files: files.map((file) => ({
1887
+ path: relativeStoragePath(config.root, file.path),
1888
+ name: file.name
1889
+ })),
1890
+ outputDir
1891
+ }]);
1893
1892
  });
1894
1893
  on("kubb:files:processing:start", ({ files }) => {
1895
- sendDataMessage({
1896
- type: "kubb:files:processing:start",
1897
- data: [{ total: files.length }]
1898
- });
1894
+ emitEvent("kubb:files:processing:start", [{ total: files.length }]);
1899
1895
  });
1900
1896
  on("kubb:files:processing:update", ({ files }) => {
1901
- sendDataMessage({
1902
- type: "kubb:files:processing:update",
1903
- data: [{ files: files.map(({ file, processed, total, percentage }) => ({
1904
- file: file.path,
1905
- processed,
1906
- total,
1907
- percentage
1908
- })) }]
1909
- });
1897
+ emitEvent("kubb:files:processing:update", [{ files: files.map(({ file, processed, total, percentage }) => ({
1898
+ file: relativeStoragePath(root, file.path),
1899
+ processed,
1900
+ total,
1901
+ percentage
1902
+ })) }]);
1910
1903
  });
1911
1904
  on("kubb:files:processing:end", ({ files }) => {
1912
- sendDataMessage({
1913
- type: "kubb:files:processing:end",
1914
- data: [{ total: files.length }]
1915
- });
1905
+ emitEvent("kubb:files:processing:end", [{ total: files.length }]);
1916
1906
  });
1917
1907
  for (const type of [
1918
1908
  "kubb:info",
1919
1909
  "kubb:success",
1920
1910
  "kubb:warn"
1921
1911
  ]) on(type, ({ message, info }) => {
1922
- sendDataMessage({
1923
- type,
1924
- data: [{
1925
- message,
1926
- info
1927
- }]
1928
- });
1912
+ emitEvent(type, [{
1913
+ message,
1914
+ info
1915
+ }]);
1929
1916
  });
1930
1917
  on("kubb:generation:start", ({ config }) => {
1931
- sendDataMessage({
1932
- type: "kubb:generation:start",
1933
- data: [{
1934
- name: config.name,
1935
- plugins: config.plugins.length
1936
- }]
1937
- });
1918
+ emitEvent("kubb:generation:start", [{
1919
+ name: config.name,
1920
+ plugins: config.plugins.length
1921
+ }]);
1938
1922
  });
1939
1923
  on("kubb:generation:end", async ({ config, storage, diagnostics = [], status, hrStart, filesCreated }) => {
1940
1924
  const { peerDependencies, missingDependencies } = await resolvePeerDependencies(config.plugins.map(({ name }) => name));
@@ -1947,29 +1931,32 @@ function setupEventsStream(ws$5, hooks, jobId, options = {}) {
1947
1931
  peerDependencies,
1948
1932
  missingDependencies
1949
1933
  });
1950
- sendDataMessage({
1951
- type: "kubb:generation:end",
1952
- data: []
1953
- });
1934
+ emitEvent("kubb:generation:end", []);
1954
1935
  if (!hrStart) return;
1955
- sendDataMessage({
1956
- type: "kubb:generation:summary",
1957
- data: [{
1958
- duration: Math.round(getElapsedMs(hrStart)),
1959
- fileCount: filesCreated ?? 0,
1960
- failedPlugins: _kubb_core.Diagnostics.failedPlugins(diagnostics).length,
1961
- status: status ?? "success"
1962
- }]
1963
- });
1936
+ emitEvent("kubb:generation:summary", [{
1937
+ duration: Math.round(getElapsedMs(hrStart)),
1938
+ fileCount: filesCreated ?? 0,
1939
+ failedPlugins: _kubb_core.Diagnostics.failedPlugins(diagnostics).length,
1940
+ status: status ?? "success"
1941
+ }]);
1964
1942
  });
1965
1943
  on("kubb:error", ({ error }) => {
1966
- sendDataMessage({
1967
- type: "kubb:error",
1968
- data: [{
1969
- message: error.message,
1970
- stack: error.stack
1971
- }]
1972
- });
1944
+ emitEvent("kubb:error", [{
1945
+ message: error.message,
1946
+ stack: error.stack
1947
+ }]);
1948
+ });
1949
+ on("kubb:diagnostic", ({ diagnostic }) => {
1950
+ const cause = "cause" in diagnostic ? diagnostic.cause : void 0;
1951
+ emitEvent("kubb:diagnostic", [{
1952
+ code: diagnostic.code,
1953
+ message: diagnostic.message,
1954
+ severity: diagnostic.severity,
1955
+ location: "location" in diagnostic ? diagnostic.location : void 0,
1956
+ help: "help" in diagnostic ? diagnostic.help : void 0,
1957
+ plugin: "plugin" in diagnostic ? diagnostic.plugin : void 0,
1958
+ stack: cause?.stack
1959
+ }]);
1973
1960
  });
1974
1961
  for (const type of [
1975
1962
  "kubb:lifecycle:start",
@@ -1981,62 +1968,144 @@ function setupEventsStream(ws$5, hooks, jobId, options = {}) {
1981
1968
  "kubb:hooks:start",
1982
1969
  "kubb:hooks:end"
1983
1970
  ]) on(type, () => {
1984
- sendDataMessage({
1985
- type,
1986
- data: []
1987
- });
1971
+ emitEvent(type, []);
1988
1972
  });
1989
1973
  on("kubb:hook:start", ({ id, command, args }) => {
1990
- sendDataMessage({
1991
- type: "kubb:hook:start",
1992
- data: [{
1993
- id,
1994
- command,
1995
- args: args ? [...args] : void 0
1996
- }]
1997
- });
1974
+ emitEvent("kubb:hook:start", [{
1975
+ id,
1976
+ command,
1977
+ args: args ? [...args] : void 0
1978
+ }]);
1998
1979
  });
1999
1980
  on("kubb:hook:line", ({ id, line }) => {
2000
- sendDataMessage({
2001
- type: "kubb:hook:line",
2002
- data: [{
2003
- id,
2004
- line
2005
- }]
2006
- });
1981
+ emitEvent("kubb:hook:line", [{
1982
+ id,
1983
+ line
1984
+ }]);
2007
1985
  });
2008
1986
  on("kubb:hook:end", ({ id, command, args, success, error }) => {
2009
- sendDataMessage({
2010
- type: "kubb:hook:end",
2011
- data: [{
2012
- id,
2013
- command,
2014
- args: args ? [...args] : void 0,
2015
- success,
2016
- error: error ? {
2017
- message: error.message,
2018
- stack: error.stack
2019
- } : void 0
2020
- }]
2021
- });
1987
+ emitEvent("kubb:hook:end", [{
1988
+ id,
1989
+ command,
1990
+ args: args ? [...args] : void 0,
1991
+ success,
1992
+ error: error ? {
1993
+ message: error.message,
1994
+ stack: error.stack
1995
+ } : void 0
1996
+ }]);
2022
1997
  });
2023
- return () => {
1998
+ /**
1999
+ * Takes this generation's listeners off the session emitter. Safe to call twice.
2000
+ */
2001
+ function detach() {
2024
2002
  for (const unhook of unhooks) unhook();
2003
+ unhooks.length = 0;
2004
+ }
2005
+ async function close() {
2006
+ if (closed) return;
2007
+ closed = true;
2008
+ detach();
2009
+ await writes;
2010
+ if (streamError) return;
2011
+ await writer.close().catch(() => void 0);
2012
+ }
2013
+ function fail(error) {
2014
+ detach();
2015
+ if (closed) return;
2016
+ closed = true;
2017
+ writer.abort(error).catch(() => void 0);
2018
+ }
2019
+ return {
2020
+ stream: transform.readable,
2021
+ close,
2022
+ dispose: () => fail(),
2023
+ fail
2025
2024
  };
2026
2025
  }
2027
2026
  //#endregion
2028
- //#region src/StudioSession.ts
2027
+ //#region src/rpc.ts
2029
2028
  /**
2030
- * How many files are read from storage at once when serving `studio:files` or packing a
2031
- * `studio:snapshot`.
2029
+ * The only methods Studio may call on an agent. A `StudioSession` carries far more than
2030
+ * {@link AgentApi}, so it is wrapped rather than exposed: what Cap'n Web can reach is exactly what
2031
+ * this class re-declares.
2032
2032
  */
2033
- const FILE_READ_CONCURRENCY = 50;
2033
+ var AgentRpcTarget = class extends capnweb.RpcTarget {
2034
+ api;
2035
+ constructor(api) {
2036
+ super();
2037
+ this.api = api;
2038
+ }
2039
+ connect() {
2040
+ return this.api.connect();
2041
+ }
2042
+ startGeneration(input) {
2043
+ return this.api.startGeneration(input);
2044
+ }
2045
+ saveConfig(input) {
2046
+ return this.api.saveConfig(input);
2047
+ }
2048
+ publishSnapshot(input) {
2049
+ return this.api.publishSnapshot(input);
2050
+ }
2051
+ readFiles(input) {
2052
+ return this.api.readFiles(input);
2053
+ }
2054
+ };
2055
+ /**
2056
+ * Opens an authenticated Cap'n Web session to Studio over a WebSocket. Rejects an unencrypted URL
2057
+ * before opening the socket, so a bearer token never reaches a plaintext host.
2058
+ *
2059
+ * @example
2060
+ * ```ts
2061
+ * const rpc = await connectWebSocketRpc({ url: 'wss://studio.kubb.dev/s/1', token, local: session })
2062
+ * await rpc.studio.ping()
2063
+ * ```
2064
+ */
2065
+ const connectWebSocketRpc = async ({ url, token, local }) => {
2066
+ const { protocol, hostname, host } = new URL(url);
2067
+ if (protocol !== "wss:" && !(protocol === "ws:" && (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]"))) throw new Error(`Refusing unencrypted WebSocket to ${host}`);
2068
+ const socket = createWebsocket(url, { headers: { Authorization: `Bearer ${token}` } });
2069
+ const closed = new Promise((resolve) => socket.once("close", resolve));
2070
+ const studio = (0, capnweb.newWebSocketRpcSession)(socket, new AgentRpcTarget(local));
2071
+ studio.onRpcBroken(() => socket.close());
2072
+ return {
2073
+ studio,
2074
+ closed,
2075
+ close: () => studio[Symbol.dispose]()
2076
+ };
2077
+ };
2078
+ //#endregion
2079
+ //#region src/StudioSession.ts
2034
2080
  /**
2035
- * How long the `agent:connect` handshake may go unacknowledged before `studio:ready` is given up
2036
- * on for this open. A Studio that predates the ack never sends one, so this only ever produces a
2037
- * warning, not a reconnect.
2081
+ * How many files are read from storage at once when serving `readFiles` or packing a snapshot.
2038
2082
  */
2039
- const READY_TIMEOUT_MS = 1e4;
2083
+ const FILE_READ_CONCURRENCY = 50;
2084
+ var GenerationRunTarget = class extends capnweb.RpcTarget {
2085
+ generationStream;
2086
+ generationResult;
2087
+ cancelGeneration;
2088
+ stopGeneration;
2089
+ constructor(generationStream, generationResult, cancelGeneration, stopGeneration) {
2090
+ super();
2091
+ this.generationStream = generationStream;
2092
+ this.generationResult = generationResult;
2093
+ this.cancelGeneration = cancelGeneration;
2094
+ this.stopGeneration = stopGeneration;
2095
+ }
2096
+ async events() {
2097
+ return this.generationStream;
2098
+ }
2099
+ result() {
2100
+ return this.generationResult;
2101
+ }
2102
+ cancel() {
2103
+ return this.cancelGeneration();
2104
+ }
2105
+ [Symbol.dispose]() {
2106
+ this.stopGeneration();
2107
+ }
2108
+ };
2040
2109
  /**
2041
2110
  * Fills in a host's options: the hosted Studio URL, the current working directory, and every
2042
2111
  * permission off unless granted. Idempotent, so a reconnect can pass an already-resolved bag
@@ -2076,7 +2145,7 @@ function reconnect(options) {
2076
2145
  const timer = setTimeout(() => {
2077
2146
  signal?.removeEventListener("abort", cancel);
2078
2147
  if (signal?.aborted) return;
2079
- new StudioSession(options).connect().catch((error) => {
2148
+ new StudioSession(options).start().catch((error) => {
2080
2149
  if (logLevel !== void 0 && logLevel > _kubb_core.logLevel.silent) console.error((0, node_util.styleText)("red", `Reconnect attempt to Kubb Studio failed: ${getErrorMessage(error)}`));
2081
2150
  if (error instanceof InvalidAgentTokenError) {
2082
2151
  onTokenRejected?.(error);
@@ -2088,8 +2157,8 @@ function reconnect(options) {
2088
2157
  signal?.addEventListener("abort", cancel, { once: true });
2089
2158
  }
2090
2159
  /**
2091
- * One WebSocket session with Studio: opening it, keeping it alive, and running the commands it
2092
- * sends. `createClient` opens one per pool slot and is the only caller.
2160
+ * One agent-to-Studio RPC transport: opening it, keeping it alive, and serving remote methods.
2161
+ * `createClient` opens one per pool slot and is the only caller.
2093
2162
  */
2094
2163
  var StudioSession = class {
2095
2164
  #options;
@@ -2104,17 +2173,20 @@ var StudioSession = class {
2104
2173
  * Before it resolves there is nothing to disconnect and no sandbox flag to read.
2105
2174
  */
2106
2175
  #session;
2107
- #ws;
2176
+ #rpc;
2108
2177
  #studioVersion;
2109
- #activeJobId = null;
2110
2178
  #disposed = false;
2111
2179
  #isGenerating = false;
2112
2180
  #heartbeatTimer;
2113
- #readyTimer;
2114
- #lastPongAt = Date.now();
2115
2181
  #lastGeneration;
2182
+ /**
2183
+ * Resolves when Studio calls {@link StudioSession.connect}. `studio:ready` waits on this so the
2184
+ * host does not queue jobs before the agent session is registered.
2185
+ */
2186
+ #connectAck = Promise.withResolvers();
2116
2187
  constructor(options) {
2117
2188
  this.#options = applyStudioDefaults(options);
2189
+ this.#connectAck.promise.catch(() => {});
2118
2190
  }
2119
2191
  /**
2120
2192
  * A sandbox agent runs on Studio's own infrastructure, so it has no user project to touch.
@@ -2141,7 +2213,7 @@ var StudioSession = class {
2141
2213
  get #canRead() {
2142
2214
  return this.#isSandbox || this.#options.permissions.allowRead;
2143
2215
  }
2144
- async connect() {
2216
+ async start() {
2145
2217
  const { token, studioUrl, signal, heartbeatInterval, installLogger } = this.#options;
2146
2218
  await installLogger?.(this.#hooks);
2147
2219
  try {
@@ -2152,48 +2224,65 @@ var StudioSession = class {
2152
2224
  });
2153
2225
  this.#session = session;
2154
2226
  this.#studioVersion = session.version;
2155
- const ws = createWebsocket(session.wsUrl, { headers: { Authorization: `Bearer ${token}` } });
2156
- this.#ws = ws;
2157
- this.#listen(ws, "open", this.#onOpen);
2158
- this.#listen(ws, "close", this.#onClose);
2159
- this.#listen(ws, "error", this.#onError);
2160
- this.#listen(ws, "message", this.#onMessage);
2227
+ const rpc = await (this.#options.connector ?? connectWebSocketRpc)({
2228
+ url: session.url,
2229
+ token,
2230
+ local: this
2231
+ });
2232
+ this.#rpc = rpc;
2233
+ rpc.closed.then(this.#onClose);
2161
2234
  signal?.addEventListener("abort", this.#onAbort, { once: true });
2162
2235
  this.#unhooks.push(() => signal?.removeEventListener("abort", this.#onAbort));
2163
- this.#heartbeatTimer = setInterval(() => this.#sendHeartbeat(), heartbeatInterval);
2164
- this.#unhooks.push(this.#hooks.hook("kubb:error", ({ error }) => sendErrorMessage(ws, error, this.#activeJobId ?? "connection")));
2236
+ this.#scheduleHeartbeat(heartbeatInterval);
2237
+ await this.#hooks.callHook("studio:connected", {
2238
+ url: studioUrl,
2239
+ versions: {
2240
+ studio: this.#studioVersion,
2241
+ kubb: version,
2242
+ agent: this.#options.version
2243
+ }
2244
+ });
2245
+ await this.#connectAck.promise;
2246
+ await this.#hooks.callHook("studio:ready", {});
2165
2247
  } catch (error) {
2248
+ this.#disposed = true;
2249
+ this.dispose();
2166
2250
  await this.#hooks.callHook("studio:error", { error: toError(error) });
2167
2251
  if (error instanceof InvalidAgentTokenError) throw error;
2168
2252
  reconnect(this.#options);
2169
2253
  }
2170
2254
  }
2171
- /**
2172
- * Adds a socket listener and tracks its remover, so `dispose` detaches every listener at once.
2173
- */
2174
- #listen(ws, event, listener) {
2175
- ws.addEventListener(event, listener);
2176
- this.#unhooks.push(() => ws.removeEventListener(event, listener));
2177
- }
2178
2255
  #warn(message) {
2179
2256
  return this.#hooks.callHook("studio:warn", { message });
2180
2257
  }
2181
2258
  /**
2182
- * Forwards a failure to Studio over the connection emitter, which `connect` wired to this
2183
- * socket. Swallows a listener's own failure, since this is already the error path.
2259
+ * Declines a request: logs why locally, then tells Studio. The two wordings differ on purpose,
2260
+ * since the log names the request that was ignored and the error names what the caller can do.
2184
2261
  */
2185
- #emitError(error) {
2186
- return Promise.resolve(this.#hooks.callHook("kubb:error", { error })).catch(() => {});
2187
- }
2188
- #sendHeartbeat() {
2189
- if (Date.now() - this.#lastPongAt > this.#options.heartbeatInterval * 2) {
2190
- this.#warn("No reply from Kubb Studio, terminating the stale connection");
2191
- clearInterval(this.#heartbeatTimer);
2192
- this.#heartbeatTimer = void 0;
2193
- this.#ws?.terminate();
2194
- return;
2195
- }
2196
- if (this.#ws) sendAgentMessage(this.#ws, { type: "agent:ping" });
2262
+ async #refuse(reason, message) {
2263
+ await this.#warn(reason);
2264
+ throw new Error(message);
2265
+ }
2266
+ #scheduleHeartbeat(interval) {
2267
+ const rpc = this.#rpc;
2268
+ if (!rpc) return;
2269
+ this.#heartbeatTimer = setTimeout(async () => {
2270
+ try {
2271
+ await this.#ping(rpc);
2272
+ } catch {
2273
+ if (this.#rpc === rpc) rpc.close();
2274
+ return;
2275
+ }
2276
+ if (this.#rpc === rpc) this.#scheduleHeartbeat(interval);
2277
+ }, interval);
2278
+ }
2279
+ /**
2280
+ * Races `studio.ping()` against a deadline, so a half-open socket can't hang it forever.
2281
+ * */
2282
+ #ping(rpc) {
2283
+ const { promise: timedOut, reject: onTimeout } = Promise.withResolvers();
2284
+ const timer = setTimeout(() => onTimeout(/* @__PURE__ */ new Error("Heartbeat ping timed out")), agentDefaults.heartbeatTimeoutMs);
2285
+ return Promise.race([rpc.studio.ping(), timedOut]).finally(() => clearTimeout(timer));
2197
2286
  }
2198
2287
  /**
2199
2288
  * Reads `kubb.config.ts` and reports which plugin options Studio may edit.
@@ -2210,90 +2299,48 @@ var StudioSession = class {
2210
2299
  return;
2211
2300
  }
2212
2301
  }
2213
- async #sendConnectedPayload() {
2302
+ async connect() {
2214
2303
  const { configPath, root, version: version$1, loadConfig, permissions } = this.#options;
2215
- if (!this.#ws) return;
2216
- const config = await loadConfig();
2217
- sendAgentMessage(this.#ws, {
2218
- type: "agent:connect",
2219
- payload: {
2220
- versions: {
2221
- kubb: version,
2222
- agent: version$1
2223
- },
2224
- root,
2225
- config: {
2226
- path: configPath,
2227
- file: await this.#readConfigFileView(),
2228
- plugins: config.plugins.map((plugin) => ({
2229
- name: toPackageName(plugin.name),
2230
- options: plugin.options ?? {}
2231
- }))
2232
- },
2233
- permissions: {
2234
- ...permissions,
2235
- allowWrite: this.#canWrite,
2236
- allowInput: this.#canUseInput,
2237
- allowConfigEdit: this.#canEditConfig,
2238
- allowRead: this.#canRead
2239
- }
2240
- }
2241
- });
2242
- }
2243
- async #handleOpen() {
2244
- this.#lastPongAt = Date.now();
2245
- await this.#hooks.callHook("studio:connected", {
2246
- url: this.#options.studioUrl,
2304
+ const [config, file] = await Promise.all([loadConfig(), this.#readConfigFileView()]);
2305
+ const payload = {
2247
2306
  versions: {
2248
- studio: this.#studioVersion,
2249
2307
  kubb: version,
2250
- agent: this.#options.version
2308
+ agent: version$1
2309
+ },
2310
+ root,
2311
+ config: {
2312
+ path: configPath,
2313
+ file,
2314
+ plugins: config.plugins.map((plugin) => ({
2315
+ name: toPackageName(plugin.name),
2316
+ options: plugin.options ?? {}
2317
+ }))
2318
+ },
2319
+ permissions: {
2320
+ ...permissions,
2321
+ allowWrite: this.#canWrite,
2322
+ allowInput: this.#canUseInput,
2323
+ allowConfigEdit: this.#canEditConfig,
2324
+ allowRead: this.#canRead
2251
2325
  }
2252
- });
2253
- try {
2254
- await this.#sendConnectedPayload();
2255
- this.#waitForReady();
2256
- } catch (error) {
2257
- await this.#warn(`Failed to send the connect payload: ${getErrorMessage(error)}`);
2258
- }
2326
+ };
2327
+ this.#connectAck.resolve();
2328
+ return payload;
2259
2329
  }
2260
- /**
2261
- * Arms the timeout for Studio's `studio:ready` acknowledgement. Re-armed on every open, since a
2262
- * command sent while the agent is not attached is lost either way (see `#sendConnectedPayload`),
2263
- * so each reconnect needs its own fresh wait.
2264
- */
2265
- #waitForReady() {
2266
- if (this.#disposed) return;
2267
- clearTimeout(this.#readyTimer);
2268
- this.#readyTimer = setTimeout(() => {
2269
- this.#readyTimer = void 0;
2270
- Promise.resolve(this.#warn(`Kubb Studio did not confirm the connection was ready within ${READY_TIMEOUT_MS}ms`)).catch(() => {});
2271
- }, READY_TIMEOUT_MS);
2272
- }
2273
- #onOpen = () => void this.#handleOpen().catch(() => {});
2274
- #onAbort = () => void this.#end({
2275
- reason: "shutdown",
2276
- retry: false
2277
- });
2330
+ #onAbort = () => void this.#end({ retry: false });
2278
2331
  #onClose = () => void this.#end({ retry: true });
2279
- #onError = () => {
2280
- this.#hooks.callHook("studio:error", { error: /* @__PURE__ */ new Error("Failed to connect to Kubb Studio") });
2281
- this.#onClose();
2282
- };
2283
2332
  /**
2284
2333
  * Drops the socket and detaches every listener and timer this session added. Idempotent, and
2285
2334
  * safe before `connect` opened anything.
2286
2335
  *
2287
2336
  * @internal
2288
2337
  */
2289
- dispose(reason = "cleanup") {
2290
- clearInterval(this.#heartbeatTimer);
2338
+ dispose() {
2339
+ clearTimeout(this.#heartbeatTimer);
2291
2340
  this.#heartbeatTimer = void 0;
2292
- clearTimeout(this.#readyTimer);
2293
- this.#readyTimer = void 0;
2294
- try {
2295
- this.#ws?.close(1e3, reason);
2296
- } catch {}
2341
+ this.#rpc?.close();
2342
+ this.#rpc = void 0;
2343
+ this.#connectAck.reject(/* @__PURE__ */ new Error("Session ended before Studio called connect()"));
2297
2344
  for (const unhook of this.#unhooks) unhook();
2298
2345
  this.#unhooks.length = 0;
2299
2346
  }
@@ -2301,15 +2348,12 @@ var StudioSession = class {
2301
2348
  * Ends the session: tells Studio it is over, drops the socket, and optionally reconnects.
2302
2349
  * `#disposed` keeps the close event from running this twice, and a shutdown from reconnecting.
2303
2350
  */
2304
- async #end({ reason, retry }) {
2351
+ async #end({ retry }) {
2305
2352
  const { studioUrl, token, logLevel } = this.#options;
2306
2353
  if (this.#disposed) return;
2307
2354
  this.#disposed = true;
2308
- if (reason === "shutdown" && this.#ws) sendAgentMessage(this.#ws, {
2309
- type: "agent:disconnect",
2310
- reason: "shutdown"
2311
- });
2312
- this.dispose(reason);
2355
+ this.dispose();
2356
+ await this.#hooks.callHook("studio:disconnected", { reason: retry ? "connection closed" : "shutdown" });
2313
2357
  if (this.#session) await disconnect({
2314
2358
  sessionId: this.#session.sessionId,
2315
2359
  studioUrl,
@@ -2319,81 +2363,35 @@ var StudioSession = class {
2319
2363
  }).catch(() => {});
2320
2364
  if (retry) reconnect(this.#options);
2321
2365
  }
2322
- #onMessage = async (message) => {
2323
- try {
2324
- const data = JSON.parse(message.data);
2325
- if (require_protocol.isStudioPongMessage(data)) {
2326
- this.#lastPongAt = Date.now();
2327
- return;
2328
- }
2329
- if (require_protocol.isStudioReadyMessage(data)) {
2330
- clearTimeout(this.#readyTimer);
2331
- this.#readyTimer = void 0;
2332
- await this.#hooks.callHook("studio:ready", {});
2333
- return;
2334
- }
2335
- if (require_protocol.isDisconnectMessage(data)) {
2336
- await this.#handleDisconnect(data.reason);
2337
- return;
2338
- }
2339
- if (require_protocol.isCommandMessage(data)) {
2340
- await this.#handleCommand(data);
2341
- return;
2342
- }
2343
- await this.#warn(`Ignored an unknown message from Kubb Studio: ${data.type}`);
2344
- } catch (error) {
2345
- await this.#hooks.callHook("studio:error", { error: toError(error) });
2346
- await this.#emitError(toError(error));
2347
- }
2348
- };
2349
- /**
2350
- * Studio ended the session itself. A revoked one stays ended, an expired one gets a fresh
2351
- * session, and anything else is left to the socket's own close event.
2352
- */
2353
- async #handleDisconnect(reason) {
2354
- await this.#hooks.callHook("studio:disconnected", { reason });
2355
- if (reason !== "revoked" && reason !== "expired") return;
2356
- this.#disposed = true;
2357
- this.dispose(`session_${reason}`);
2358
- if (reason === "expired") reconnect(this.#options);
2359
- }
2360
- async #handleCommand(data) {
2361
- const ws = this.#ws;
2362
- if (!ws) return;
2363
- const command = data.type.slice(7);
2364
- await this.#hooks.callHook("studio:command:start", { command });
2365
- switch (data.type) {
2366
- case "studio:generate":
2367
- await this.#handleGenerate(ws, data, command);
2368
- return;
2369
- case "studio:connect":
2370
- this.#studioVersion = data.version ?? this.#studioVersion;
2371
- await this.#sendConnectedPayload();
2372
- await this.#hooks.callHook("studio:command:end", { command });
2373
- return;
2374
- case "studio:save":
2375
- await this.#handleSave(ws, data, command);
2376
- return;
2377
- case "studio:snapshot":
2378
- await this.#handleSnapshot(ws, data, command);
2379
- return;
2380
- case "studio:files":
2381
- await this.#handleFiles(ws, data, command);
2382
- return;
2383
- }
2366
+ startGeneration(data) {
2367
+ const generationStream = createGenerationStream(this.#hooks, data.jobId, { onGenerationEnd: (result) => {
2368
+ this.#lastGeneration = result;
2369
+ } });
2370
+ const controller = new AbortController();
2371
+ const result = this.#runGeneration(data, controller).then(async (value) => {
2372
+ await generationStream.close();
2373
+ return value;
2374
+ }).catch((error) => {
2375
+ generationStream.fail(error);
2376
+ throw error;
2377
+ });
2378
+ result.catch(() => {});
2379
+ return new GenerationRunTarget(generationStream.stream, result, async () => {
2380
+ controller.abort(/* @__PURE__ */ new Error("Generation canceled"));
2381
+ }, () => {
2382
+ controller.abort(/* @__PURE__ */ new Error("Generation canceled"));
2383
+ generationStream.dispose();
2384
+ });
2384
2385
  }
2385
- async #handleGenerate(ws, data, command) {
2386
- const { root, loadConfig, permissions, client } = this.#options;
2387
- if (this.#isGenerating) {
2388
- await this.#warn("Ignored generate: a generation is already in progress");
2389
- await this.#emitError(/* @__PURE__ */ new Error("A generation is already in progress, please wait for it to finish"));
2390
- return;
2391
- }
2386
+ async #runGeneration(data, controller) {
2387
+ if (this.#isGenerating) return this.#refuse("Ignored generate: a generation is already in progress", "A generation is already in progress, please wait for it to finish");
2392
2388
  this.#isGenerating = true;
2393
- this.#activeJobId = data.jobId;
2389
+ const command = "generate";
2390
+ const { root, loadConfig, permissions, client } = this.#options;
2394
2391
  try {
2392
+ await this.#hooks.callHook("studio:command:start", { command });
2395
2393
  const config = await loadConfig();
2396
- const patch = data.payload;
2394
+ const patch = data.config;
2397
2395
  const plugins = await mergePlugins(config.plugins, patch?.plugins);
2398
2396
  const adapter = await mergeAdapter(config.adapter, patch?.adapter);
2399
2397
  const inputOverride = this.#isSandbox ? patch?.input ?? "" : permissions.allowInput && patch?.input || void 0;
@@ -2404,9 +2402,7 @@ var StudioSession = class {
2404
2402
  }
2405
2403
  const resolvedPlugins = plugins ?? config.plugins;
2406
2404
  this.#lastGeneration = void 0;
2407
- const detach = [setupHookListener(this.#hooks, root), setupEventsStream(ws, this.#hooks, data.jobId, { onGenerationEnd: (result) => {
2408
- this.#lastGeneration = result;
2409
- } })];
2405
+ const detach = [setupHookListener(this.#hooks, root, controller.signal)];
2410
2406
  try {
2411
2407
  await generate({
2412
2408
  config: {
@@ -2423,7 +2419,8 @@ var StudioSession = class {
2423
2419
  plugins: resolvedPlugins,
2424
2420
  adapter
2425
2421
  },
2426
- hooks: this.#hooks
2422
+ hooks: this.#hooks,
2423
+ signal: controller.signal
2427
2424
  });
2428
2425
  } finally {
2429
2426
  for (const remove of detach) remove();
@@ -2432,102 +2429,70 @@ var StudioSession = class {
2432
2429
  command,
2433
2430
  info: `${resolvedPlugins.length} plugin${resolvedPlugins.length === 1 ? "" : "s"}, ${this.#canWrite ? "written to disk" : "in memory"}${inputOverride !== void 0 ? ", from a Studio spec" : ""}`
2434
2431
  });
2432
+ const files = [...this.#lastGeneration?.paths ?? []];
2433
+ return {
2434
+ status: "success",
2435
+ files,
2436
+ fileCount: files.length
2437
+ };
2435
2438
  } finally {
2436
2439
  this.#isGenerating = false;
2437
- this.#activeJobId = null;
2438
2440
  }
2439
2441
  }
2440
- async #handleSave(ws, data, command) {
2442
+ async saveConfig(data) {
2443
+ const command = "saveConfig";
2444
+ await this.#hooks.callHook("studio:command:start", { command });
2441
2445
  const { configPath, configFile } = this.#options;
2442
2446
  if (!Array.isArray(data.edits)) {
2443
2447
  await this.#warn("Ignored save: the message carried no edits");
2444
- sendAgentMessage(ws, {
2445
- type: "agent:save",
2446
- jobId: data.jobId,
2447
- payload: {
2448
- outcomes: [],
2449
- changed: false
2450
- }
2451
- });
2452
- return;
2448
+ return {
2449
+ outcomes: [],
2450
+ changed: false
2451
+ };
2453
2452
  }
2454
2453
  const edits = data.edits;
2455
- const refuse = (reason) => sendAgentMessage(ws, {
2456
- type: "agent:save",
2457
- jobId: data.jobId,
2458
- payload: {
2459
- outcomes: edits.map((edit) => ({
2460
- edit,
2461
- applied: false,
2462
- reason
2463
- })),
2464
- changed: false
2465
- }
2454
+ const refuse = (reason) => ({
2455
+ outcomes: edits.map((edit) => ({
2456
+ edit,
2457
+ applied: false,
2458
+ reason
2459
+ })),
2460
+ changed: false
2466
2461
  });
2467
2462
  if (!this.#canEditConfig) {
2468
2463
  await this.#warn("Ignored save: editing kubb.config.ts was not granted");
2469
- refuse("the agent was not granted permission to edit kubb.config.ts");
2470
- return;
2471
- }
2472
- if (this.#isGenerating) {
2473
- refuse("a generation is in progress");
2474
- return;
2464
+ return refuse("the agent was not granted permission to edit kubb.config.ts");
2475
2465
  }
2466
+ if (this.#isGenerating) return refuse("a generation is in progress");
2476
2467
  try {
2477
2468
  const { source: patched, outcomes, changed } = applyConfigEdits(await read(configFile), edits);
2478
2469
  if (changed) await (0, node_fs_promises.writeFile)(configFile, patched, "utf-8");
2479
- sendAgentMessage(ws, {
2480
- type: "agent:save",
2481
- jobId: data.jobId,
2482
- payload: {
2483
- outcomes,
2484
- changed,
2485
- file: changed ? await this.#readConfigFileView(patched) : void 0
2486
- }
2487
- });
2488
2470
  const applied = outcomes.filter((outcome) => outcome.applied).length;
2489
2471
  await this.#hooks.callHook("studio:command:end", {
2490
2472
  command,
2491
2473
  info: `applied ${applied}/${outcomes.length} edits to ${configPath}`
2492
2474
  });
2475
+ return {
2476
+ outcomes,
2477
+ changed,
2478
+ file: changed ? await this.#readConfigFileView(patched) : void 0
2479
+ };
2493
2480
  } catch (error) {
2494
2481
  await this.#hooks.callHook("studio:error", { error: toError(error) });
2495
- refuse(getErrorMessage(error));
2482
+ return refuse(getErrorMessage(error));
2496
2483
  }
2497
2484
  }
2498
- async #handleSnapshot(ws, data, command) {
2499
- const refuse = (message) => sendAgentMessage(ws, {
2500
- type: "agent:snapshot",
2501
- jobId: data.jobId,
2502
- payload: {
2503
- status: "error",
2504
- message
2505
- }
2506
- });
2507
- if (this.#isSandbox) {
2508
- await this.#warn("Ignored snapshot: a sandbox agent has no project to build a package from");
2509
- refuse("a sandbox agent has no project to build a package from");
2510
- return;
2511
- }
2512
- const { name, version, bundledDependencies, uploadPath } = data.payload;
2513
- if (!name || !version || !uploadPath) {
2514
- await this.#warn("Ignored snapshot: the message was missing required fields");
2515
- refuse("the message was missing required fields");
2516
- return;
2517
- }
2485
+ async publishSnapshot(data) {
2486
+ const command = "snapshot";
2487
+ await this.#hooks.callHook("studio:command:start", { command });
2488
+ if (this.#isSandbox) return this.#refuse("Ignored snapshot: a sandbox agent has no project to build a package from", "A sandbox agent has no project to build a package from");
2489
+ const { name, version, bundledDependencies, uploadPath } = data;
2490
+ if (!name || !version || !uploadPath) return this.#refuse("Ignored snapshot: the message was missing required fields", "The request was missing required fields");
2518
2491
  const generation = this.#lastGeneration;
2519
- if (!generation) {
2520
- await this.#warn("Ignored snapshot: no prior generation to pack");
2521
- refuse("no prior generation exists to pack, run a generation first");
2522
- return;
2523
- }
2492
+ if (!generation) return this.#refuse("Ignored snapshot: no prior generation to pack", "No prior generation exists to pack, run a generation first");
2524
2493
  const bundled = new Set(bundledDependencies ?? []);
2525
2494
  const missing = generation.missingDependencies.filter((dependency) => !bundled.has(dependency));
2526
- if (missing.length) {
2527
- await this.#warn(`Ignored snapshot: missing dependencies: ${missing.join(", ")}`);
2528
- refuse(`missing dependencies: ${missing.join(", ")}`);
2529
- return;
2530
- }
2495
+ if (missing.length) return this.#refuse(`Ignored snapshot: missing dependencies: ${missing.join(", ")}`, `Missing dependencies: ${missing.join(", ")}`);
2531
2496
  try {
2532
2497
  const files = {};
2533
2498
  await inParallel({
@@ -2544,68 +2509,50 @@ var StudioSession = class {
2544
2509
  peerDependencies: generation.peerDependencies
2545
2510
  });
2546
2511
  const { token, studioUrl } = this.#options;
2547
- const redirect = await fetch(new URL(uploadPath, studioUrl), {
2512
+ const uploadUrl = new URL(uploadPath, studioUrl);
2513
+ if (uploadUrl.origin !== new URL(studioUrl).origin) throw new Error("Snapshot upload path must stay on the Studio origin");
2514
+ const redirect = await fetch(uploadUrl, {
2548
2515
  method: "PUT",
2549
2516
  headers: { Authorization: `Bearer ${token}` },
2550
2517
  redirect: "manual"
2551
2518
  });
2552
2519
  const storageUrl = redirect.headers.get("location");
2553
2520
  if (redirect.status !== 307 || !storageUrl) throw new Error(`Studio did not provide a storage URL (status ${redirect.status})`);
2554
- const response = await fetch(storageUrl, {
2521
+ const storage = new URL(storageUrl);
2522
+ if (storage.protocol !== "https:" && storage.hostname !== "localhost" && storage.hostname !== "127.0.0.1") throw new Error(`Refusing snapshot upload to ${storage.origin}`);
2523
+ const response = await fetch(storage, {
2555
2524
  method: "PUT",
2556
- body: new Uint8Array(bytes)
2525
+ body: new Uint8Array(bytes),
2526
+ redirect: "error"
2557
2527
  });
2558
2528
  if (!response.ok) throw new Error(`Snapshot upload failed with status ${response.status}`);
2559
- sendAgentMessage(ws, {
2560
- type: "agent:snapshot",
2561
- jobId: data.jobId,
2562
- payload: {
2563
- status: "ok",
2564
- integrity,
2565
- peerDependencies: generation.peerDependencies
2566
- }
2567
- });
2568
2529
  await this.#hooks.callHook("studio:command:end", {
2569
2530
  command,
2570
2531
  info: `packed ${Object.keys(files).length} file${Object.keys(files).length === 1 ? "" : "s"}`
2571
2532
  });
2533
+ return {
2534
+ integrity,
2535
+ peerDependencies: generation.peerDependencies
2536
+ };
2572
2537
  } catch (error) {
2573
2538
  await this.#hooks.callHook("studio:error", { error: toError(error) });
2574
- refuse(getErrorMessage(error));
2539
+ throw error;
2575
2540
  }
2576
2541
  }
2577
- async #handleFiles(ws, data, command) {
2542
+ async readFiles(data) {
2543
+ const command = "readFiles";
2544
+ await this.#hooks.callHook("studio:command:start", { command });
2578
2545
  const { client } = this.#options;
2579
- const refuse = (message) => sendAgentMessage(ws, {
2580
- type: "agent:files",
2581
- jobId: data.jobId,
2582
- payload: {
2583
- status: "error",
2584
- message
2585
- }
2586
- });
2587
2546
  if (!this.#canRead) {
2588
2547
  await this.#warn("Ignored files: reading generated files was not granted");
2589
- refuse(`the agent was not granted permission to read generated files; set ${client?.kind === "cli" ? "--allow-read, or answer yes when kubb studio asks," : "KUBB_AGENT_ALLOW_READ=true"} to allow it`);
2590
- return;
2591
- }
2592
- if (!Array.isArray(data.payload?.paths)) {
2593
- await this.#warn("Ignored files: the message carried no paths");
2594
- refuse("the message carried no paths");
2595
- return;
2596
- }
2597
- const { paths } = data.payload;
2598
- if (paths.length > 50) {
2599
- await this.#warn(`Ignored files: requested ${paths.length} paths, more than the 50 allowed per request`);
2600
- refuse(`at most 50 paths may be requested at once`);
2601
- return;
2548
+ const remedy = client?.kind === "cli" ? "--allow-read, or answer yes when kubb studio asks," : "KUBB_AGENT_ALLOW_READ=true";
2549
+ throw new Error(`The agent was not granted permission to read generated files; set ${remedy} to allow it`);
2602
2550
  }
2551
+ if (!Array.isArray(data.paths)) return this.#refuse("Ignored files: the message carried no paths", "The request carried no paths");
2552
+ const { paths } = data;
2553
+ if (paths.length > 50) return this.#refuse(`Ignored files: requested ${paths.length} paths, more than the 50 allowed per request`, `At most 50 paths may be requested at once`);
2603
2554
  const generation = this.#lastGeneration;
2604
- if (!generation) {
2605
- await this.#warn("Ignored files: no prior generation to read from");
2606
- refuse("no prior generation to read from, run a generation first");
2607
- return;
2608
- }
2555
+ if (!generation) return this.#refuse("Ignored files: no prior generation to read from", "No prior generation to read from, run a generation first");
2609
2556
  const requested = paths.filter((path) => generation.paths.has(path));
2610
2557
  const files = {};
2611
2558
  await inParallel({
@@ -2616,18 +2563,11 @@ var StudioSession = class {
2616
2563
  if (content !== null) files[path] = content;
2617
2564
  }
2618
2565
  });
2619
- sendAgentMessage(ws, {
2620
- type: "agent:files",
2621
- jobId: data.jobId,
2622
- payload: {
2623
- status: "ok",
2624
- files
2625
- }
2626
- });
2627
2566
  await this.#hooks.callHook("studio:command:end", {
2628
2567
  command,
2629
2568
  info: `read ${Object.keys(files).length}/${paths.length} requested file${paths.length === 1 ? "" : "s"}`
2630
2569
  });
2570
+ return { files };
2631
2571
  }
2632
2572
  };
2633
2573
  //#endregion
@@ -2664,7 +2604,7 @@ function createClient({ storage, onAuthRequired, ...options }) {
2664
2604
  ...options,
2665
2605
  signal: controller.signal,
2666
2606
  onTokenRejected: notifyAuthRequired
2667
- }).connect()));
2607
+ }).start()));
2668
2608
  },
2669
2609
  disconnect() {
2670
2610
  controller.abort();
@@ -2836,12 +2776,13 @@ async function pollForPairingToken({ studioUrl = agentDefaults.studioUrl, sessio
2836
2776
  //#endregion
2837
2777
  exports.InvalidAgentTokenError = InvalidAgentTokenError;
2838
2778
  exports.PairingCanceledError = PairingCanceledError;
2779
+ exports.connectWebSocketRpc = connectWebSocketRpc;
2839
2780
  exports.createAgent = createAgent;
2840
2781
  exports.createClient = createClient;
2841
2782
  exports.createFileStorage = createFileStorage;
2842
2783
  exports.createJob = createJob;
2843
- exports.createJobId = require_protocol.createJobId;
2844
2784
  exports.defaultStudioUrl = defaultStudioUrl;
2785
+ exports.generationEventTypes = require_protocol.generationEventTypes;
2845
2786
  exports.machineTokenFrom = machineTokenFrom;
2846
2787
  exports.pollForPairingToken = pollForPairingToken;
2847
2788
  exports.runConnection = runConnection;