@kubb/studio 5.2.4 → 5.2.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.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { t as __name } from "./rolldown-runtime-CRm0XQPb.js";
2
- import { a as AgentPermissions, c as ClientInfo } from "./index-V3fjg2mj.js";
2
+ import { AgentPermissions, ClientInfo, createJobId } from "./protocol.js";
3
3
  import { Storage } from "unstorage";
4
4
  import { Config, Hookable, KubbHooks } from "@kubb/core";
5
5
  //#region src/api.d.ts
@@ -147,6 +147,11 @@ type StudioConnectedContext = {
147
147
  agent: string;
148
148
  };
149
149
  };
150
+ /**
151
+ * Fired once Studio confirms the `agent:connect` handshake was received and the session is fully
152
+ * registered. Distinct from `studio:connected`, which only means the socket is open.
153
+ */
154
+ type StudioReadyContext = Record<string, never>;
150
155
  type StudioDisconnectedContext = {
151
156
  /**
152
157
  * Why Studio ended the session.
@@ -187,6 +192,7 @@ declare global {
187
192
  interface KubbHooksRegistry {
188
193
  'studio:connecting': [ctx: StudioConnectingContext];
189
194
  'studio:connected': [ctx: StudioConnectedContext];
195
+ 'studio:ready': [ctx: StudioReadyContext];
190
196
  'studio:disconnected': [ctx: StudioDisconnectedContext];
191
197
  'studio:command:start': [ctx: StudioCommandStartContext];
192
198
  'studio:command:end': [ctx: StudioCommandEndContext];
@@ -371,5 +377,5 @@ type PollOptions = {
371
377
  */
372
378
  export declare function pollForPairingToken({ studioUrl, session, signal }: PollOptions): Promise<PairingResult>;
373
379
  //#endregion
374
- export type { Client, ClientOptions, ConnectionOptions, StudioConnectedContext };
380
+ export { type Client, type ClientOptions, type ConnectionOptions, type StudioConnectedContext, createJobId };
375
381
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { t as __name } from "./rolldown-runtime-CRm0XQPb.js";
2
- import { isCommandMessage, isDisconnectMessage, isStudioPingMessage } from "./protocol.js";
2
+ import { createJobId, isCommandMessage, isDisconnectMessage, isStudioPingMessage, isStudioReadyMessage } from "./protocol.js";
3
3
  import { createRequire } from "node:module";
4
4
  import { styleText } from "node:util";
5
5
  import { readFile, writeFile } from "node:fs/promises";
6
- import path from "node:path";
6
+ import path, { isAbsolute, relative, resolve } from "node:path";
7
7
  import { spawn } from "node:child_process";
8
8
  import { FetchError, ofetch } from "ofetch";
9
9
  import { hash, randomBytes } from "node:crypto";
@@ -559,12 +559,14 @@ async function disconnect({ sessionId, token, studioUrl, slug }) {
559
559
  });
560
560
  console.log(styleText("green", `[${tag}] Disconnected from Studio`));
561
561
  } catch (error) {
562
+ const statusCode = error?.statusCode;
563
+ if (statusCode !== void 0 && statusCode >= 400 && statusCode < 500) return;
562
564
  console.warn(styleText("yellow", `[${tag}] Failed to notify Studio of disconnection: ${getErrorMessage(error)}`));
563
565
  }
564
566
  }
565
567
  //#endregion
566
568
  //#region package.json
567
- var version = "5.2.4";
569
+ var version = "5.2.6";
568
570
  //#endregion
569
571
  //#region src/hooks.ts
570
572
  /**
@@ -1517,6 +1519,9 @@ const CONNECT_TIMEOUT_MS = 5e3;
1517
1519
  */
1518
1520
  const eventSeqCounters = /* @__PURE__ */ new WeakMap();
1519
1521
  const require = createRequire(import.meta.url);
1522
+ function relativeStoragePath(root, filePath) {
1523
+ return (isAbsolute(filePath) ? relative(resolve(root), filePath) : filePath).replaceAll("\\", "/");
1524
+ }
1520
1525
  async function resolvePeerDependencies(names) {
1521
1526
  const uniqueNames = [...new Set(names.map(toPackageName))];
1522
1527
  const peerDependencies = {};
@@ -1571,9 +1576,10 @@ function sendAgentMessage(ws, message) {
1571
1576
  * Sends a single `kubb:error` payload to Studio, stamped from the same per-socket counter the event stream
1572
1577
  * uses so Studio can still order it against the generation events around it.
1573
1578
  */
1574
- function sendErrorMessage(ws, error) {
1579
+ function sendErrorMessage(ws, error, jobId) {
1575
1580
  sendAgentMessage(ws, {
1576
1581
  type: "agent:data",
1582
+ jobId,
1577
1583
  payload: {
1578
1584
  type: "kubb:error",
1579
1585
  data: [{
@@ -1588,7 +1594,7 @@ function sendErrorMessage(ws, error) {
1588
1594
  /**
1589
1595
  * Forwards selected Kubb lifecycle events to Studio as data messages for the active session.
1590
1596
  */
1591
- function setupEventsStream(ws, hooks) {
1597
+ function setupEventsStream(ws, hooks, jobId) {
1592
1598
  const unhooks = [];
1593
1599
  /**
1594
1600
  * Registers a listener and keeps its remover, so one generation's listeners come off the session
@@ -1600,6 +1606,7 @@ function setupEventsStream(ws, hooks) {
1600
1606
  function sendDataMessage(payload) {
1601
1607
  sendAgentMessage(ws, {
1602
1608
  type: "agent:data",
1609
+ jobId,
1603
1610
  payload: {
1604
1611
  ...payload,
1605
1612
  timestamp: Date.now(),
@@ -1698,7 +1705,7 @@ function setupEventsStream(ws, hooks) {
1698
1705
  limit: FILE_READ_CONCURRENCY,
1699
1706
  run: async (path) => {
1700
1707
  const content = await storage.readItem(path);
1701
- if (content !== null) files[path] = content;
1708
+ if (content !== null) files[relativeStoragePath(config.root, path)] = content;
1702
1709
  }
1703
1710
  });
1704
1711
  sendDataMessage({
@@ -1786,6 +1793,12 @@ function setupEventsStream(ws, hooks) {
1786
1793
  //#endregion
1787
1794
  //#region src/StudioSession.ts
1788
1795
  /**
1796
+ * How long the `agent:connect` handshake may go unacknowledged before `studio:ready` is given up
1797
+ * on for this open. A Studio that predates the ack never sends one, so this only ever produces a
1798
+ * warning, not a reconnect.
1799
+ */
1800
+ const READY_TIMEOUT_MS = 1e4;
1801
+ /**
1789
1802
  * Fills in a host's options: the hosted Studio URL, the current working directory, and every
1790
1803
  * permission off unless granted. Idempotent, so a reconnect can pass an already-resolved bag
1791
1804
  * back in.
@@ -1853,9 +1866,11 @@ var StudioSession = class {
1853
1866
  #session;
1854
1867
  #ws;
1855
1868
  #studioVersion;
1869
+ #activeJobId = null;
1856
1870
  #disposed = false;
1857
1871
  #isGenerating = false;
1858
1872
  #heartbeatTimer;
1873
+ #readyTimer;
1859
1874
  #lastPongAt = Date.now();
1860
1875
  constructor(options) {
1861
1876
  this.#options = applyStudioDefaults(options);
@@ -1899,7 +1914,7 @@ var StudioSession = class {
1899
1914
  signal?.addEventListener("abort", this.#onAbort, { once: true });
1900
1915
  this.#unhooks.push(() => signal?.removeEventListener("abort", this.#onAbort));
1901
1916
  this.#heartbeatTimer = setInterval(() => this.#sendHeartbeat(), heartbeatInterval);
1902
- this.#unhooks.push(this.#hooks.hook("kubb:error", ({ error }) => sendErrorMessage(ws, error)));
1917
+ this.#unhooks.push(this.#hooks.hook("kubb:error", ({ error }) => sendErrorMessage(ws, error, this.#activeJobId ?? "connection")));
1903
1918
  } catch (error) {
1904
1919
  await this.#hooks.callHook("studio:error", { error: toError(error) });
1905
1920
  if (error instanceof InvalidAgentTokenError) throw error;
@@ -1989,10 +2004,24 @@ var StudioSession = class {
1989
2004
  });
1990
2005
  try {
1991
2006
  await this.#sendConnectedPayload();
2007
+ this.#waitForReady();
1992
2008
  } catch (error) {
1993
2009
  await this.#warn(`Failed to send the connect payload: ${getErrorMessage(error)}`);
1994
2010
  }
1995
2011
  }
2012
+ /**
2013
+ * Arms the timeout for Studio's `studio:ready` acknowledgement. Re-armed on every open, since a
2014
+ * command sent while the agent is not attached is lost either way (see `#sendConnectedPayload`),
2015
+ * so each reconnect needs its own fresh wait.
2016
+ */
2017
+ #waitForReady() {
2018
+ if (this.#disposed) return;
2019
+ clearTimeout(this.#readyTimer);
2020
+ this.#readyTimer = setTimeout(() => {
2021
+ this.#readyTimer = void 0;
2022
+ Promise.resolve(this.#warn(`Kubb Studio did not confirm the connection was ready within ${READY_TIMEOUT_MS}ms`)).catch(() => {});
2023
+ }, READY_TIMEOUT_MS);
2024
+ }
1996
2025
  #onOpen = () => void this.#handleOpen().catch(() => {});
1997
2026
  #onAbort = () => void this.#end({
1998
2027
  reason: "shutdown",
@@ -2012,6 +2041,8 @@ var StudioSession = class {
2012
2041
  dispose(reason = "cleanup") {
2013
2042
  clearInterval(this.#heartbeatTimer);
2014
2043
  this.#heartbeatTimer = void 0;
2044
+ clearTimeout(this.#readyTimer);
2045
+ this.#readyTimer = void 0;
2015
2046
  try {
2016
2047
  this.#ws?.close(1e3, reason);
2017
2048
  } catch {}
@@ -2046,6 +2077,12 @@ var StudioSession = class {
2046
2077
  this.#lastPongAt = Date.now();
2047
2078
  return;
2048
2079
  }
2080
+ if (isStudioReadyMessage(data)) {
2081
+ clearTimeout(this.#readyTimer);
2082
+ this.#readyTimer = void 0;
2083
+ await this.#hooks.callHook("studio:ready", {});
2084
+ return;
2085
+ }
2049
2086
  if (isDisconnectMessage(data)) {
2050
2087
  await this.#handleDisconnect(data.reason);
2051
2088
  return;
@@ -2098,6 +2135,7 @@ var StudioSession = class {
2098
2135
  return;
2099
2136
  }
2100
2137
  this.#isGenerating = true;
2138
+ this.#activeJobId = data.jobId;
2101
2139
  try {
2102
2140
  const config = await loadConfig();
2103
2141
  const patch = data.payload;
@@ -2110,7 +2148,7 @@ var StudioSession = class {
2110
2148
  await this.#warn(`Ignored the spec from Studio; set ${remedy} to generate from it`);
2111
2149
  }
2112
2150
  const resolvedPlugins = plugins ?? config.plugins;
2113
- const detach = [setupHookListener(this.#hooks, root), setupEventsStream(ws, this.#hooks)];
2151
+ const detach = [setupHookListener(this.#hooks, root), setupEventsStream(ws, this.#hooks, data.jobId)];
2114
2152
  try {
2115
2153
  await generate({
2116
2154
  config: {
@@ -2138,6 +2176,7 @@ var StudioSession = class {
2138
2176
  });
2139
2177
  } finally {
2140
2178
  this.#isGenerating = false;
2179
+ this.#activeJobId = null;
2141
2180
  }
2142
2181
  }
2143
2182
  async #handleSave(ws, data, command) {
@@ -2146,6 +2185,7 @@ var StudioSession = class {
2146
2185
  await this.#warn("Ignored save: the message carried no edits");
2147
2186
  sendAgentMessage(ws, {
2148
2187
  type: "agent:save",
2188
+ jobId: data.jobId,
2149
2189
  payload: {
2150
2190
  outcomes: [],
2151
2191
  changed: false
@@ -2156,6 +2196,7 @@ var StudioSession = class {
2156
2196
  const edits = data.edits;
2157
2197
  const refuse = (reason) => sendAgentMessage(ws, {
2158
2198
  type: "agent:save",
2199
+ jobId: data.jobId,
2159
2200
  payload: {
2160
2201
  outcomes: edits.map((edit) => ({
2161
2202
  edit,
@@ -2179,6 +2220,7 @@ var StudioSession = class {
2179
2220
  if (changed) await writeFile(configFile, patched, "utf-8");
2180
2221
  sendAgentMessage(ws, {
2181
2222
  type: "agent:save",
2223
+ jobId: data.jobId,
2182
2224
  payload: {
2183
2225
  outcomes,
2184
2226
  changed,
@@ -2400,6 +2442,6 @@ async function pollForPairingToken({ studioUrl = agentDefaults.studioUrl, sessio
2400
2442
  throw new Error("The pairing code expired, pair again");
2401
2443
  }
2402
2444
  //#endregion
2403
- export { InvalidAgentTokenError, PairingCanceledError, createClient, createFileStorage, defaultStudioUrl, pollForPairingToken, runConnection, setStorage, startPairing };
2445
+ export { InvalidAgentTokenError, PairingCanceledError, createClient, createFileStorage, createJobId, defaultStudioUrl, pollForPairingToken, runConnection, setStorage, startPairing };
2404
2446
 
2405
2447
  //# sourceMappingURL=index.js.map