@kubb/studio 5.3.3 → 5.3.5
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.cjs +143 -43
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +143 -43
- package/dist/index.js.map +1 -1
- package/dist/protocol.cjs +7 -1
- package/dist/protocol.cjs.map +1 -1
- package/dist/protocol.d.ts +92 -19
- package/dist/protocol.js +7 -2
- package/dist/protocol.js.map +1 -1
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -98,7 +98,9 @@ export declare function createJob({ studioUrl, token, type, agentId, name, versi
|
|
|
98
98
|
config?: Record<string, unknown>;
|
|
99
99
|
}): Promise<StudioJob>;
|
|
100
100
|
/**
|
|
101
|
-
* Polls `GET /api/jobs/{id}` until the job reaches `success` or `failed
|
|
101
|
+
* Polls `GET /api/jobs/{id}` until the job reaches `success` or `failed`, waiting
|
|
102
|
+
* {@link INITIAL_POLL_DELAY_MS} first and doubling up to {@link MAX_POLL_INTERVAL_MS} so a long
|
|
103
|
+
* job stays inside the API key's rate limit.
|
|
102
104
|
*
|
|
103
105
|
* A `failed` job resolves normally. Check `job.status` and `job.error`. Throws only when the
|
|
104
106
|
* deadline passes before Studio finishes.
|
package/dist/index.js
CHANGED
|
@@ -609,18 +609,41 @@ async function createJob({ studioUrl, token, type, agentId, name, version, confi
|
|
|
609
609
|
return job;
|
|
610
610
|
}
|
|
611
611
|
/**
|
|
612
|
-
*
|
|
612
|
+
* A job runs a generation and packs a tarball, so it is never done the instant it is queued.
|
|
613
|
+
*/
|
|
614
|
+
const INITIAL_POLL_DELAY_MS = 2e3;
|
|
615
|
+
/**
|
|
616
|
+
* Slowest the poll backs off to. Requests per run are roughly `timeoutMs` divided by this, and
|
|
617
|
+
* every concurrent run on the same organization key draws on one budget.
|
|
618
|
+
*/
|
|
619
|
+
const MAX_POLL_INTERVAL_MS = 3e4;
|
|
620
|
+
/**
|
|
621
|
+
* Polls `GET /api/jobs/{id}` until the job reaches `success` or `failed`, waiting
|
|
622
|
+
* {@link INITIAL_POLL_DELAY_MS} first and doubling up to {@link MAX_POLL_INTERVAL_MS} so a long
|
|
623
|
+
* job stays inside the API key's rate limit.
|
|
613
624
|
*
|
|
614
625
|
* A `failed` job resolves normally. Check `job.status` and `job.error`. Throws only when the
|
|
615
626
|
* deadline passes before Studio finishes.
|
|
616
627
|
*/
|
|
617
628
|
async function waitForJob({ studioUrl, token, id, timeoutMs = 6e4 }) {
|
|
618
629
|
const deadline = Date.now() + timeoutMs;
|
|
630
|
+
let interval = INITIAL_POLL_DELAY_MS;
|
|
619
631
|
for (;;) {
|
|
620
|
-
|
|
621
|
-
if (job.status === "success" || job.status === "failed") return job;
|
|
632
|
+
await new Promise((resolve) => setTimeout(resolve, Math.max(Math.min(interval, deadline - Date.now()), 0)));
|
|
622
633
|
if (Date.now() >= deadline) throw new Error("Timed out waiting for the Studio job");
|
|
623
|
-
|
|
634
|
+
interval = Math.min(interval * 2, MAX_POLL_INTERVAL_MS);
|
|
635
|
+
try {
|
|
636
|
+
const { job } = await ofetch(`${studioUrl}/api/jobs/${id}`, {
|
|
637
|
+
headers: { "x-api-key": token },
|
|
638
|
+
retry: false
|
|
639
|
+
});
|
|
640
|
+
if (job.status === "success" || job.status === "failed") return job;
|
|
641
|
+
} catch (error) {
|
|
642
|
+
const response = error.response;
|
|
643
|
+
if (response?.status !== 429) throw error;
|
|
644
|
+
const retryAfter = response._data?.data?.tryAgainIn;
|
|
645
|
+
interval = Math.max(interval, typeof retryAfter === "number" && Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter : MAX_POLL_INTERVAL_MS);
|
|
646
|
+
}
|
|
624
647
|
}
|
|
625
648
|
}
|
|
626
649
|
/**
|
|
@@ -650,7 +673,7 @@ async function createAgent({ studioUrl, token, name, machineToken }) {
|
|
|
650
673
|
}
|
|
651
674
|
//#endregion
|
|
652
675
|
//#region package.json
|
|
653
|
-
var version = "5.3.
|
|
676
|
+
var version = "5.3.5";
|
|
654
677
|
//#endregion
|
|
655
678
|
//#region src/hooks.ts
|
|
656
679
|
/**
|
|
@@ -1713,12 +1736,6 @@ async function createSnapshotPackage(files, packageInfo) {
|
|
|
1713
1736
|
//#endregion
|
|
1714
1737
|
//#region src/ws.ts
|
|
1715
1738
|
/**
|
|
1716
|
-
* How many generated files are read from storage at once when building the
|
|
1717
|
-
* `kubb:generation:end` payload. A spec producing thousands of files would otherwise fire one
|
|
1718
|
-
* `storage.readItem` per file simultaneously.
|
|
1719
|
-
*/
|
|
1720
|
-
const FILE_READ_CONCURRENCY = 50;
|
|
1721
|
-
/**
|
|
1722
1739
|
* How long the initial handshake may take before the socket is closed and the reconnect loop
|
|
1723
1740
|
* takes over.
|
|
1724
1741
|
*/
|
|
@@ -1734,6 +1751,12 @@ const require = createRequire(import.meta.url);
|
|
|
1734
1751
|
function relativeStoragePath(root, filePath) {
|
|
1735
1752
|
return (isAbsolute(filePath) ? relative(resolve(root), filePath) : filePath).replaceAll("\\", "/");
|
|
1736
1753
|
}
|
|
1754
|
+
/**
|
|
1755
|
+
* Inverse of {@link relativeStoragePath}: rebuilds the storage key a relative path came from.
|
|
1756
|
+
*/
|
|
1757
|
+
function absoluteStoragePath(root, relativePath) {
|
|
1758
|
+
return resolve(root, relativePath);
|
|
1759
|
+
}
|
|
1737
1760
|
async function resolvePeerDependencies(names) {
|
|
1738
1761
|
const uniqueNames = [...new Set(names.map(toPackageName))];
|
|
1739
1762
|
const peerDependencies = {};
|
|
@@ -1851,12 +1874,12 @@ function setupEventsStream(ws, hooks, jobId, options = {}) {
|
|
|
1851
1874
|
}]
|
|
1852
1875
|
});
|
|
1853
1876
|
});
|
|
1854
|
-
on("kubb:build:end", ({ files, outputDir }) => {
|
|
1877
|
+
on("kubb:build:end", ({ files, config, outputDir }) => {
|
|
1855
1878
|
sendDataMessage({
|
|
1856
1879
|
type: "kubb:build:end",
|
|
1857
1880
|
data: [{
|
|
1858
1881
|
files: files.map((file) => ({
|
|
1859
|
-
path: file.path,
|
|
1882
|
+
path: relativeStoragePath(config.root, file.path),
|
|
1860
1883
|
name: file.name
|
|
1861
1884
|
})),
|
|
1862
1885
|
outputDir
|
|
@@ -1910,25 +1933,18 @@ function setupEventsStream(ws, hooks, jobId, options = {}) {
|
|
|
1910
1933
|
});
|
|
1911
1934
|
on("kubb:generation:end", async ({ config, storage, diagnostics = [], status, hrStart, filesCreated }) => {
|
|
1912
1935
|
const { peerDependencies, missingDependencies } = await resolvePeerDependencies(config.plugins.map(({ name }) => name));
|
|
1913
|
-
const
|
|
1914
|
-
const
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
}
|
|
1936
|
+
const keys = await storage.readKeys();
|
|
1937
|
+
const paths = new Set(keys.map((key) => relativeStoragePath(config.root, key)));
|
|
1938
|
+
options.onGenerationEnd?.({
|
|
1939
|
+
storage,
|
|
1940
|
+
root: config.root,
|
|
1941
|
+
paths,
|
|
1942
|
+
peerDependencies,
|
|
1943
|
+
missingDependencies
|
|
1922
1944
|
});
|
|
1923
|
-
options.onGenerationEnd?.(files);
|
|
1924
1945
|
sendDataMessage({
|
|
1925
1946
|
type: "kubb:generation:end",
|
|
1926
|
-
data: [
|
|
1927
|
-
config,
|
|
1928
|
-
storage: options.skipStorage ? {} : files,
|
|
1929
|
-
peerDependencies,
|
|
1930
|
-
missingDependencies
|
|
1931
|
-
}]
|
|
1947
|
+
data: []
|
|
1932
1948
|
});
|
|
1933
1949
|
if (!hrStart) return;
|
|
1934
1950
|
sendDataMessage({
|
|
@@ -2006,6 +2022,11 @@ function setupEventsStream(ws, hooks, jobId, options = {}) {
|
|
|
2006
2022
|
//#endregion
|
|
2007
2023
|
//#region src/StudioSession.ts
|
|
2008
2024
|
/**
|
|
2025
|
+
* How many files are read from storage at once when serving `studio:files` or packing a
|
|
2026
|
+
* `studio:snapshot`.
|
|
2027
|
+
*/
|
|
2028
|
+
const FILE_READ_CONCURRENCY = 50;
|
|
2029
|
+
/**
|
|
2009
2030
|
* How long the `agent:connect` handshake may go unacknowledged before `studio:ready` is given up
|
|
2010
2031
|
* on for this open. A Studio that predates the ack never sends one, so this only ever produces a
|
|
2011
2032
|
* warning, not a reconnect.
|
|
@@ -2028,6 +2049,7 @@ function applyStudioDefaults(options) {
|
|
|
2028
2049
|
allowConfigEdit: false,
|
|
2029
2050
|
allowInput: false,
|
|
2030
2051
|
allowExec: false,
|
|
2052
|
+
allowRead: false,
|
|
2031
2053
|
...options.permissions
|
|
2032
2054
|
},
|
|
2033
2055
|
retryInterval: options.retryInterval ?? agentDefaults.retryIntervalMs,
|
|
@@ -2108,6 +2130,12 @@ var StudioSession = class {
|
|
|
2108
2130
|
get #canUseInput() {
|
|
2109
2131
|
return this.#isSandbox || this.#options.permissions.allowInput;
|
|
2110
2132
|
}
|
|
2133
|
+
/**
|
|
2134
|
+
* A sandbox agent always allows reading its output back; a local agent only when opted in.
|
|
2135
|
+
*/
|
|
2136
|
+
get #canRead() {
|
|
2137
|
+
return this.#isSandbox || this.#options.permissions.allowRead;
|
|
2138
|
+
}
|
|
2111
2139
|
async connect() {
|
|
2112
2140
|
const { token, studioUrl, signal, heartbeatInterval, installLogger } = this.#options;
|
|
2113
2141
|
await installLogger?.(this.#hooks);
|
|
@@ -2201,7 +2229,8 @@ var StudioSession = class {
|
|
|
2201
2229
|
...permissions,
|
|
2202
2230
|
allowWrite: this.#canWrite,
|
|
2203
2231
|
allowInput: this.#canUseInput,
|
|
2204
|
-
allowConfigEdit: this.#canEditConfig
|
|
2232
|
+
allowConfigEdit: this.#canEditConfig,
|
|
2233
|
+
allowRead: this.#canRead
|
|
2205
2234
|
}
|
|
2206
2235
|
}
|
|
2207
2236
|
});
|
|
@@ -2343,6 +2372,9 @@ var StudioSession = class {
|
|
|
2343
2372
|
case "studio:snapshot":
|
|
2344
2373
|
await this.#handleSnapshot(ws, data, command);
|
|
2345
2374
|
return;
|
|
2375
|
+
case "studio:files":
|
|
2376
|
+
await this.#handleFiles(ws, data, command);
|
|
2377
|
+
return;
|
|
2346
2378
|
}
|
|
2347
2379
|
}
|
|
2348
2380
|
async #handleGenerate(ws, data, command) {
|
|
@@ -2366,13 +2398,10 @@ var StudioSession = class {
|
|
|
2366
2398
|
await this.#warn(`Ignored the spec from Studio; set ${remedy} to generate from it`);
|
|
2367
2399
|
}
|
|
2368
2400
|
const resolvedPlugins = plugins ?? config.plugins;
|
|
2369
|
-
|
|
2370
|
-
const detach = [setupHookListener(this.#hooks, root), setupEventsStream(ws, this.#hooks, data.jobId, {
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
generatedFiles = files;
|
|
2374
|
-
}
|
|
2375
|
-
})];
|
|
2401
|
+
this.#lastGeneration = void 0;
|
|
2402
|
+
const detach = [setupHookListener(this.#hooks, root), setupEventsStream(ws, this.#hooks, data.jobId, { onGenerationEnd: (result) => {
|
|
2403
|
+
this.#lastGeneration = result;
|
|
2404
|
+
} })];
|
|
2376
2405
|
try {
|
|
2377
2406
|
await generate({
|
|
2378
2407
|
config: {
|
|
@@ -2393,7 +2422,6 @@ var StudioSession = class {
|
|
|
2393
2422
|
});
|
|
2394
2423
|
} finally {
|
|
2395
2424
|
for (const remove of detach) remove();
|
|
2396
|
-
this.#lastGeneration = generatedFiles;
|
|
2397
2425
|
}
|
|
2398
2426
|
await this.#hooks.callHook("studio:command:end", {
|
|
2399
2427
|
command,
|
|
@@ -2476,23 +2504,39 @@ var StudioSession = class {
|
|
|
2476
2504
|
refuse("a sandbox agent has no project to build a package from");
|
|
2477
2505
|
return;
|
|
2478
2506
|
}
|
|
2479
|
-
const { name, version,
|
|
2507
|
+
const { name, version, bundledDependencies, uploadPath } = data.payload;
|
|
2480
2508
|
if (!name || !version || !uploadPath) {
|
|
2481
2509
|
await this.#warn("Ignored snapshot: the message was missing required fields");
|
|
2482
2510
|
refuse("the message was missing required fields");
|
|
2483
2511
|
return;
|
|
2484
2512
|
}
|
|
2485
|
-
const
|
|
2486
|
-
if (!
|
|
2513
|
+
const generation = this.#lastGeneration;
|
|
2514
|
+
if (!generation) {
|
|
2487
2515
|
await this.#warn("Ignored snapshot: no prior generation to pack");
|
|
2488
2516
|
refuse("no prior generation exists to pack, run a generation first");
|
|
2489
2517
|
return;
|
|
2490
2518
|
}
|
|
2519
|
+
const bundled = new Set(bundledDependencies ?? []);
|
|
2520
|
+
const missing = generation.missingDependencies.filter((dependency) => !bundled.has(dependency));
|
|
2521
|
+
if (missing.length) {
|
|
2522
|
+
await this.#warn(`Ignored snapshot: missing dependencies: ${missing.join(", ")}`);
|
|
2523
|
+
refuse(`missing dependencies: ${missing.join(", ")}`);
|
|
2524
|
+
return;
|
|
2525
|
+
}
|
|
2491
2526
|
try {
|
|
2527
|
+
const files = {};
|
|
2528
|
+
await inParallel({
|
|
2529
|
+
items: [...generation.paths],
|
|
2530
|
+
limit: FILE_READ_CONCURRENCY,
|
|
2531
|
+
run: async (relativePath) => {
|
|
2532
|
+
const content = await generation.storage.readItem(absoluteStoragePath(generation.root, relativePath));
|
|
2533
|
+
if (content !== null) files[relativePath] = content;
|
|
2534
|
+
}
|
|
2535
|
+
});
|
|
2492
2536
|
const { bytes, integrity } = await createSnapshotPackage(files, {
|
|
2493
2537
|
name,
|
|
2494
2538
|
version,
|
|
2495
|
-
peerDependencies: peerDependencies
|
|
2539
|
+
peerDependencies: generation.peerDependencies
|
|
2496
2540
|
});
|
|
2497
2541
|
const { token, studioUrl } = this.#options;
|
|
2498
2542
|
const redirect = await fetch(new URL(uploadPath, studioUrl), {
|
|
@@ -2512,7 +2556,8 @@ var StudioSession = class {
|
|
|
2512
2556
|
jobId: data.jobId,
|
|
2513
2557
|
payload: {
|
|
2514
2558
|
status: "ok",
|
|
2515
|
-
integrity
|
|
2559
|
+
integrity,
|
|
2560
|
+
peerDependencies: generation.peerDependencies
|
|
2516
2561
|
}
|
|
2517
2562
|
});
|
|
2518
2563
|
await this.#hooks.callHook("studio:command:end", {
|
|
@@ -2524,6 +2569,61 @@ var StudioSession = class {
|
|
|
2524
2569
|
refuse(getErrorMessage(error));
|
|
2525
2570
|
}
|
|
2526
2571
|
}
|
|
2572
|
+
async #handleFiles(ws, data, command) {
|
|
2573
|
+
const { client } = this.#options;
|
|
2574
|
+
const refuse = (message) => sendAgentMessage(ws, {
|
|
2575
|
+
type: "agent:files",
|
|
2576
|
+
jobId: data.jobId,
|
|
2577
|
+
payload: {
|
|
2578
|
+
status: "error",
|
|
2579
|
+
message
|
|
2580
|
+
}
|
|
2581
|
+
});
|
|
2582
|
+
if (!this.#canRead) {
|
|
2583
|
+
await this.#warn("Ignored files: reading generated files was not granted");
|
|
2584
|
+
refuse(`the agent was not granted permission to read generated files; set ${client?.kind === "cli" ? "--allow-read, or answer yes when kubb studio asks," : "KUBB_AGENT_ALLOW_READ=true"} to allow it`);
|
|
2585
|
+
return;
|
|
2586
|
+
}
|
|
2587
|
+
if (!Array.isArray(data.payload?.paths)) {
|
|
2588
|
+
await this.#warn("Ignored files: the message carried no paths");
|
|
2589
|
+
refuse("the message carried no paths");
|
|
2590
|
+
return;
|
|
2591
|
+
}
|
|
2592
|
+
const { paths } = data.payload;
|
|
2593
|
+
if (paths.length > 50) {
|
|
2594
|
+
await this.#warn(`Ignored files: requested ${paths.length} paths, more than the 50 allowed per request`);
|
|
2595
|
+
refuse(`at most 50 paths may be requested at once`);
|
|
2596
|
+
return;
|
|
2597
|
+
}
|
|
2598
|
+
const generation = this.#lastGeneration;
|
|
2599
|
+
if (!generation) {
|
|
2600
|
+
await this.#warn("Ignored files: no prior generation to read from");
|
|
2601
|
+
refuse("no prior generation to read from, run a generation first");
|
|
2602
|
+
return;
|
|
2603
|
+
}
|
|
2604
|
+
const requested = paths.filter((path) => generation.paths.has(path));
|
|
2605
|
+
const files = {};
|
|
2606
|
+
await inParallel({
|
|
2607
|
+
items: requested,
|
|
2608
|
+
limit: FILE_READ_CONCURRENCY,
|
|
2609
|
+
run: async (path) => {
|
|
2610
|
+
const content = await generation.storage.readItem(absoluteStoragePath(generation.root, path));
|
|
2611
|
+
if (content !== null) files[path] = content;
|
|
2612
|
+
}
|
|
2613
|
+
});
|
|
2614
|
+
sendAgentMessage(ws, {
|
|
2615
|
+
type: "agent:files",
|
|
2616
|
+
jobId: data.jobId,
|
|
2617
|
+
payload: {
|
|
2618
|
+
status: "ok",
|
|
2619
|
+
files
|
|
2620
|
+
}
|
|
2621
|
+
});
|
|
2622
|
+
await this.#hooks.callHook("studio:command:end", {
|
|
2623
|
+
command,
|
|
2624
|
+
info: `read ${Object.keys(files).length}/${paths.length} requested file${paths.length === 1 ? "" : "s"}`
|
|
2625
|
+
});
|
|
2626
|
+
}
|
|
2527
2627
|
};
|
|
2528
2628
|
//#endregion
|
|
2529
2629
|
//#region src/client.ts
|