@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/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-Dq2uq38g.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
@@ -11,6 +11,109 @@ import { Config, Hookable, KubbHooks } from "@kubb/core";
11
11
  export declare class InvalidAgentTokenError extends Error {
12
12
  constructor(studioUrl: string, options?: ErrorOptions);
13
13
  }
14
+ /**
15
+ * Status values returned by Studio's jobs API.
16
+ */
17
+ type StudioJobStatus = 'queued' | 'running' | 'success' | 'failed';
18
+ /**
19
+ * Package view returned on a successful snapshot job from Studio.
20
+ */
21
+ type StudioSnapshot = {
22
+ /**
23
+ * Immutable snapshot id.
24
+ */
25
+ id: string;
26
+ /**
27
+ * npm package name, or `null` when Studio stored none.
28
+ */
29
+ name: string | null;
30
+ /**
31
+ * npm package version, or `null` when Studio stored none.
32
+ */
33
+ version: string | null;
34
+ /**
35
+ * Subresource integrity hash for the tarball, or `null` when unavailable.
36
+ */
37
+ integrity: string | null;
38
+ /**
39
+ * Preferable download path, often the readable `/packages/{agentSlug}/{name}.tgz` form.
40
+ */
41
+ url: string;
42
+ /**
43
+ * Stable download path keyed by snapshot id.
44
+ */
45
+ snapshotIdUrl: string;
46
+ /**
47
+ * ISO timestamp after which Studio may delete the tarball.
48
+ */
49
+ expiresAt: string;
50
+ };
51
+ /**
52
+ * Job record from `POST /api/jobs` and `GET /api/jobs/{id}`.
53
+ */
54
+ type StudioJob = {
55
+ /**
56
+ * Job id returned by Studio when the job was queued.
57
+ */
58
+ id: string;
59
+ /**
60
+ * Current status. Poll until `success` or `failed`.
61
+ */
62
+ status: StudioJobStatus;
63
+ /**
64
+ * Failure message when `status` is `failed`.
65
+ */
66
+ error?: string;
67
+ /**
68
+ * Package view when a snapshot job finished successfully.
69
+ */
70
+ snapshot?: StudioSnapshot;
71
+ };
72
+ /**
73
+ * Queues a generation or snapshot job on Studio (`POST /api/jobs`).
74
+ *
75
+ * Returns as soon as Studio accepts the job (`202`). Poll with {@link waitForJob} until it finishes.
76
+ * Authenticates with the organization CI API key via `x-api-key`.
77
+ *
78
+ * @example Snapshot job
79
+ * ```ts
80
+ * const job = await createJob({
81
+ * studioUrl: 'https://kubb.studio',
82
+ * token: process.env.KUBB_TOKEN!,
83
+ * type: 'snapshot',
84
+ * agentId: agent.id,
85
+ * name: '@kubb/demo',
86
+ * version: '1.0.0',
87
+ * })
88
+ * const finished = await waitForJob({ studioUrl, token, id: job.id })
89
+ * ```
90
+ */
91
+ export declare function createJob({ studioUrl, token, type, agentId, name, version, config }: {
92
+ studioUrl: string;
93
+ token: string;
94
+ type: 'generation' | 'snapshot';
95
+ agentId: string;
96
+ name?: string;
97
+ version?: string;
98
+ config?: Record<string, unknown>;
99
+ }): Promise<StudioJob>;
100
+ /**
101
+ * Polls `GET /api/jobs/{id}` until the job reaches `success` or `failed`.
102
+ *
103
+ * A `failed` job resolves normally. Check `job.status` and `job.error`. Throws only when the
104
+ * deadline passes before Studio finishes.
105
+ */
106
+ export declare function waitForJob({ studioUrl, token, id, timeoutMs }: {
107
+ studioUrl: string;
108
+ token: string;
109
+ id: string;
110
+ /**
111
+ * How long to keep polling before throwing, in milliseconds.
112
+ *
113
+ * @default 60000
114
+ */
115
+ timeoutMs?: number;
116
+ }): Promise<StudioJob>;
14
117
  //#endregion
15
118
  //#region src/StudioSession.d.ts
16
119
  type StudioSessionOptions = {
@@ -377,5 +480,5 @@ type PollOptions = {
377
480
  */
378
481
  export declare function pollForPairingToken({ studioUrl, session, signal }: PollOptions): Promise<PairingResult>;
379
482
  //#endregion
380
- export type { Client, ClientOptions, ConnectionOptions, StudioConnectedContext };
483
+ export { type Client, type ClientOptions, type ConnectionOptions, type StudioConnectedContext, type StudioJob, type StudioJobStatus, type StudioSnapshot, createJobId };
381
484
  //# 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, isStudioReadyMessage } 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,62 @@ 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
  }
567
+ /**
568
+ * Queues a generation or snapshot job on Studio (`POST /api/jobs`).
569
+ *
570
+ * Returns as soon as Studio accepts the job (`202`). Poll with {@link waitForJob} until it finishes.
571
+ * Authenticates with the organization CI API key via `x-api-key`.
572
+ *
573
+ * @example Snapshot job
574
+ * ```ts
575
+ * const job = await createJob({
576
+ * studioUrl: 'https://kubb.studio',
577
+ * token: process.env.KUBB_TOKEN!,
578
+ * type: 'snapshot',
579
+ * agentId: agent.id,
580
+ * name: '@kubb/demo',
581
+ * version: '1.0.0',
582
+ * })
583
+ * const finished = await waitForJob({ studioUrl, token, id: job.id })
584
+ * ```
585
+ */
586
+ async function createJob({ studioUrl, token, type, agentId, name, version, config }) {
587
+ const { job } = await ofetch(`${studioUrl}/api/jobs`, {
588
+ method: "POST",
589
+ headers: { "x-api-key": token },
590
+ body: {
591
+ type,
592
+ agentId,
593
+ name,
594
+ version,
595
+ config
596
+ }
597
+ });
598
+ return job;
599
+ }
600
+ /**
601
+ * Polls `GET /api/jobs/{id}` until the job reaches `success` or `failed`.
602
+ *
603
+ * A `failed` job resolves normally. Check `job.status` and `job.error`. Throws only when the
604
+ * deadline passes before Studio finishes.
605
+ */
606
+ async function waitForJob({ studioUrl, token, id, timeoutMs = 6e4 }) {
607
+ const deadline = Date.now() + timeoutMs;
608
+ for (;;) {
609
+ const { job } = await ofetch(`${studioUrl}/api/jobs/${id}`, { headers: { "x-api-key": token } });
610
+ if (job.status === "success" || job.status === "failed") return job;
611
+ if (Date.now() >= deadline) throw new Error("Timed out waiting for the Studio job");
612
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
613
+ }
614
+ }
565
615
  //#endregion
566
616
  //#region package.json
567
- var version = "5.2.5";
617
+ var version = "5.2.7";
568
618
  //#endregion
569
619
  //#region src/hooks.ts
570
620
  /**
@@ -1517,6 +1567,9 @@ const CONNECT_TIMEOUT_MS = 5e3;
1517
1567
  */
1518
1568
  const eventSeqCounters = /* @__PURE__ */ new WeakMap();
1519
1569
  const require = createRequire(import.meta.url);
1570
+ function relativeStoragePath(root, filePath) {
1571
+ return (isAbsolute(filePath) ? relative(resolve(root), filePath) : filePath).replaceAll("\\", "/");
1572
+ }
1520
1573
  async function resolvePeerDependencies(names) {
1521
1574
  const uniqueNames = [...new Set(names.map(toPackageName))];
1522
1575
  const peerDependencies = {};
@@ -1571,9 +1624,10 @@ function sendAgentMessage(ws, message) {
1571
1624
  * Sends a single `kubb:error` payload to Studio, stamped from the same per-socket counter the event stream
1572
1625
  * uses so Studio can still order it against the generation events around it.
1573
1626
  */
1574
- function sendErrorMessage(ws, error) {
1627
+ function sendErrorMessage(ws, error, jobId) {
1575
1628
  sendAgentMessage(ws, {
1576
1629
  type: "agent:data",
1630
+ jobId,
1577
1631
  payload: {
1578
1632
  type: "kubb:error",
1579
1633
  data: [{
@@ -1588,7 +1642,7 @@ function sendErrorMessage(ws, error) {
1588
1642
  /**
1589
1643
  * Forwards selected Kubb lifecycle events to Studio as data messages for the active session.
1590
1644
  */
1591
- function setupEventsStream(ws, hooks) {
1645
+ function setupEventsStream(ws, hooks, jobId) {
1592
1646
  const unhooks = [];
1593
1647
  /**
1594
1648
  * Registers a listener and keeps its remover, so one generation's listeners come off the session
@@ -1600,6 +1654,7 @@ function setupEventsStream(ws, hooks) {
1600
1654
  function sendDataMessage(payload) {
1601
1655
  sendAgentMessage(ws, {
1602
1656
  type: "agent:data",
1657
+ jobId,
1603
1658
  payload: {
1604
1659
  ...payload,
1605
1660
  timestamp: Date.now(),
@@ -1698,7 +1753,7 @@ function setupEventsStream(ws, hooks) {
1698
1753
  limit: FILE_READ_CONCURRENCY,
1699
1754
  run: async (path) => {
1700
1755
  const content = await storage.readItem(path);
1701
- if (content !== null) files[path] = content;
1756
+ if (content !== null) files[relativeStoragePath(config.root, path)] = content;
1702
1757
  }
1703
1758
  });
1704
1759
  sendDataMessage({
@@ -1859,6 +1914,7 @@ var StudioSession = class {
1859
1914
  #session;
1860
1915
  #ws;
1861
1916
  #studioVersion;
1917
+ #activeJobId = null;
1862
1918
  #disposed = false;
1863
1919
  #isGenerating = false;
1864
1920
  #heartbeatTimer;
@@ -1906,7 +1962,7 @@ var StudioSession = class {
1906
1962
  signal?.addEventListener("abort", this.#onAbort, { once: true });
1907
1963
  this.#unhooks.push(() => signal?.removeEventListener("abort", this.#onAbort));
1908
1964
  this.#heartbeatTimer = setInterval(() => this.#sendHeartbeat(), heartbeatInterval);
1909
- this.#unhooks.push(this.#hooks.hook("kubb:error", ({ error }) => sendErrorMessage(ws, error)));
1965
+ this.#unhooks.push(this.#hooks.hook("kubb:error", ({ error }) => sendErrorMessage(ws, error, this.#activeJobId ?? "connection")));
1910
1966
  } catch (error) {
1911
1967
  await this.#hooks.callHook("studio:error", { error: toError(error) });
1912
1968
  if (error instanceof InvalidAgentTokenError) throw error;
@@ -2127,6 +2183,7 @@ var StudioSession = class {
2127
2183
  return;
2128
2184
  }
2129
2185
  this.#isGenerating = true;
2186
+ this.#activeJobId = data.jobId;
2130
2187
  try {
2131
2188
  const config = await loadConfig();
2132
2189
  const patch = data.payload;
@@ -2139,7 +2196,7 @@ var StudioSession = class {
2139
2196
  await this.#warn(`Ignored the spec from Studio; set ${remedy} to generate from it`);
2140
2197
  }
2141
2198
  const resolvedPlugins = plugins ?? config.plugins;
2142
- const detach = [setupHookListener(this.#hooks, root), setupEventsStream(ws, this.#hooks)];
2199
+ const detach = [setupHookListener(this.#hooks, root), setupEventsStream(ws, this.#hooks, data.jobId)];
2143
2200
  try {
2144
2201
  await generate({
2145
2202
  config: {
@@ -2167,6 +2224,7 @@ var StudioSession = class {
2167
2224
  });
2168
2225
  } finally {
2169
2226
  this.#isGenerating = false;
2227
+ this.#activeJobId = null;
2170
2228
  }
2171
2229
  }
2172
2230
  async #handleSave(ws, data, command) {
@@ -2175,6 +2233,7 @@ var StudioSession = class {
2175
2233
  await this.#warn("Ignored save: the message carried no edits");
2176
2234
  sendAgentMessage(ws, {
2177
2235
  type: "agent:save",
2236
+ jobId: data.jobId,
2178
2237
  payload: {
2179
2238
  outcomes: [],
2180
2239
  changed: false
@@ -2185,6 +2244,7 @@ var StudioSession = class {
2185
2244
  const edits = data.edits;
2186
2245
  const refuse = (reason) => sendAgentMessage(ws, {
2187
2246
  type: "agent:save",
2247
+ jobId: data.jobId,
2188
2248
  payload: {
2189
2249
  outcomes: edits.map((edit) => ({
2190
2250
  edit,
@@ -2208,6 +2268,7 @@ var StudioSession = class {
2208
2268
  if (changed) await writeFile(configFile, patched, "utf-8");
2209
2269
  sendAgentMessage(ws, {
2210
2270
  type: "agent:save",
2271
+ jobId: data.jobId,
2211
2272
  payload: {
2212
2273
  outcomes,
2213
2274
  changed,
@@ -2429,6 +2490,6 @@ async function pollForPairingToken({ studioUrl = agentDefaults.studioUrl, sessio
2429
2490
  throw new Error("The pairing code expired, pair again");
2430
2491
  }
2431
2492
  //#endregion
2432
- export { InvalidAgentTokenError, PairingCanceledError, createClient, createFileStorage, defaultStudioUrl, pollForPairingToken, runConnection, setStorage, startPairing };
2493
+ export { InvalidAgentTokenError, PairingCanceledError, createClient, createFileStorage, createJob, createJobId, defaultStudioUrl, pollForPairingToken, runConnection, setStorage, startPairing, waitForJob };
2433
2494
 
2434
2495
  //# sourceMappingURL=index.js.map