@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 +32 -0
- package/dist/index.cjs +70 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +105 -2
- package/dist/index.js +70 -9
- package/dist/index.js.map +1 -1
- package/dist/protocol.cjs +4 -0
- package/dist/protocol.cjs.map +1 -1
- package/dist/protocol.d.ts +584 -2
- package/dist/protocol.js +4 -1
- package/dist/protocol.js.map +1 -1
- package/package.json +4 -4
- package/dist/index-Dq2uq38g.d.ts +0 -579
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.
|
|
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
|