@kubb/studio 5.2.5 → 5.2.7

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
@@ -96,6 +96,38 @@ nothing can read it back.
96
96
  member can approve. A host that pairs a shared or tier-limited agent passes `clientId: 'kubb-agent'`
97
97
  and an `agentKind`, whose codes only an admin can approve.
98
98
 
99
+ ## Asynchronous jobs
100
+
101
+ CI and automation queue work with `createJob` and poll with `waitForJob`. Both send the
102
+ organization CI API key as `x-api-key`. They do not open a WebSocket.
103
+
104
+ | Step | Call | What it does |
105
+ | ------ | -------------------- | --------------------------------------------------------------------- |
106
+ | Queue | `POST /api/jobs` | Accepts a `generation` or `snapshot` job and returns `202` with an id |
107
+ | Status | `GET /api/jobs/{id}` | Returns the job until `success` or `failed` |
108
+
109
+ ```typescript
110
+ import { createJob, waitForJob } from '@kubb/studio'
111
+
112
+ const job = await createJob({
113
+ studioUrl: 'https://kubb.studio',
114
+ token: process.env.KUBB_TOKEN!,
115
+ type: 'snapshot',
116
+ agentId: agent.id,
117
+ name: '@scope/package',
118
+ version: '1.0.0',
119
+ })
120
+
121
+ const finished = await waitForJob({
122
+ studioUrl: 'https://kubb.studio',
123
+ token: process.env.KUBB_TOKEN!,
124
+ id: job.id,
125
+ })
126
+
127
+ if (finished.status === 'failed') throw new Error(finished.error)
128
+ const snapshot = finished.snapshot
129
+ ```
130
+
99
131
  ## Protocol
100
132
 
101
133
  `@kubb/studio/protocol` holds the WebSocket message types shared by both ends, so the agent and
package/dist/index.cjs CHANGED
@@ -564,12 +564,62 @@ async function disconnect({ sessionId, token, studioUrl, slug }) {
564
564
  });
565
565
  console.log((0, node_util.styleText)("green", `[${tag}] Disconnected from Studio`));
566
566
  } catch (error) {
567
+ const statusCode = error?.statusCode;
568
+ if (statusCode !== void 0 && statusCode >= 400 && statusCode < 500) return;
567
569
  console.warn((0, node_util.styleText)("yellow", `[${tag}] Failed to notify Studio of disconnection: ${getErrorMessage(error)}`));
568
570
  }
569
571
  }
572
+ /**
573
+ * Queues a generation or snapshot job on Studio (`POST /api/jobs`).
574
+ *
575
+ * Returns as soon as Studio accepts the job (`202`). Poll with {@link waitForJob} until it finishes.
576
+ * Authenticates with the organization CI API key via `x-api-key`.
577
+ *
578
+ * @example Snapshot job
579
+ * ```ts
580
+ * const job = await createJob({
581
+ * studioUrl: 'https://kubb.studio',
582
+ * token: process.env.KUBB_TOKEN!,
583
+ * type: 'snapshot',
584
+ * agentId: agent.id,
585
+ * name: '@kubb/demo',
586
+ * version: '1.0.0',
587
+ * })
588
+ * const finished = await waitForJob({ studioUrl, token, id: job.id })
589
+ * ```
590
+ */
591
+ async function createJob({ studioUrl, token, type, agentId, name, version, config }) {
592
+ const { job } = await (0, ofetch.ofetch)(`${studioUrl}/api/jobs`, {
593
+ method: "POST",
594
+ headers: { "x-api-key": token },
595
+ body: {
596
+ type,
597
+ agentId,
598
+ name,
599
+ version,
600
+ config
601
+ }
602
+ });
603
+ return job;
604
+ }
605
+ /**
606
+ * Polls `GET /api/jobs/{id}` until the job reaches `success` or `failed`.
607
+ *
608
+ * A `failed` job resolves normally. Check `job.status` and `job.error`. Throws only when the
609
+ * deadline passes before Studio finishes.
610
+ */
611
+ async function waitForJob({ studioUrl, token, id, timeoutMs = 6e4 }) {
612
+ const deadline = Date.now() + timeoutMs;
613
+ for (;;) {
614
+ const { job } = await (0, ofetch.ofetch)(`${studioUrl}/api/jobs/${id}`, { headers: { "x-api-key": token } });
615
+ if (job.status === "success" || job.status === "failed") return job;
616
+ if (Date.now() >= deadline) throw new Error("Timed out waiting for the Studio job");
617
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
618
+ }
619
+ }
570
620
  //#endregion
571
621
  //#region package.json
572
- var version = "5.2.5";
622
+ var version = "5.2.7";
573
623
  //#endregion
574
624
  //#region src/hooks.ts
575
625
  /**
@@ -1522,6 +1572,9 @@ const CONNECT_TIMEOUT_MS = 5e3;
1522
1572
  */
1523
1573
  const eventSeqCounters = /* @__PURE__ */ new WeakMap();
1524
1574
  const require$1 = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href);
1575
+ function relativeStoragePath(root, filePath) {
1576
+ return ((0, node_path.isAbsolute)(filePath) ? (0, node_path.relative)((0, node_path.resolve)(root), filePath) : filePath).replaceAll("\\", "/");
1577
+ }
1525
1578
  async function resolvePeerDependencies(names) {
1526
1579
  const uniqueNames = [...new Set(names.map(toPackageName))];
1527
1580
  const peerDependencies = {};
@@ -1576,9 +1629,10 @@ function sendAgentMessage(ws$3, message) {
1576
1629
  * Sends a single `kubb:error` payload to Studio, stamped from the same per-socket counter the event stream
1577
1630
  * uses so Studio can still order it against the generation events around it.
1578
1631
  */
1579
- function sendErrorMessage(ws$4, error) {
1632
+ function sendErrorMessage(ws$4, error, jobId) {
1580
1633
  sendAgentMessage(ws$4, {
1581
1634
  type: "agent:data",
1635
+ jobId,
1582
1636
  payload: {
1583
1637
  type: "kubb:error",
1584
1638
  data: [{
@@ -1593,7 +1647,7 @@ function sendErrorMessage(ws$4, error) {
1593
1647
  /**
1594
1648
  * Forwards selected Kubb lifecycle events to Studio as data messages for the active session.
1595
1649
  */
1596
- function setupEventsStream(ws$5, hooks) {
1650
+ function setupEventsStream(ws$5, hooks, jobId) {
1597
1651
  const unhooks = [];
1598
1652
  /**
1599
1653
  * Registers a listener and keeps its remover, so one generation's listeners come off the session
@@ -1605,6 +1659,7 @@ function setupEventsStream(ws$5, hooks) {
1605
1659
  function sendDataMessage(payload) {
1606
1660
  sendAgentMessage(ws$5, {
1607
1661
  type: "agent:data",
1662
+ jobId,
1608
1663
  payload: {
1609
1664
  ...payload,
1610
1665
  timestamp: Date.now(),
@@ -1703,7 +1758,7 @@ function setupEventsStream(ws$5, hooks) {
1703
1758
  limit: FILE_READ_CONCURRENCY,
1704
1759
  run: async (path) => {
1705
1760
  const content = await storage.readItem(path);
1706
- if (content !== null) files[path] = content;
1761
+ if (content !== null) files[relativeStoragePath(config.root, path)] = content;
1707
1762
  }
1708
1763
  });
1709
1764
  sendDataMessage({
@@ -1864,6 +1919,7 @@ var StudioSession = class {
1864
1919
  #session;
1865
1920
  #ws;
1866
1921
  #studioVersion;
1922
+ #activeJobId = null;
1867
1923
  #disposed = false;
1868
1924
  #isGenerating = false;
1869
1925
  #heartbeatTimer;
@@ -1911,7 +1967,7 @@ var StudioSession = class {
1911
1967
  signal?.addEventListener("abort", this.#onAbort, { once: true });
1912
1968
  this.#unhooks.push(() => signal?.removeEventListener("abort", this.#onAbort));
1913
1969
  this.#heartbeatTimer = setInterval(() => this.#sendHeartbeat(), heartbeatInterval);
1914
- this.#unhooks.push(this.#hooks.hook("kubb:error", ({ error }) => sendErrorMessage(ws, error)));
1970
+ this.#unhooks.push(this.#hooks.hook("kubb:error", ({ error }) => sendErrorMessage(ws, error, this.#activeJobId ?? "connection")));
1915
1971
  } catch (error) {
1916
1972
  await this.#hooks.callHook("studio:error", { error: toError(error) });
1917
1973
  if (error instanceof InvalidAgentTokenError) throw error;
@@ -2132,6 +2188,7 @@ var StudioSession = class {
2132
2188
  return;
2133
2189
  }
2134
2190
  this.#isGenerating = true;
2191
+ this.#activeJobId = data.jobId;
2135
2192
  try {
2136
2193
  const config = await loadConfig();
2137
2194
  const patch = data.payload;
@@ -2144,7 +2201,7 @@ var StudioSession = class {
2144
2201
  await this.#warn(`Ignored the spec from Studio; set ${remedy} to generate from it`);
2145
2202
  }
2146
2203
  const resolvedPlugins = plugins ?? config.plugins;
2147
- const detach = [setupHookListener(this.#hooks, root), setupEventsStream(ws, this.#hooks)];
2204
+ const detach = [setupHookListener(this.#hooks, root), setupEventsStream(ws, this.#hooks, data.jobId)];
2148
2205
  try {
2149
2206
  await generate({
2150
2207
  config: {
@@ -2172,6 +2229,7 @@ var StudioSession = class {
2172
2229
  });
2173
2230
  } finally {
2174
2231
  this.#isGenerating = false;
2232
+ this.#activeJobId = null;
2175
2233
  }
2176
2234
  }
2177
2235
  async #handleSave(ws, data, command) {
@@ -2180,6 +2238,7 @@ var StudioSession = class {
2180
2238
  await this.#warn("Ignored save: the message carried no edits");
2181
2239
  sendAgentMessage(ws, {
2182
2240
  type: "agent:save",
2241
+ jobId: data.jobId,
2183
2242
  payload: {
2184
2243
  outcomes: [],
2185
2244
  changed: false
@@ -2190,6 +2249,7 @@ var StudioSession = class {
2190
2249
  const edits = data.edits;
2191
2250
  const refuse = (reason) => sendAgentMessage(ws, {
2192
2251
  type: "agent:save",
2252
+ jobId: data.jobId,
2193
2253
  payload: {
2194
2254
  outcomes: edits.map((edit) => ({
2195
2255
  edit,
@@ -2213,6 +2273,7 @@ var StudioSession = class {
2213
2273
  if (changed) await (0, node_fs_promises.writeFile)(configFile, patched, "utf-8");
2214
2274
  sendAgentMessage(ws, {
2215
2275
  type: "agent:save",
2276
+ jobId: data.jobId,
2216
2277
  payload: {
2217
2278
  outcomes,
2218
2279
  changed,
@@ -2438,10 +2499,13 @@ exports.InvalidAgentTokenError = InvalidAgentTokenError;
2438
2499
  exports.PairingCanceledError = PairingCanceledError;
2439
2500
  exports.createClient = createClient;
2440
2501
  exports.createFileStorage = createFileStorage;
2502
+ exports.createJob = createJob;
2503
+ exports.createJobId = require_protocol.createJobId;
2441
2504
  exports.defaultStudioUrl = defaultStudioUrl;
2442
2505
  exports.pollForPairingToken = pollForPairingToken;
2443
2506
  exports.runConnection = runConnection;
2444
2507
  exports.setStorage = setStorage;
2445
2508
  exports.startPairing = startPairing;
2509
+ exports.waitForJob = waitForJob;
2446
2510
 
2447
2511
  //# sourceMappingURL=index.cjs.map