@kubb/studio 0.0.0-canary-20260903193839
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/LICENSE +21 -0
- package/README.md +136 -0
- package/dist/configFile-DjzP1_Ln.cjs +575 -0
- package/dist/configFile-DjzP1_Ln.cjs.map +1 -0
- package/dist/configFile-ZnV5tPon.js +574 -0
- package/dist/configFile-ZnV5tPon.js.map +1 -0
- package/dist/index-BVn89Nw2.d.ts +562 -0
- package/dist/index.cjs +1330 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +274 -0
- package/dist/index.js +1319 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol.cjs +42 -0
- package/dist/protocol.cjs.map +1 -0
- package/dist/protocol.d.ts +2 -0
- package/dist/protocol.js +37 -0
- package/dist/protocol.js.map +1 -0
- package/dist/resolveConfig-B9oGiNMi.js +179 -0
- package/dist/resolveConfig-B9oGiNMi.js.map +1 -0
- package/dist/resolveConfig-Ci-BVhN_.cjs +202 -0
- package/dist/resolveConfig-Ci-BVhN_.cjs.map +1 -0
- package/dist/rolldown-runtime-CRm0XQPb.js +9 -0
- package/dist/rolldown-runtime-qbf5tadS.cjs +38 -0
- package/package.json +86 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1319 @@
|
|
|
1
|
+
import { t as __name } from "./rolldown-runtime-CRm0XQPb.js";
|
|
2
|
+
import { n as mergeAdapter, r as mergePlugins } from "./resolveConfig-B9oGiNMi.js";
|
|
3
|
+
import { isCommandMessage, isDisconnectMessage, isStudioPingMessage } from "./protocol.js";
|
|
4
|
+
import { styleText } from "node:util";
|
|
5
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
8
|
+
import { FetchError, ofetch } from "ofetch";
|
|
9
|
+
import { hash, randomBytes } from "node:crypto";
|
|
10
|
+
import process$1 from "node:process";
|
|
11
|
+
import { createStorage } from "unstorage";
|
|
12
|
+
import fsDriver from "unstorage/drivers/fs";
|
|
13
|
+
import { Diagnostics, Hookable, createKubb, fsStorage, memoryStorage } from "@kubb/core";
|
|
14
|
+
import { x } from "tinyexec";
|
|
15
|
+
import WebSocket from "ws";
|
|
16
|
+
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
17
|
+
//#region src/constants.ts
|
|
18
|
+
/**
|
|
19
|
+
* Hosted Kubb Studio URL. Exported so credential stores can bind tokens to the resolved instance,
|
|
20
|
+
* not whatever default the client would pick on its own.
|
|
21
|
+
*/
|
|
22
|
+
const defaultStudioUrl = "https://kubb.studio";
|
|
23
|
+
/**
|
|
24
|
+
* Defaults the Studio client uses when a host passes nothing.
|
|
25
|
+
* Config path is left out on purpose: each host discovers that itself.
|
|
26
|
+
*/
|
|
27
|
+
const agentDefaults = {
|
|
28
|
+
studioUrl: defaultStudioUrl,
|
|
29
|
+
retryIntervalMs: 3e4,
|
|
30
|
+
/**
|
|
31
|
+
* Maximum heartbeat interval. Studio drops agents from the active list after ~90s without a ping,
|
|
32
|
+
* so a slower override would make a healthy agent look dead.
|
|
33
|
+
*/
|
|
34
|
+
heartbeatIntervalMs: 3e4,
|
|
35
|
+
poolSize: 1
|
|
36
|
+
};
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region ../../internals/utils/src/errors.ts
|
|
39
|
+
/**
|
|
40
|
+
* Coerces an unknown thrown value to an `Error` instance.
|
|
41
|
+
* Returns the value as-is when it is already an `Error`; otherwise wraps it with `String(value)`.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```ts
|
|
45
|
+
* try { ... } catch(err) {
|
|
46
|
+
* throw new Error('Build failed', { cause: toError(err) })
|
|
47
|
+
* }
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
function toError(value) {
|
|
51
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Extracts a human-readable message from any thrown value.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```ts
|
|
58
|
+
* getErrorMessage(new Error('oops')) // 'oops'
|
|
59
|
+
* getErrorMessage('plain string') // 'plain string'
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
function getErrorMessage(value) {
|
|
63
|
+
return value instanceof Error ? value.message : String(value);
|
|
64
|
+
}
|
|
65
|
+
//#endregion
|
|
66
|
+
//#region ../../internals/utils/src/promise.ts
|
|
67
|
+
/**
|
|
68
|
+
* Wraps `factory` with a keyed cache backed by the provided store.
|
|
69
|
+
*
|
|
70
|
+
* Pass a `WeakMap` for object keys (results are GC-eligible when the key is
|
|
71
|
+
* collected) or a `Map` for primitive keys. For multi-argument functions,
|
|
72
|
+
* nest two `memoize` calls — the outer keyed by the first argument, the
|
|
73
|
+
* inner (created once per outer miss) keyed by the second.
|
|
74
|
+
*
|
|
75
|
+
* Because the cache is owned by the caller, it can be shared, inspected, or
|
|
76
|
+
* cleared independently of the memoized function.
|
|
77
|
+
*
|
|
78
|
+
* @example Single WeakMap key
|
|
79
|
+
* ```ts
|
|
80
|
+
* const cache = new WeakMap<SchemaNode, Set<string>>()
|
|
81
|
+
* const getRefs = memoize(cache, (node) => collectRefs(node))
|
|
82
|
+
* ```
|
|
83
|
+
*
|
|
84
|
+
* @example Single Map key (primitive)
|
|
85
|
+
* ```ts
|
|
86
|
+
* const cache = new Map<string, Resolver>()
|
|
87
|
+
* const getResolver = memoize(cache, (name) => buildResolver(name))
|
|
88
|
+
* ```
|
|
89
|
+
*
|
|
90
|
+
* @example Two-level (object + primitive)
|
|
91
|
+
* ```ts
|
|
92
|
+
* const outer = new WeakMap<Params[], Map<string, Params[]>>()
|
|
93
|
+
* const fn = memoize(outer, (params) => memoize(new Map(), (key) => transform(params, key)))
|
|
94
|
+
* fn(params)('camelcase')
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
function memoize(store, factory) {
|
|
98
|
+
return (key) => {
|
|
99
|
+
if (store.has(key)) return store.get(key);
|
|
100
|
+
const value = factory(key);
|
|
101
|
+
store.set(key, value);
|
|
102
|
+
return value;
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Runs `run` over every item with at most `limit` in flight. Workers share one iterator, so each
|
|
107
|
+
* takes the next item the moment it frees up instead of waiting for a batch to drain.
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* ```ts
|
|
111
|
+
* await inParallel({ items: files, limit: 50, run: (file) => storage.writeItem(file.path, file.source) })
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
async function inParallel({ items, limit, run }) {
|
|
115
|
+
const queue = items.entries();
|
|
116
|
+
const worker = async () => {
|
|
117
|
+
for (const [index, item] of queue) await run(item, index);
|
|
118
|
+
};
|
|
119
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => worker()));
|
|
120
|
+
}
|
|
121
|
+
//#endregion
|
|
122
|
+
//#region ../../internals/utils/src/tools.ts
|
|
123
|
+
/**
|
|
124
|
+
* CLI command descriptors for each supported code formatter.
|
|
125
|
+
*/
|
|
126
|
+
const formatters = {
|
|
127
|
+
prettier: {
|
|
128
|
+
command: "prettier",
|
|
129
|
+
args: (outputPath) => [
|
|
130
|
+
"--ignore-unknown",
|
|
131
|
+
"--write",
|
|
132
|
+
outputPath
|
|
133
|
+
],
|
|
134
|
+
errorMessage: "Prettier not found"
|
|
135
|
+
},
|
|
136
|
+
biome: {
|
|
137
|
+
command: "biome",
|
|
138
|
+
args: (outputPath) => [
|
|
139
|
+
"format",
|
|
140
|
+
"--write",
|
|
141
|
+
outputPath
|
|
142
|
+
],
|
|
143
|
+
errorMessage: "Biome not found"
|
|
144
|
+
},
|
|
145
|
+
oxfmt: {
|
|
146
|
+
command: "oxfmt",
|
|
147
|
+
args: (outputPath) => [outputPath],
|
|
148
|
+
errorMessage: "Oxfmt not found"
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
/**
|
|
152
|
+
* CLI command descriptors for each supported linter.
|
|
153
|
+
*/
|
|
154
|
+
const linters = {
|
|
155
|
+
eslint: {
|
|
156
|
+
command: "eslint",
|
|
157
|
+
args: (outputPath) => [outputPath, "--fix"],
|
|
158
|
+
errorMessage: "Eslint not found"
|
|
159
|
+
},
|
|
160
|
+
biome: {
|
|
161
|
+
command: "biome",
|
|
162
|
+
args: (outputPath) => [
|
|
163
|
+
"lint",
|
|
164
|
+
"--fix",
|
|
165
|
+
outputPath
|
|
166
|
+
],
|
|
167
|
+
errorMessage: "Biome not found"
|
|
168
|
+
},
|
|
169
|
+
oxlint: {
|
|
170
|
+
command: "oxlint",
|
|
171
|
+
args: (outputPath) => [
|
|
172
|
+
"--fix",
|
|
173
|
+
"--no-ignore",
|
|
174
|
+
outputPath
|
|
175
|
+
],
|
|
176
|
+
errorMessage: "Oxlint not found"
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
/**
|
|
180
|
+
* Preference order for `format: 'auto'`, most-preferred first. Spelled out rather than taken from
|
|
181
|
+
* the table's key order, which is arbitrary and would silently change what `auto` picks.
|
|
182
|
+
*/
|
|
183
|
+
const FORMATTER_PREFERENCE = [
|
|
184
|
+
"oxfmt",
|
|
185
|
+
"biome",
|
|
186
|
+
"prettier"
|
|
187
|
+
];
|
|
188
|
+
/**
|
|
189
|
+
* Preference order for `lint: 'auto'`, most-preferred first.
|
|
190
|
+
*/
|
|
191
|
+
const LINTER_PREFERENCE = [
|
|
192
|
+
"oxlint",
|
|
193
|
+
"biome",
|
|
194
|
+
"eslint"
|
|
195
|
+
];
|
|
196
|
+
/**
|
|
197
|
+
* Whether `name` is on PATH and answers `--version` with a zero exit.
|
|
198
|
+
*/
|
|
199
|
+
function isToolAvailable(name) {
|
|
200
|
+
return new Promise((resolve) => {
|
|
201
|
+
const child = spawn(name, ["--version"], { stdio: "ignore" });
|
|
202
|
+
child.on("close", (code) => resolve(code === 0));
|
|
203
|
+
child.on("error", () => resolve(false));
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Returns the first installed executable from `candidates`, or `null` when none are found.
|
|
208
|
+
*
|
|
209
|
+
* Not memoized: a long-running host that probes repeatedly should cache the result itself, and a
|
|
210
|
+
* `--watch` build should keep noticing a tool installed mid-session.
|
|
211
|
+
*/
|
|
212
|
+
async function detectTool$1(candidates) {
|
|
213
|
+
for (const candidate of candidates) if (await isToolAvailable(candidate)) return candidate;
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
__name(detectTool$1, "detectTool");
|
|
217
|
+
/**
|
|
218
|
+
* Tokenizes a shell command string, respecting single and double quotes.
|
|
219
|
+
*
|
|
220
|
+
* @example
|
|
221
|
+
* ```ts
|
|
222
|
+
* tokenize('git commit -m "initial commit"')
|
|
223
|
+
* // → ['git', 'commit', '-m', 'initial commit']
|
|
224
|
+
* ```
|
|
225
|
+
*/
|
|
226
|
+
function tokenize(command) {
|
|
227
|
+
return (command.match(/[^\s"']+|"([^"]*)"|'([^']*)'/g) ?? []).map((token) => token.replace(/^["']|["']$/g, ""));
|
|
228
|
+
}
|
|
229
|
+
//#endregion
|
|
230
|
+
//#region ../../internals/utils/src/time.ts
|
|
231
|
+
/**
|
|
232
|
+
* Calculates elapsed time in milliseconds from a high-resolution `process.hrtime` start time.
|
|
233
|
+
* Rounds to 2 decimal places for sub-millisecond precision without noise.
|
|
234
|
+
*
|
|
235
|
+
* @example
|
|
236
|
+
* ```ts
|
|
237
|
+
* const start = process.hrtime()
|
|
238
|
+
* doWork()
|
|
239
|
+
* getElapsedMs(start) // 42.35
|
|
240
|
+
* ```
|
|
241
|
+
*/
|
|
242
|
+
function getElapsedMs(hrStart) {
|
|
243
|
+
const [seconds, nanoseconds] = process.hrtime(hrStart);
|
|
244
|
+
const ms = seconds * 1e3 + nanoseconds / 1e6;
|
|
245
|
+
return Math.round(ms * 100) / 100;
|
|
246
|
+
}
|
|
247
|
+
//#endregion
|
|
248
|
+
//#region src/machine.ts
|
|
249
|
+
/**
|
|
250
|
+
* Key-value storage the runtime uses for its machine secret and the last Studio config.
|
|
251
|
+
*
|
|
252
|
+
* One storage per process, since one process serves one config file. Hosts install their own
|
|
253
|
+
* driver on startup: Nitro passes its `kubb` mount, the CLI an fs driver under `~/.kubb/cache`.
|
|
254
|
+
* The in-memory default keeps the runtime usable without a host, at the cost of a machine
|
|
255
|
+
* identity that changes on every restart.
|
|
256
|
+
*/
|
|
257
|
+
let storage = createStorage();
|
|
258
|
+
let hasInstalledStorage = false;
|
|
259
|
+
/**
|
|
260
|
+
* Installs the storage driver the runtime persists to. Call once, before connecting.
|
|
261
|
+
*/
|
|
262
|
+
function setStorage(next) {
|
|
263
|
+
storage = next;
|
|
264
|
+
hasInstalledStorage = true;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* A storage backed by files under `base`, so the machine secret and the last Studio config
|
|
268
|
+
* survive a restart. Repeated pairings of one machine depend on that secret staying put.
|
|
269
|
+
*/
|
|
270
|
+
function createFileStorage(base) {
|
|
271
|
+
return createStorage({ driver: fsDriver({ base }) });
|
|
272
|
+
}
|
|
273
|
+
let fallbackSecretPromise = null;
|
|
274
|
+
/**
|
|
275
|
+
* Loads the fallback machine secret from the runtime storage.
|
|
276
|
+
* On first use it generates a secret and persists it, so the machine identity stays
|
|
277
|
+
* stable across restarts. An identity that changes on every boot breaks session
|
|
278
|
+
* creation with Studio whenever the startup registration call fails.
|
|
279
|
+
*/
|
|
280
|
+
async function loadOrCreateFallbackSecret() {
|
|
281
|
+
if (!hasInstalledStorage) console.warn(styleText("yellow", "Deriving the machine token before a storage driver was installed"), "call setStorage() first, or set KUBB_AGENT_SECRET, to keep a stable machine identity across restarts");
|
|
282
|
+
const stored = await storage.getItem("machine-secret").catch(() => null);
|
|
283
|
+
if (typeof stored === "string" && stored) return stored;
|
|
284
|
+
const secret = randomBytes(32).toString("hex");
|
|
285
|
+
await storage.setItem("machine-secret", secret).catch(() => {
|
|
286
|
+
console.warn(styleText("yellow", "Could not persist the generated machine secret"), "set KUBB_AGENT_SECRET to keep a stable machine identity across restarts");
|
|
287
|
+
});
|
|
288
|
+
return secret;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Returns the machine token derived from the `KUBB_AGENT_SECRET` environment variable.
|
|
292
|
+
* Falls back to a generated secret persisted in the runtime storage if the env var is not set.
|
|
293
|
+
* The token is hashed with SHA-256.
|
|
294
|
+
*/
|
|
295
|
+
async function getMachineToken() {
|
|
296
|
+
if (process$1.env.KUBB_AGENT_SECRET) return hash("sha256", process$1.env.KUBB_AGENT_SECRET);
|
|
297
|
+
fallbackSecretPromise ??= loadOrCreateFallbackSecret();
|
|
298
|
+
return hash("sha256", await fallbackSecretPromise);
|
|
299
|
+
}
|
|
300
|
+
//#endregion
|
|
301
|
+
//#region src/api.ts
|
|
302
|
+
/**
|
|
303
|
+
* Reads a human-readable message from a Studio JSON error body, when it has one. `FetchError`'s own
|
|
304
|
+
* message stops at the status line, so the detail Studio sends with a failure (an agent limit, a
|
|
305
|
+
* revoked token) would otherwise never reach the user.
|
|
306
|
+
*/
|
|
307
|
+
function responseMessage(data) {
|
|
308
|
+
if (!data || typeof data !== "object") return;
|
|
309
|
+
const body = data;
|
|
310
|
+
for (const value of [
|
|
311
|
+
body.error_description,
|
|
312
|
+
body.message,
|
|
313
|
+
body.error
|
|
314
|
+
]) if (typeof value === "string" && value) return value;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Retries after the first registration attempt, each backing off twice as far as the last.
|
|
318
|
+
*/
|
|
319
|
+
const REGISTER_RETRIES = 3;
|
|
320
|
+
/**
|
|
321
|
+
* Shared in-flight registration so concurrent pool sessions trigger one purge, not N.
|
|
322
|
+
*/
|
|
323
|
+
let registrationInFlight = null;
|
|
324
|
+
/**
|
|
325
|
+
* Thrown when Studio rejects the agent token itself (401). Retrying cannot help: the token was
|
|
326
|
+
* revoked, or the agent it belonged to was deleted in the Studio UI. Hosts catch this to forget
|
|
327
|
+
* the stored credential and pair again.
|
|
328
|
+
*/
|
|
329
|
+
var InvalidAgentTokenError = class extends Error {
|
|
330
|
+
constructor(studioUrl, options) {
|
|
331
|
+
super(`Kubb Studio rejected this agent's token. It was revoked or the agent was deleted in ${studioUrl}.`, options);
|
|
332
|
+
this.name = "InvalidAgentTokenError";
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
/**
|
|
336
|
+
* Whether a thrown value carries `statusCode`. Not narrowed to `FetchError`: a host wrapper can
|
|
337
|
+
* throw its own error shape with the same field.
|
|
338
|
+
*
|
|
339
|
+
* A 401 means the agent token itself was rejected. A 403 from the session create endpoint means
|
|
340
|
+
* the machine token stored in Studio no longer matches this agent (missing or mismatched).
|
|
341
|
+
*/
|
|
342
|
+
function rejectedWith(error, statusCode) {
|
|
343
|
+
return error?.statusCode === statusCode;
|
|
344
|
+
}
|
|
345
|
+
function sessionError(cause) {
|
|
346
|
+
const detail = (cause instanceof FetchError ? responseMessage(cause.data) : void 0) ?? getErrorMessage(cause);
|
|
347
|
+
return new Error(detail ? `Failed to get agent session from Kubb Studio: ${detail}` : "Failed to get agent session from Kubb Studio", { cause });
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Performs the raw session create request against Studio.
|
|
351
|
+
*/
|
|
352
|
+
async function requestAgentSession({ token, studioUrl }) {
|
|
353
|
+
const url = `${studioUrl}/api/agent/sessions`;
|
|
354
|
+
const data = await ofetch(url, {
|
|
355
|
+
method: "POST",
|
|
356
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
357
|
+
body: { machineToken: await getMachineToken() }
|
|
358
|
+
});
|
|
359
|
+
if (!data) throw new Error("No data available for agent session");
|
|
360
|
+
return data;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Obtain an agent session token from Kubb Studio via HTTP.
|
|
364
|
+
*
|
|
365
|
+
* When Studio rejects the machine token (403), for example after the agent restarted
|
|
366
|
+
* with a new identity while the startup registration call failed, the agent re-registers
|
|
367
|
+
* and retries once, so a single failed registration can't permanently block session creation.
|
|
368
|
+
*/
|
|
369
|
+
async function createAgentSession({ token, studioUrl }) {
|
|
370
|
+
try {
|
|
371
|
+
return await requestAgentSession({
|
|
372
|
+
token,
|
|
373
|
+
studioUrl
|
|
374
|
+
});
|
|
375
|
+
} catch (error) {
|
|
376
|
+
if (rejectedWith(error, 401)) throw new InvalidAgentTokenError(studioUrl, { cause: error });
|
|
377
|
+
if (!rejectedWith(error, 403) || !await registerAgent({
|
|
378
|
+
token,
|
|
379
|
+
studioUrl
|
|
380
|
+
})) throw sessionError(error);
|
|
381
|
+
try {
|
|
382
|
+
return await requestAgentSession({
|
|
383
|
+
token,
|
|
384
|
+
studioUrl
|
|
385
|
+
});
|
|
386
|
+
} catch (retryError) {
|
|
387
|
+
if (rejectedWith(retryError, 401)) throw new InvalidAgentTokenError(studioUrl, { cause: retryError });
|
|
388
|
+
throw sessionError(retryError);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Register this agent with Kubb Studio by sending the machine ID.
|
|
394
|
+
* Called on agent startup before creating a WebSocket session, and again when
|
|
395
|
+
* Studio rejects the machine token during session creation.
|
|
396
|
+
*
|
|
397
|
+
* Retries with backoff because a failed registration leaves Studio with a stale
|
|
398
|
+
* machine token that blocks every subsequent session create call. Registration
|
|
399
|
+
* purges all of the agent's sessions on the Studio side, so concurrent callers
|
|
400
|
+
* (multiple pool sessions hitting a 403 at once) share one in-flight run instead
|
|
401
|
+
* of purging each other's fresh sessions.
|
|
402
|
+
*/
|
|
403
|
+
function registerAgent(props) {
|
|
404
|
+
registrationInFlight ??= runRegistration(props).finally(() => {
|
|
405
|
+
registrationInFlight = null;
|
|
406
|
+
});
|
|
407
|
+
return registrationInFlight;
|
|
408
|
+
}
|
|
409
|
+
async function runRegistration({ token, studioUrl, poolSize }) {
|
|
410
|
+
const machineToken = await getMachineToken();
|
|
411
|
+
try {
|
|
412
|
+
await ofetch(`${studioUrl}/api/agent/connect`, {
|
|
413
|
+
method: "POST",
|
|
414
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
415
|
+
body: {
|
|
416
|
+
machineToken,
|
|
417
|
+
poolSize
|
|
418
|
+
},
|
|
419
|
+
retry: REGISTER_RETRIES,
|
|
420
|
+
retryDelay: ({ options }) => 2e3 * 2 ** (REGISTER_RETRIES - Number(options.retry))
|
|
421
|
+
});
|
|
422
|
+
return true;
|
|
423
|
+
} catch (error) {
|
|
424
|
+
if (rejectedWith(error, 401)) throw new InvalidAgentTokenError(studioUrl, { cause: error });
|
|
425
|
+
console.error(styleText("red", `Failed to register agent with Studio after 4 attempts`));
|
|
426
|
+
return false;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Notify Kubb Studio that this agent is disconnecting.
|
|
431
|
+
* Called on process termination or server close. A failed notify is logged and swallowed: the
|
|
432
|
+
* local socket is already gone, and failing teardown must not block shutdown or reconnect.
|
|
433
|
+
*/
|
|
434
|
+
async function disconnect({ sessionId, token, studioUrl, slug }) {
|
|
435
|
+
const url = `${studioUrl}/api/agent/sessions/${sessionId}/disconnect`;
|
|
436
|
+
const tag = slug ?? "agent";
|
|
437
|
+
try {
|
|
438
|
+
await ofetch(url, {
|
|
439
|
+
method: "POST",
|
|
440
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
441
|
+
});
|
|
442
|
+
console.log(styleText("green", `[${tag}] Disconnected from Studio`));
|
|
443
|
+
} catch (error) {
|
|
444
|
+
console.warn(styleText("yellow", `[${tag}] Failed to notify Studio of disconnection: ${getErrorMessage(error)}`));
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
//#endregion
|
|
448
|
+
//#region package.json
|
|
449
|
+
var version = "5.1.0";
|
|
450
|
+
//#endregion
|
|
451
|
+
//#region src/hooks.ts
|
|
452
|
+
/**
|
|
453
|
+
* Register a `kubb:hook:start` listener that spawns the requested command via tinyexec,
|
|
454
|
+
* streams each stdout line as a `kubb:hook:line` event, and calls `kubb:hook:end` with the result.
|
|
455
|
+
* Streaming the output lets Kubb Studio render live hook progress over the WebSocket connection.
|
|
456
|
+
*/
|
|
457
|
+
function setupHookListener(hooks, root) {
|
|
458
|
+
hooks.hook("kubb:hook:start", async (ctx) => {
|
|
459
|
+
const { id, command, args } = ctx;
|
|
460
|
+
if (!id) return;
|
|
461
|
+
const commandWithArgs = args?.length ? `${command} ${args.join(" ")}` : command;
|
|
462
|
+
try {
|
|
463
|
+
const proc = x(command, [...args ?? []], { nodeOptions: {
|
|
464
|
+
cwd: root,
|
|
465
|
+
detached: true
|
|
466
|
+
} });
|
|
467
|
+
for await (const line of proc) await hooks.callHook("kubb:hook:line", {
|
|
468
|
+
id,
|
|
469
|
+
line
|
|
470
|
+
});
|
|
471
|
+
const { exitCode } = await proc;
|
|
472
|
+
if (exitCode !== 0) {
|
|
473
|
+
const error = /* @__PURE__ */ new Error(`Hook execute failed: ${commandWithArgs}`);
|
|
474
|
+
await hooks.callHook("kubb:hook:end", {
|
|
475
|
+
id,
|
|
476
|
+
command,
|
|
477
|
+
args,
|
|
478
|
+
success: false,
|
|
479
|
+
error
|
|
480
|
+
});
|
|
481
|
+
await hooks.callHook("kubb:error", { error });
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
await hooks.callHook("kubb:hook:end", {
|
|
485
|
+
id,
|
|
486
|
+
command,
|
|
487
|
+
args,
|
|
488
|
+
success: true,
|
|
489
|
+
error: null
|
|
490
|
+
});
|
|
491
|
+
} catch (caughtError) {
|
|
492
|
+
const error = /* @__PURE__ */ new Error(`Hook execute failed: ${commandWithArgs}`);
|
|
493
|
+
error.cause = caughtError;
|
|
494
|
+
await hooks.callHook("kubb:hook:end", {
|
|
495
|
+
id,
|
|
496
|
+
command,
|
|
497
|
+
args,
|
|
498
|
+
success: false,
|
|
499
|
+
error
|
|
500
|
+
});
|
|
501
|
+
await hooks.callHook("kubb:error", { error });
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Waits for the `kubb:hook:end` matching `hookId`. Register this before calling `kubb:hook:start`:
|
|
507
|
+
* `callHook` awaits its listeners, and {@link setupHookListener} calls `kubb:hook:end` from inside
|
|
508
|
+
* that same listener, so a handler added afterward would already have missed it.
|
|
509
|
+
*/
|
|
510
|
+
function waitForHookEnd(hooks, hookId) {
|
|
511
|
+
return new Promise((resolve, reject) => {
|
|
512
|
+
const handleHookEnd = (ctx) => {
|
|
513
|
+
if (ctx.id !== hookId) return;
|
|
514
|
+
hooks.removeHook("kubb:hook:end", handleHookEnd);
|
|
515
|
+
if (ctx.success) resolve();
|
|
516
|
+
else reject(ctx.error);
|
|
517
|
+
};
|
|
518
|
+
hooks.hook("kubb:hook:end", handleHookEnd);
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
//#endregion
|
|
522
|
+
//#region src/generate.ts
|
|
523
|
+
/**
|
|
524
|
+
* `isToolAvailable` spawns a process, and a long-lived connection generates repeatedly, so each
|
|
525
|
+
* executable is probed once per process. The CLI deliberately does not memoize: a `--watch` build
|
|
526
|
+
* should keep noticing a tool installed mid-session.
|
|
527
|
+
*/
|
|
528
|
+
const detectTool = memoize(/* @__PURE__ */ new Map(), detectTool$1);
|
|
529
|
+
/**
|
|
530
|
+
* The two post-build tool steps. Formatting and linting differ only in which tools they look for,
|
|
531
|
+
* so they run through one loop rather than two near-identical blocks.
|
|
532
|
+
*
|
|
533
|
+
* `noun` and `verbing` are spelled out instead of built from `kind`. Concatenating `` `${kind}ter` ``
|
|
534
|
+
* and `` `${kind}ting` `` works for `format`, but doubles the `t` in `lint`, giving "lintter" and
|
|
535
|
+
* "lintting" instead of "linter" and "linting".
|
|
536
|
+
*/
|
|
537
|
+
const TOOL_STEPS = [{
|
|
538
|
+
kind: "format",
|
|
539
|
+
noun: "formatter",
|
|
540
|
+
verbing: "Formatting",
|
|
541
|
+
tools: formatters,
|
|
542
|
+
detect: FORMATTER_PREFERENCE
|
|
543
|
+
}, {
|
|
544
|
+
kind: "lint",
|
|
545
|
+
noun: "linter",
|
|
546
|
+
verbing: "Linting",
|
|
547
|
+
tools: linters,
|
|
548
|
+
detect: LINTER_PREFERENCE
|
|
549
|
+
}];
|
|
550
|
+
/**
|
|
551
|
+
* Absolute path of the directory the formatter and linter are pointed at.
|
|
552
|
+
*/
|
|
553
|
+
function outputPath(config) {
|
|
554
|
+
return path.isAbsolute(config.output.path) ? config.output.path : path.resolve(process$1.cwd(), config.root, config.output.path);
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Emits `kubb:hook:start` and waits for the matching `kubb:hook:end`. The host spawns the process:
|
|
558
|
+
* this only describes what to run and when it finished.
|
|
559
|
+
*
|
|
560
|
+
* @throws whatever the command failed with, so callers can report it their own way.
|
|
561
|
+
*/
|
|
562
|
+
async function runHook({ hooks, id, command, args }) {
|
|
563
|
+
const hookId = hash("sha256", id);
|
|
564
|
+
const hookEnd = waitForHookEnd(hooks, hookId);
|
|
565
|
+
await hooks.callHook("kubb:hook:start", {
|
|
566
|
+
id: hookId,
|
|
567
|
+
command,
|
|
568
|
+
args: [...args]
|
|
569
|
+
});
|
|
570
|
+
await hookEnd;
|
|
571
|
+
}
|
|
572
|
+
function isProblemErrorDiagnostic(diagnostic) {
|
|
573
|
+
return (diagnostic.kind ?? "problem") === "problem" && diagnostic.severity === "error";
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Folds error-severity diagnostics into one thrown error so logs name the failing plugin.
|
|
577
|
+
*/
|
|
578
|
+
function formatGenerationFailure(diagnostics) {
|
|
579
|
+
const reasons = diagnostics.filter(isProblemErrorDiagnostic).map((diagnostic) => diagnostic.plugin ? `${diagnostic.plugin}: ${diagnostic.message}` : diagnostic.message);
|
|
580
|
+
if (!reasons.length) return /* @__PURE__ */ new Error("Generation failed");
|
|
581
|
+
return /* @__PURE__ */ new Error(`Generation failed: ${reasons.length} error${reasons.length === 1 ? "" : "s"}: ${reasons.join("; ")}`);
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Runs a full Kubb code-generation cycle for the given config.
|
|
585
|
+
*
|
|
586
|
+
* Emits lifecycle events on the provided `hooks` emitter so callers (e.g. the WebSocket stream)
|
|
587
|
+
* can forward progress to connected clients. After a successful build, auto-formatting and
|
|
588
|
+
* linting are applied when configured, followed by any user-defined `hooks.done` commands.
|
|
589
|
+
*/
|
|
590
|
+
async function generate({ config, hooks }) {
|
|
591
|
+
const hrStart = process$1.hrtime();
|
|
592
|
+
await hooks.callHook("kubb:generation:start", { config });
|
|
593
|
+
await hooks.callHook("kubb:info", { message: config.name ? `Setup generation ${config.name}` : "Setup generation" });
|
|
594
|
+
const kubb = createKubb(config, { hooks });
|
|
595
|
+
await kubb.setup();
|
|
596
|
+
await hooks.callHook("kubb:info", { message: config.name ? `Build generation ${config.name}` : "Build generation" });
|
|
597
|
+
const { files, diagnostics, storage } = await kubb.safeBuild();
|
|
598
|
+
await hooks.callHook("kubb:info", { message: "Load summary" });
|
|
599
|
+
for (const diagnostic of diagnostics.filter(isProblemErrorDiagnostic)) await hooks.callHook("kubb:error", { error: new Error(diagnostic.plugin ? `${diagnostic.plugin}: ${diagnostic.message}` : diagnostic.message) });
|
|
600
|
+
const status = Diagnostics.hasError(diagnostics) ? "failed" : "success";
|
|
601
|
+
await hooks.callHook("kubb:generation:end", {
|
|
602
|
+
config,
|
|
603
|
+
storage: {
|
|
604
|
+
...storage,
|
|
605
|
+
readKeys: async () => [...new Set(files.map((file) => file.path))]
|
|
606
|
+
},
|
|
607
|
+
diagnostics,
|
|
608
|
+
status,
|
|
609
|
+
hrStart,
|
|
610
|
+
filesCreated: files.length
|
|
611
|
+
});
|
|
612
|
+
if (status === "failed") throw formatGenerationFailure(diagnostics);
|
|
613
|
+
await hooks.callHook("kubb:success", { message: "Generation successfully" });
|
|
614
|
+
for (const step of TOOL_STEPS) {
|
|
615
|
+
const setting = config.output[step.kind];
|
|
616
|
+
if (!setting) continue;
|
|
617
|
+
await hooks.callHook(`kubb:${step.kind}:start`);
|
|
618
|
+
const tool = setting === "auto" ? await detectTool(step.detect) : setting;
|
|
619
|
+
if (!tool) await hooks.callHook("kubb:warn", { message: `No ${step.noun} found (${step.detect.join(", ")}). Skipping ${step.verbing.toLowerCase()}.` });
|
|
620
|
+
if (tool && setting === "auto") await hooks.callHook("kubb:info", { message: `Auto-detected ${step.noun}: ${styleText("dim", tool)}` });
|
|
621
|
+
const command = tool ? step.tools[tool] : void 0;
|
|
622
|
+
if (command) try {
|
|
623
|
+
await runHook({
|
|
624
|
+
hooks,
|
|
625
|
+
id: [config.name, tool].filter(Boolean).join("-"),
|
|
626
|
+
command: command.command,
|
|
627
|
+
args: command.args(outputPath(config))
|
|
628
|
+
});
|
|
629
|
+
await hooks.callHook("kubb:success", { message: `${step.verbing} with ${tool} successfully` });
|
|
630
|
+
} catch (caughtError) {
|
|
631
|
+
await hooks.callHook("kubb:error", { error: new Error(command.errorMessage, { cause: caughtError }) });
|
|
632
|
+
}
|
|
633
|
+
await hooks.callHook(`kubb:${step.kind}:end`);
|
|
634
|
+
}
|
|
635
|
+
if (config.output.postGenerate?.length) {
|
|
636
|
+
await hooks.callHook("kubb:hooks:start");
|
|
637
|
+
for (const entry of config.output.postGenerate) {
|
|
638
|
+
const line = typeof entry === "string" ? entry : entry.command;
|
|
639
|
+
const [cmd, ...args] = tokenize(line);
|
|
640
|
+
if (!cmd) continue;
|
|
641
|
+
await runHook({
|
|
642
|
+
hooks,
|
|
643
|
+
id: line,
|
|
644
|
+
command: cmd,
|
|
645
|
+
args
|
|
646
|
+
});
|
|
647
|
+
await hooks.callHook("kubb:success", { message: `${line} successfully executed` });
|
|
648
|
+
}
|
|
649
|
+
await hooks.callHook("kubb:hooks:end");
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
//#endregion
|
|
653
|
+
//#region src/ws.ts
|
|
654
|
+
/**
|
|
655
|
+
* How many generated files are read from storage at once when building the
|
|
656
|
+
* `kubb:generation:end` payload. A spec producing thousands of files would otherwise fire one
|
|
657
|
+
* `storage.readItem` per file simultaneously.
|
|
658
|
+
*/
|
|
659
|
+
const FILE_READ_CONCURRENCY = 50;
|
|
660
|
+
/**
|
|
661
|
+
* How long the initial handshake may take before the socket is closed and the reconnect loop
|
|
662
|
+
* takes over.
|
|
663
|
+
*/
|
|
664
|
+
const CONNECT_TIMEOUT_MS = 5e3;
|
|
665
|
+
/**
|
|
666
|
+
* Per-socket event counter. Every data message carries the next value so Studio can restore the
|
|
667
|
+
* agent's emission order even when the relay delivers frames out of order. Keyed by the socket so
|
|
668
|
+
* the count stays monotonic across every generation run on one connection, and is dropped
|
|
669
|
+
* automatically once the socket is collected.
|
|
670
|
+
*/
|
|
671
|
+
const eventSeqCounters = /* @__PURE__ */ new WeakMap();
|
|
672
|
+
function nextEventSeq(ws) {
|
|
673
|
+
const seq = eventSeqCounters.get(ws) ?? 0;
|
|
674
|
+
eventSeqCounters.set(ws, seq + 1);
|
|
675
|
+
return seq;
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Opens a Studio WebSocket connection and closes it when the initial handshake exceeds the configured timeout.
|
|
679
|
+
*/
|
|
680
|
+
function createWebsocket(url, options) {
|
|
681
|
+
const ws = new WebSocket(url, options);
|
|
682
|
+
const timer = setTimeout(() => {
|
|
683
|
+
if (ws.readyState === WebSocket.CONNECTING) ws.close(3008, "Connection timeout");
|
|
684
|
+
}, CONNECT_TIMEOUT_MS);
|
|
685
|
+
ws.once("open", () => clearTimeout(timer));
|
|
686
|
+
ws.once("close", () => clearTimeout(timer));
|
|
687
|
+
return ws;
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* Sends a serialized agent message when the Studio socket is ready to accept frames.
|
|
691
|
+
*/
|
|
692
|
+
function sendAgentMessage(ws, message) {
|
|
693
|
+
try {
|
|
694
|
+
if (ws.readyState !== WebSocket.OPEN) return;
|
|
695
|
+
ws.send(JSON.stringify(message));
|
|
696
|
+
} catch (error) {
|
|
697
|
+
throw new Error("Failed to send message to Kubb Studio", { cause: error });
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Sends a single `kubb:error` payload to Studio, stamped from the same per-socket counter the event stream
|
|
702
|
+
* uses so Studio can still order it against the generation events around it.
|
|
703
|
+
*/
|
|
704
|
+
function sendErrorMessage(ws, error) {
|
|
705
|
+
sendAgentMessage(ws, {
|
|
706
|
+
type: "agent:data",
|
|
707
|
+
payload: {
|
|
708
|
+
type: "kubb:error",
|
|
709
|
+
data: [{
|
|
710
|
+
message: error.message,
|
|
711
|
+
stack: error.stack
|
|
712
|
+
}],
|
|
713
|
+
timestamp: Date.now(),
|
|
714
|
+
seq: nextEventSeq(ws)
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Forwards selected Kubb lifecycle events to Studio as data messages for the active session.
|
|
720
|
+
*/
|
|
721
|
+
function setupEventsStream(ws, hooks) {
|
|
722
|
+
function sendDataMessage(payload) {
|
|
723
|
+
sendAgentMessage(ws, {
|
|
724
|
+
type: "agent:data",
|
|
725
|
+
payload: {
|
|
726
|
+
...payload,
|
|
727
|
+
timestamp: Date.now(),
|
|
728
|
+
seq: nextEventSeq(ws)
|
|
729
|
+
}
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
hooks.hook("kubb:plugin:start", (ctx) => {
|
|
733
|
+
sendDataMessage({
|
|
734
|
+
type: "kubb:plugin:start",
|
|
735
|
+
data: [{ plugin: ctx.plugin }]
|
|
736
|
+
});
|
|
737
|
+
});
|
|
738
|
+
hooks.hook("kubb:plugin:end", (ctx) => {
|
|
739
|
+
sendDataMessage({
|
|
740
|
+
type: "kubb:plugin:end",
|
|
741
|
+
data: [{
|
|
742
|
+
plugin: ctx.plugin,
|
|
743
|
+
duration: ctx.duration,
|
|
744
|
+
success: ctx.success
|
|
745
|
+
}]
|
|
746
|
+
});
|
|
747
|
+
});
|
|
748
|
+
hooks.hook("kubb:build:start", ({ config, adapter }) => {
|
|
749
|
+
sendDataMessage({
|
|
750
|
+
type: "kubb:build:start",
|
|
751
|
+
data: [{
|
|
752
|
+
config: { name: config.name },
|
|
753
|
+
adapter: { name: adapter.name }
|
|
754
|
+
}]
|
|
755
|
+
});
|
|
756
|
+
});
|
|
757
|
+
hooks.hook("kubb:build:end", ({ files, outputDir }) => {
|
|
758
|
+
sendDataMessage({
|
|
759
|
+
type: "kubb:build:end",
|
|
760
|
+
data: [{
|
|
761
|
+
files: files.map((file) => ({
|
|
762
|
+
path: file.path,
|
|
763
|
+
name: file.name
|
|
764
|
+
})),
|
|
765
|
+
outputDir
|
|
766
|
+
}]
|
|
767
|
+
});
|
|
768
|
+
});
|
|
769
|
+
hooks.hook("kubb:files:processing:start", ({ files }) => {
|
|
770
|
+
sendDataMessage({
|
|
771
|
+
type: "kubb:files:processing:start",
|
|
772
|
+
data: [{ total: files.length }]
|
|
773
|
+
});
|
|
774
|
+
});
|
|
775
|
+
hooks.hook("kubb:files:processing:update", ({ files }) => {
|
|
776
|
+
sendDataMessage({
|
|
777
|
+
type: "kubb:files:processing:update",
|
|
778
|
+
data: [{ files: files.map(({ file, processed, total, percentage }) => ({
|
|
779
|
+
file: file.path,
|
|
780
|
+
processed,
|
|
781
|
+
total,
|
|
782
|
+
percentage
|
|
783
|
+
})) }]
|
|
784
|
+
});
|
|
785
|
+
});
|
|
786
|
+
hooks.hook("kubb:files:processing:end", ({ files }) => {
|
|
787
|
+
sendDataMessage({
|
|
788
|
+
type: "kubb:files:processing:end",
|
|
789
|
+
data: [{ total: files.length }]
|
|
790
|
+
});
|
|
791
|
+
});
|
|
792
|
+
for (const type of [
|
|
793
|
+
"kubb:info",
|
|
794
|
+
"kubb:success",
|
|
795
|
+
"kubb:warn"
|
|
796
|
+
]) hooks.hook(type, ({ message, info }) => {
|
|
797
|
+
sendDataMessage({
|
|
798
|
+
type,
|
|
799
|
+
data: [{
|
|
800
|
+
message,
|
|
801
|
+
info
|
|
802
|
+
}]
|
|
803
|
+
});
|
|
804
|
+
});
|
|
805
|
+
hooks.hook("kubb:generation:start", ({ config }) => {
|
|
806
|
+
sendDataMessage({
|
|
807
|
+
type: "kubb:generation:start",
|
|
808
|
+
data: [{
|
|
809
|
+
name: config.name,
|
|
810
|
+
plugins: config.plugins.length
|
|
811
|
+
}]
|
|
812
|
+
});
|
|
813
|
+
});
|
|
814
|
+
hooks.hook("kubb:generation:end", async ({ config, storage, diagnostics = [], status, hrStart, filesCreated }) => {
|
|
815
|
+
const paths = await storage.readKeys();
|
|
816
|
+
const files = {};
|
|
817
|
+
await inParallel({
|
|
818
|
+
items: paths,
|
|
819
|
+
limit: FILE_READ_CONCURRENCY,
|
|
820
|
+
run: async (path) => {
|
|
821
|
+
const content = await storage.readItem(path);
|
|
822
|
+
if (content !== null) files[path] = content;
|
|
823
|
+
}
|
|
824
|
+
});
|
|
825
|
+
sendDataMessage({
|
|
826
|
+
type: "kubb:generation:end",
|
|
827
|
+
data: [{
|
|
828
|
+
config,
|
|
829
|
+
storage: files
|
|
830
|
+
}]
|
|
831
|
+
});
|
|
832
|
+
if (!hrStart) return;
|
|
833
|
+
sendDataMessage({
|
|
834
|
+
type: "kubb:generation:summary",
|
|
835
|
+
data: [{
|
|
836
|
+
duration: Math.round(getElapsedMs(hrStart)),
|
|
837
|
+
fileCount: filesCreated ?? 0,
|
|
838
|
+
failedPlugins: Diagnostics.failedPlugins(diagnostics).length,
|
|
839
|
+
status: status ?? "success"
|
|
840
|
+
}]
|
|
841
|
+
});
|
|
842
|
+
});
|
|
843
|
+
hooks.hook("kubb:error", ({ error }) => {
|
|
844
|
+
sendDataMessage({
|
|
845
|
+
type: "kubb:error",
|
|
846
|
+
data: [{
|
|
847
|
+
message: error.message,
|
|
848
|
+
stack: error.stack
|
|
849
|
+
}]
|
|
850
|
+
});
|
|
851
|
+
});
|
|
852
|
+
for (const type of [
|
|
853
|
+
"kubb:lifecycle:start",
|
|
854
|
+
"kubb:lifecycle:end",
|
|
855
|
+
"kubb:format:start",
|
|
856
|
+
"kubb:format:end",
|
|
857
|
+
"kubb:lint:start",
|
|
858
|
+
"kubb:lint:end",
|
|
859
|
+
"kubb:hooks:start",
|
|
860
|
+
"kubb:hooks:end"
|
|
861
|
+
]) hooks.hook(type, () => {
|
|
862
|
+
sendDataMessage({
|
|
863
|
+
type,
|
|
864
|
+
data: []
|
|
865
|
+
});
|
|
866
|
+
});
|
|
867
|
+
hooks.hook("kubb:hook:start", ({ id, command, args }) => {
|
|
868
|
+
sendDataMessage({
|
|
869
|
+
type: "kubb:hook:start",
|
|
870
|
+
data: [{
|
|
871
|
+
id,
|
|
872
|
+
command,
|
|
873
|
+
args: args ? [...args] : void 0
|
|
874
|
+
}]
|
|
875
|
+
});
|
|
876
|
+
});
|
|
877
|
+
hooks.hook("kubb:hook:line", ({ id, line }) => {
|
|
878
|
+
sendDataMessage({
|
|
879
|
+
type: "kubb:hook:line",
|
|
880
|
+
data: [{
|
|
881
|
+
id,
|
|
882
|
+
line
|
|
883
|
+
}]
|
|
884
|
+
});
|
|
885
|
+
});
|
|
886
|
+
hooks.hook("kubb:hook:end", ({ id, command, args, success, error }) => {
|
|
887
|
+
sendDataMessage({
|
|
888
|
+
type: "kubb:hook:end",
|
|
889
|
+
data: [{
|
|
890
|
+
id,
|
|
891
|
+
command,
|
|
892
|
+
args: args ? [...args] : void 0,
|
|
893
|
+
success,
|
|
894
|
+
error: error ? {
|
|
895
|
+
message: error.message,
|
|
896
|
+
stack: error.stack
|
|
897
|
+
} : void 0
|
|
898
|
+
}]
|
|
899
|
+
});
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
//#endregion
|
|
903
|
+
//#region src/connectStudio.ts
|
|
904
|
+
/**
|
|
905
|
+
* Schedules another connection attempt.
|
|
906
|
+
*
|
|
907
|
+
* Hoisted out of `connectToStudio` on purpose: a pending retry timer reaches its whole enclosing
|
|
908
|
+
* scope, so keeping it inside would pin the closed socket, the hook emitter, and the session id
|
|
909
|
+
* alive for the length of every retry interval.
|
|
910
|
+
*/
|
|
911
|
+
function reconnect(options) {
|
|
912
|
+
const { signal, retryInterval = agentDefaults.retryIntervalMs } = options;
|
|
913
|
+
if (signal?.aborted) return;
|
|
914
|
+
console.info(styleText("dim", `Retrying connection in ${retryInterval}ms to Kubb Studio ...`));
|
|
915
|
+
const cancel = () => clearTimeout(timer);
|
|
916
|
+
const timer = setTimeout(() => {
|
|
917
|
+
signal?.removeEventListener("abort", cancel);
|
|
918
|
+
if (signal?.aborted) return;
|
|
919
|
+
connectToStudio(options).catch((error) => {
|
|
920
|
+
console.error(styleText("red", `Reconnect attempt to Kubb Studio failed: ${getErrorMessage(error)}`));
|
|
921
|
+
if (error instanceof InvalidAgentTokenError) return;
|
|
922
|
+
reconnect(options);
|
|
923
|
+
});
|
|
924
|
+
}, retryInterval);
|
|
925
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
926
|
+
}
|
|
927
|
+
async function connectToStudio(options) {
|
|
928
|
+
const { token, studioUrl = agentDefaults.studioUrl, configPath, loadConfig, version: version$1, client, allowWrite = false, allowConfigEdit = false, allowInput = false, allowExec = false, root = process$1.cwd(), heartbeatInterval: requestedHeartbeatInterval = agentDefaults.heartbeatIntervalMs, signal, installLogger } = options;
|
|
929
|
+
const heartbeatInterval = Math.min(requestedHeartbeatInterval, agentDefaults.heartbeatIntervalMs);
|
|
930
|
+
const hooks = new Hookable();
|
|
931
|
+
await installLogger?.(hooks);
|
|
932
|
+
try {
|
|
933
|
+
await hooks.callHook("studio:connecting", { url: studioUrl });
|
|
934
|
+
const { sessionId, slug, wsUrl, isSandbox, version: sessionStudioVersion } = await createAgentSession({
|
|
935
|
+
token,
|
|
936
|
+
studioUrl
|
|
937
|
+
});
|
|
938
|
+
let studioVersion = sessionStudioVersion;
|
|
939
|
+
const ws = createWebsocket(wsUrl, { headers: { Authorization: `Bearer ${token}` } });
|
|
940
|
+
const canWrite = isSandbox ? false : allowWrite;
|
|
941
|
+
const canEditConfig = isSandbox ? false : allowConfigEdit;
|
|
942
|
+
const configFilePath = path.resolve(root, configPath);
|
|
943
|
+
const canUseInput = isSandbox || allowInput;
|
|
944
|
+
let serverDisconnected = false;
|
|
945
|
+
let isGenerating = false;
|
|
946
|
+
let heartbeatTimer;
|
|
947
|
+
let lastPongAt = Date.now();
|
|
948
|
+
const onAbort = () => void teardown({
|
|
949
|
+
reason: "shutdown",
|
|
950
|
+
retry: false
|
|
951
|
+
});
|
|
952
|
+
function cleanup(reason = "cleanup") {
|
|
953
|
+
clearInterval(heartbeatTimer);
|
|
954
|
+
heartbeatTimer = void 0;
|
|
955
|
+
signal?.removeEventListener("abort", onAbort);
|
|
956
|
+
hooks.removeAllHooks();
|
|
957
|
+
try {
|
|
958
|
+
ws.close(1e3, reason);
|
|
959
|
+
} catch {}
|
|
960
|
+
ws.removeEventListener("open", onOpen);
|
|
961
|
+
ws.removeEventListener("close", onClose);
|
|
962
|
+
ws.removeEventListener("error", onError);
|
|
963
|
+
ws.removeEventListener("message", onMessage);
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* Reads `kubb.config.ts` and reports which plugin options Studio may edit.
|
|
967
|
+
*
|
|
968
|
+
* Skipped when the host did not grant `allowConfigEdit`. The patcher pulls in `magicast`
|
|
969
|
+
* (~25ms, ~55MB RSS), so read-only agents never import it.
|
|
970
|
+
*
|
|
971
|
+
* Not cached: the user can edit the file between two Studio actions.
|
|
972
|
+
*/
|
|
973
|
+
async function readConfigFileView(source) {
|
|
974
|
+
if (!canEditConfig) return;
|
|
975
|
+
try {
|
|
976
|
+
const { readConfig } = await import("./configFile-ZnV5tPon.js");
|
|
977
|
+
return readConfig(source ?? await readFile(configFilePath, "utf-8"));
|
|
978
|
+
} catch (error) {
|
|
979
|
+
await hooks.callHook("studio:warn", { message: `Could not read ${configFilePath}: ${getErrorMessage(error)}` });
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
async function sendConnectedPayload() {
|
|
984
|
+
const config = await loadConfig();
|
|
985
|
+
sendAgentMessage(ws, {
|
|
986
|
+
type: "agent:connect",
|
|
987
|
+
payload: {
|
|
988
|
+
versions: {
|
|
989
|
+
kubb: version,
|
|
990
|
+
agent: version$1
|
|
991
|
+
},
|
|
992
|
+
root,
|
|
993
|
+
config: {
|
|
994
|
+
path: configPath,
|
|
995
|
+
file: await readConfigFileView(),
|
|
996
|
+
plugins: config.plugins.map((plugin) => ({
|
|
997
|
+
name: `@kubb/${plugin.name}`,
|
|
998
|
+
options: plugin.options ?? {}
|
|
999
|
+
}))
|
|
1000
|
+
},
|
|
1001
|
+
permissions: {
|
|
1002
|
+
allowWrite: canWrite,
|
|
1003
|
+
allowInput: canUseInput,
|
|
1004
|
+
allowExec,
|
|
1005
|
+
allowConfigEdit: canEditConfig
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
async function handleOpen() {
|
|
1011
|
+
lastPongAt = Date.now();
|
|
1012
|
+
await hooks.callHook("studio:connected", {
|
|
1013
|
+
url: studioUrl,
|
|
1014
|
+
versions: {
|
|
1015
|
+
studio: studioVersion,
|
|
1016
|
+
kubb: version,
|
|
1017
|
+
agent: version$1
|
|
1018
|
+
}
|
|
1019
|
+
});
|
|
1020
|
+
try {
|
|
1021
|
+
await sendConnectedPayload();
|
|
1022
|
+
} catch (error) {
|
|
1023
|
+
await hooks.callHook("studio:warn", { message: `Failed to send the connect payload: ${getErrorMessage(error)}` });
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
const onOpen = () => void handleOpen().catch(() => {});
|
|
1027
|
+
/**
|
|
1028
|
+
* Drops the socket and tells Studio the session is over. `serverDisconnected` guards against
|
|
1029
|
+
* the close event running this a second time, and against a shutdown reconnecting.
|
|
1030
|
+
*/
|
|
1031
|
+
async function teardown({ reason, retry }) {
|
|
1032
|
+
if (serverDisconnected) return;
|
|
1033
|
+
serverDisconnected = true;
|
|
1034
|
+
if (reason === "shutdown") sendAgentMessage(ws, {
|
|
1035
|
+
type: "agent:disconnect",
|
|
1036
|
+
reason: "shutdown"
|
|
1037
|
+
});
|
|
1038
|
+
cleanup(reason);
|
|
1039
|
+
await disconnect({
|
|
1040
|
+
sessionId,
|
|
1041
|
+
studioUrl,
|
|
1042
|
+
token,
|
|
1043
|
+
slug
|
|
1044
|
+
}).catch(() => {});
|
|
1045
|
+
if (retry) reconnect(options);
|
|
1046
|
+
}
|
|
1047
|
+
const onClose = () => teardown({ retry: true });
|
|
1048
|
+
const onError = () => {
|
|
1049
|
+
hooks.callHook("studio:error", { error: /* @__PURE__ */ new Error("Failed to connect to Kubb Studio") });
|
|
1050
|
+
return onClose();
|
|
1051
|
+
};
|
|
1052
|
+
ws.addEventListener("open", onOpen);
|
|
1053
|
+
ws.addEventListener("close", onClose);
|
|
1054
|
+
ws.addEventListener("error", onError);
|
|
1055
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1056
|
+
heartbeatTimer = setInterval(() => {
|
|
1057
|
+
if (Date.now() - lastPongAt > heartbeatInterval * 2) {
|
|
1058
|
+
hooks.callHook("studio:warn", { message: "No reply from Kubb Studio, terminating the stale connection" });
|
|
1059
|
+
clearInterval(heartbeatTimer);
|
|
1060
|
+
heartbeatTimer = void 0;
|
|
1061
|
+
ws.terminate();
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
sendAgentMessage(ws, { type: "agent:ping" });
|
|
1065
|
+
}, heartbeatInterval);
|
|
1066
|
+
hooks.hook("kubb:error", ({ error }) => sendErrorMessage(ws, error));
|
|
1067
|
+
const onMessage = async (message) => {
|
|
1068
|
+
try {
|
|
1069
|
+
const data = JSON.parse(message.data);
|
|
1070
|
+
if (isStudioPingMessage(data)) {
|
|
1071
|
+
lastPongAt = Date.now();
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
if (isDisconnectMessage(data)) {
|
|
1075
|
+
await hooks.callHook("studio:disconnected", { reason: data.reason });
|
|
1076
|
+
if (data.reason === "revoked") {
|
|
1077
|
+
cleanup(`session_${data.reason}`);
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
if (data.reason === "expired") {
|
|
1081
|
+
cleanup();
|
|
1082
|
+
reconnect(options);
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
if (isCommandMessage(data)) {
|
|
1088
|
+
const command = data.type.slice(7);
|
|
1089
|
+
await hooks.callHook("studio:command:start", { command });
|
|
1090
|
+
if (data.type === "studio:generate") {
|
|
1091
|
+
if (isGenerating) {
|
|
1092
|
+
await hooks.callHook("studio:warn", { message: "Ignored generate: a generation is already in progress" });
|
|
1093
|
+
await Promise.resolve(hooks.callHook("kubb:error", { error: /* @__PURE__ */ new Error("A generation is already in progress, please wait for it to finish") })).catch(() => {});
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
isGenerating = true;
|
|
1097
|
+
try {
|
|
1098
|
+
const config = await loadConfig();
|
|
1099
|
+
const patch = data.payload;
|
|
1100
|
+
const plugins = await mergePlugins(config.plugins, patch?.plugins);
|
|
1101
|
+
const adapter = await mergeAdapter(config.adapter, patch?.adapter);
|
|
1102
|
+
const inputOverride = isSandbox ? patch?.input ?? "" : allowInput && patch?.input || void 0;
|
|
1103
|
+
if (allowWrite && isSandbox) await hooks.callHook("studio:warn", { message: "Running in a sandbox, so writing files is disabled" });
|
|
1104
|
+
if (patch?.input && !canUseInput) {
|
|
1105
|
+
const remedy = client?.kind === "cli" ? "--allowInput, or answer yes when kubb studio asks," : "KUBB_AGENT_ALLOW_INPUT=true";
|
|
1106
|
+
await hooks.callHook("studio:warn", { message: `Ignored the spec from Studio; set ${remedy} to generate from it` });
|
|
1107
|
+
}
|
|
1108
|
+
const generationHooks = new Hookable();
|
|
1109
|
+
await installLogger?.(generationHooks);
|
|
1110
|
+
setupHookListener(generationHooks, root);
|
|
1111
|
+
setupEventsStream(ws, generationHooks);
|
|
1112
|
+
const resolvedPlugins = plugins ?? config.plugins;
|
|
1113
|
+
await generate({
|
|
1114
|
+
config: {
|
|
1115
|
+
...config,
|
|
1116
|
+
root,
|
|
1117
|
+
input: inputOverride ?? config.input,
|
|
1118
|
+
storage: canWrite ? fsStorage() : memoryStorage(),
|
|
1119
|
+
output: allowExec ? { ...config.output } : {
|
|
1120
|
+
...config.output,
|
|
1121
|
+
format: false,
|
|
1122
|
+
lint: false,
|
|
1123
|
+
postGenerate: []
|
|
1124
|
+
},
|
|
1125
|
+
plugins: resolvedPlugins,
|
|
1126
|
+
adapter
|
|
1127
|
+
},
|
|
1128
|
+
hooks: generationHooks
|
|
1129
|
+
});
|
|
1130
|
+
await hooks.callHook("studio:command:end", {
|
|
1131
|
+
command,
|
|
1132
|
+
info: `${resolvedPlugins.length} plugin${resolvedPlugins.length === 1 ? "" : "s"}, ${canWrite ? "written to disk" : "in memory"}${inputOverride !== void 0 ? ", from a Studio spec" : ""}`
|
|
1133
|
+
});
|
|
1134
|
+
} finally {
|
|
1135
|
+
isGenerating = false;
|
|
1136
|
+
}
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
if (data.type === "studio:connect") {
|
|
1140
|
+
studioVersion = data.version ?? studioVersion;
|
|
1141
|
+
await sendConnectedPayload();
|
|
1142
|
+
await hooks.callHook("studio:command:end", { command });
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
if (data.type === "studio:save") {
|
|
1146
|
+
if (!Array.isArray(data.edits)) {
|
|
1147
|
+
await hooks.callHook("studio:warn", { message: "Ignored save: the message carried no edits" });
|
|
1148
|
+
sendAgentMessage(ws, {
|
|
1149
|
+
type: "agent:save",
|
|
1150
|
+
payload: {
|
|
1151
|
+
outcomes: [],
|
|
1152
|
+
changed: false
|
|
1153
|
+
}
|
|
1154
|
+
});
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
const edits = data.edits;
|
|
1158
|
+
const refuse = (reason) => sendAgentMessage(ws, {
|
|
1159
|
+
type: "agent:save",
|
|
1160
|
+
payload: {
|
|
1161
|
+
outcomes: edits.map((edit) => ({
|
|
1162
|
+
edit,
|
|
1163
|
+
applied: false,
|
|
1164
|
+
reason
|
|
1165
|
+
})),
|
|
1166
|
+
changed: false
|
|
1167
|
+
}
|
|
1168
|
+
});
|
|
1169
|
+
if (!canEditConfig) {
|
|
1170
|
+
await hooks.callHook("studio:warn", { message: "Ignored save: editing kubb.config.ts was not granted" });
|
|
1171
|
+
refuse("the agent was not granted permission to edit kubb.config.ts");
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
if (isGenerating) {
|
|
1175
|
+
refuse("a generation is in progress");
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
try {
|
|
1179
|
+
const { applyConfigEdits } = await import("./configFile-ZnV5tPon.js");
|
|
1180
|
+
const { source: patched, outcomes, changed } = applyConfigEdits(await readFile(configFilePath, "utf-8"), edits);
|
|
1181
|
+
if (changed) await writeFile(configFilePath, patched, "utf-8");
|
|
1182
|
+
sendAgentMessage(ws, {
|
|
1183
|
+
type: "agent:save",
|
|
1184
|
+
payload: {
|
|
1185
|
+
outcomes,
|
|
1186
|
+
changed,
|
|
1187
|
+
file: changed ? await readConfigFileView(patched) : void 0
|
|
1188
|
+
}
|
|
1189
|
+
});
|
|
1190
|
+
const applied = outcomes.filter((outcome) => outcome.applied).length;
|
|
1191
|
+
await hooks.callHook("studio:command:end", {
|
|
1192
|
+
command,
|
|
1193
|
+
info: `applied ${applied}/${outcomes.length} edits to ${configPath}`
|
|
1194
|
+
});
|
|
1195
|
+
} catch (error) {
|
|
1196
|
+
await hooks.callHook("studio:error", { error: toError(error) });
|
|
1197
|
+
refuse(getErrorMessage(error));
|
|
1198
|
+
}
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
await hooks.callHook("studio:warn", { message: `Ignored an unknown message from Kubb Studio: ${data.type}` });
|
|
1204
|
+
} catch (error) {
|
|
1205
|
+
await hooks.callHook("studio:error", { error: toError(error) });
|
|
1206
|
+
await Promise.resolve(hooks.callHook("kubb:error", { error: toError(error) })).catch(() => {});
|
|
1207
|
+
}
|
|
1208
|
+
};
|
|
1209
|
+
ws.addEventListener("message", onMessage);
|
|
1210
|
+
} catch (error) {
|
|
1211
|
+
await hooks.callHook("studio:error", { error: toError(error) });
|
|
1212
|
+
if (error instanceof InvalidAgentTokenError) throw error;
|
|
1213
|
+
reconnect(options);
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
//#endregion
|
|
1217
|
+
//#region src/client.ts
|
|
1218
|
+
/**
|
|
1219
|
+
* Creates the Kubb Studio client: the connection, the command loop, and the generation event
|
|
1220
|
+
* stream shared by the `kubb studio` CLI command and the Docker agent.
|
|
1221
|
+
*
|
|
1222
|
+
* Every permission is off by default. A host that wants more grants it explicitly.
|
|
1223
|
+
*
|
|
1224
|
+
* @example
|
|
1225
|
+
* ```ts
|
|
1226
|
+
* const studio = createClient({ token, configPath, version, loadConfig: () => loadMyConfig() })
|
|
1227
|
+
* await studio.connect()
|
|
1228
|
+
* ```
|
|
1229
|
+
*/
|
|
1230
|
+
function createClient({ storage, ...options }) {
|
|
1231
|
+
if (storage) setStorage(storage);
|
|
1232
|
+
const controller = new AbortController();
|
|
1233
|
+
const poolSize = options.poolSize ?? agentDefaults.poolSize;
|
|
1234
|
+
return {
|
|
1235
|
+
async connect() {
|
|
1236
|
+
await registerAgent({
|
|
1237
|
+
token: options.token,
|
|
1238
|
+
studioUrl: options.studioUrl ?? agentDefaults.studioUrl,
|
|
1239
|
+
poolSize
|
|
1240
|
+
});
|
|
1241
|
+
await Promise.all(Array.from({ length: poolSize }, () => connectToStudio({
|
|
1242
|
+
...options,
|
|
1243
|
+
signal: controller.signal
|
|
1244
|
+
})));
|
|
1245
|
+
},
|
|
1246
|
+
disconnect() {
|
|
1247
|
+
controller.abort();
|
|
1248
|
+
}
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
//#endregion
|
|
1252
|
+
//#region src/pair.ts
|
|
1253
|
+
/**
|
|
1254
|
+
* Identifies the CLI to Studio's device authorization endpoint. A label, not a secret: what
|
|
1255
|
+
* authorizes a pairing is a signed-in person approving the code in the browser.
|
|
1256
|
+
*/
|
|
1257
|
+
const CLIENT_ID = "kubb-cli";
|
|
1258
|
+
/**
|
|
1259
|
+
* Asks Studio for a pairing code. The machine token travels with the request and is stored against
|
|
1260
|
+
* the code, so approval knows which machine it is pairing: the same machine pairing twice rotates
|
|
1261
|
+
* one agent's token instead of creating a second agent.
|
|
1262
|
+
*/
|
|
1263
|
+
async function startPairing({ studioUrl = agentDefaults.studioUrl, name, hostname, clientId = CLIENT_ID, agentKind }) {
|
|
1264
|
+
return ofetch(`${studioUrl}/api/auth/device/code`, {
|
|
1265
|
+
method: "POST",
|
|
1266
|
+
body: {
|
|
1267
|
+
client_id: clientId,
|
|
1268
|
+
name,
|
|
1269
|
+
hostname,
|
|
1270
|
+
machine_token: await getMachineToken(),
|
|
1271
|
+
agent_kind: agentKind
|
|
1272
|
+
}
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
function isPairingResult(response) {
|
|
1276
|
+
return !!response && typeof response === "object" && "token" in response && typeof response.token === "string";
|
|
1277
|
+
}
|
|
1278
|
+
/**
|
|
1279
|
+
* Polls until the user approves or denies, honoring the server's `slow_down` back-off. A poll that
|
|
1280
|
+
* cannot reach Studio is warned about and retried, since the code stays valid either way.
|
|
1281
|
+
*
|
|
1282
|
+
* Studio's own endpoint is used rather than the auth layer's `/device/token`, because an approved
|
|
1283
|
+
* Kubb pairing is worth an agent bearer token, not a user session.
|
|
1284
|
+
*
|
|
1285
|
+
* @throws when the code expires, the user denies it, or Studio returns an unexpected error.
|
|
1286
|
+
*/
|
|
1287
|
+
async function pollForPairingToken({ studioUrl = agentDefaults.studioUrl, session }) {
|
|
1288
|
+
const deadline = Date.now() + (session.expires_in > 0 ? session.expires_in : 600) * 1e3;
|
|
1289
|
+
let intervalMs = (session.interval > 0 ? session.interval : 5) * 1e3;
|
|
1290
|
+
while (Date.now() < deadline) {
|
|
1291
|
+
await setTimeout$1(intervalMs);
|
|
1292
|
+
let response;
|
|
1293
|
+
try {
|
|
1294
|
+
response = await ofetch(`${studioUrl}/api/agent/token`, {
|
|
1295
|
+
method: "POST",
|
|
1296
|
+
body: { device_code: session.device_code },
|
|
1297
|
+
ignoreResponseError: true
|
|
1298
|
+
});
|
|
1299
|
+
} catch (error) {
|
|
1300
|
+
console.warn(styleText("yellow", `Could not reach Kubb Studio while waiting for approval, retrying: ${getErrorMessage(error)}`));
|
|
1301
|
+
continue;
|
|
1302
|
+
}
|
|
1303
|
+
if (isPairingResult(response)) return response;
|
|
1304
|
+
if (!response || typeof response !== "object" || !("error" in response) || typeof response.error !== "string") throw new Error("Kubb Studio returned an empty pairing response, pair again");
|
|
1305
|
+
if (response.error === "authorization_pending") continue;
|
|
1306
|
+
if (response.error === "slow_down") {
|
|
1307
|
+
intervalMs += 5e3;
|
|
1308
|
+
continue;
|
|
1309
|
+
}
|
|
1310
|
+
if (response.error === "access_denied") throw new Error(response.error_description ?? "Pairing was denied in the browser");
|
|
1311
|
+
if (response.error === "expired_token" || response.error === "invalid_grant") throw new Error(response.error_description ?? "The pairing code expired, pair again");
|
|
1312
|
+
throw new Error(response.error_description ?? `Pairing failed (${response.error})`);
|
|
1313
|
+
}
|
|
1314
|
+
throw new Error("The pairing code expired, pair again");
|
|
1315
|
+
}
|
|
1316
|
+
//#endregion
|
|
1317
|
+
export { InvalidAgentTokenError, createClient, createFileStorage, defaultStudioUrl, pollForPairingToken, setStorage, startPairing };
|
|
1318
|
+
|
|
1319
|
+
//# sourceMappingURL=index.js.map
|