@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.cjs
CHANGED
|
@@ -614,18 +614,41 @@ async function createJob({ studioUrl, token, type, agentId, name, version, confi
|
|
|
614
614
|
return job;
|
|
615
615
|
}
|
|
616
616
|
/**
|
|
617
|
-
*
|
|
617
|
+
* A job runs a generation and packs a tarball, so it is never done the instant it is queued.
|
|
618
|
+
*/
|
|
619
|
+
const INITIAL_POLL_DELAY_MS = 2e3;
|
|
620
|
+
/**
|
|
621
|
+
* Slowest the poll backs off to. Requests per run are roughly `timeoutMs` divided by this, and
|
|
622
|
+
* every concurrent run on the same organization key draws on one budget.
|
|
623
|
+
*/
|
|
624
|
+
const MAX_POLL_INTERVAL_MS = 3e4;
|
|
625
|
+
/**
|
|
626
|
+
* Polls `GET /api/jobs/{id}` until the job reaches `success` or `failed`, waiting
|
|
627
|
+
* {@link INITIAL_POLL_DELAY_MS} first and doubling up to {@link MAX_POLL_INTERVAL_MS} so a long
|
|
628
|
+
* job stays inside the API key's rate limit.
|
|
618
629
|
*
|
|
619
630
|
* A `failed` job resolves normally. Check `job.status` and `job.error`. Throws only when the
|
|
620
631
|
* deadline passes before Studio finishes.
|
|
621
632
|
*/
|
|
622
633
|
async function waitForJob({ studioUrl, token, id, timeoutMs = 6e4 }) {
|
|
623
634
|
const deadline = Date.now() + timeoutMs;
|
|
635
|
+
let interval = INITIAL_POLL_DELAY_MS;
|
|
624
636
|
for (;;) {
|
|
625
|
-
|
|
626
|
-
if (job.status === "success" || job.status === "failed") return job;
|
|
637
|
+
await new Promise((resolve) => setTimeout(resolve, Math.max(Math.min(interval, deadline - Date.now()), 0)));
|
|
627
638
|
if (Date.now() >= deadline) throw new Error("Timed out waiting for the Studio job");
|
|
628
|
-
|
|
639
|
+
interval = Math.min(interval * 2, MAX_POLL_INTERVAL_MS);
|
|
640
|
+
try {
|
|
641
|
+
const { job } = await (0, ofetch.ofetch)(`${studioUrl}/api/jobs/${id}`, {
|
|
642
|
+
headers: { "x-api-key": token },
|
|
643
|
+
retry: false
|
|
644
|
+
});
|
|
645
|
+
if (job.status === "success" || job.status === "failed") return job;
|
|
646
|
+
} catch (error) {
|
|
647
|
+
const response = error.response;
|
|
648
|
+
if (response?.status !== 429) throw error;
|
|
649
|
+
const retryAfter = response._data?.data?.tryAgainIn;
|
|
650
|
+
interval = Math.max(interval, typeof retryAfter === "number" && Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter : MAX_POLL_INTERVAL_MS);
|
|
651
|
+
}
|
|
629
652
|
}
|
|
630
653
|
}
|
|
631
654
|
/**
|
|
@@ -655,7 +678,7 @@ async function createAgent({ studioUrl, token, name, machineToken }) {
|
|
|
655
678
|
}
|
|
656
679
|
//#endregion
|
|
657
680
|
//#region package.json
|
|
658
|
-
var version = "5.3.
|
|
681
|
+
var version = "5.3.5";
|
|
659
682
|
//#endregion
|
|
660
683
|
//#region src/hooks.ts
|
|
661
684
|
/**
|
|
@@ -1718,12 +1741,6 @@ async function createSnapshotPackage(files, packageInfo) {
|
|
|
1718
1741
|
//#endregion
|
|
1719
1742
|
//#region src/ws.ts
|
|
1720
1743
|
/**
|
|
1721
|
-
* How many generated files are read from storage at once when building the
|
|
1722
|
-
* `kubb:generation:end` payload. A spec producing thousands of files would otherwise fire one
|
|
1723
|
-
* `storage.readItem` per file simultaneously.
|
|
1724
|
-
*/
|
|
1725
|
-
const FILE_READ_CONCURRENCY = 50;
|
|
1726
|
-
/**
|
|
1727
1744
|
* How long the initial handshake may take before the socket is closed and the reconnect loop
|
|
1728
1745
|
* takes over.
|
|
1729
1746
|
*/
|
|
@@ -1739,6 +1756,12 @@ const require$1 = (0, node_module.createRequire)(require("url").pathToFileURL(__
|
|
|
1739
1756
|
function relativeStoragePath(root, filePath) {
|
|
1740
1757
|
return ((0, node_path.isAbsolute)(filePath) ? (0, node_path.relative)((0, node_path.resolve)(root), filePath) : filePath).replaceAll("\\", "/");
|
|
1741
1758
|
}
|
|
1759
|
+
/**
|
|
1760
|
+
* Inverse of {@link relativeStoragePath}: rebuilds the storage key a relative path came from.
|
|
1761
|
+
*/
|
|
1762
|
+
function absoluteStoragePath(root, relativePath) {
|
|
1763
|
+
return (0, node_path.resolve)(root, relativePath);
|
|
1764
|
+
}
|
|
1742
1765
|
async function resolvePeerDependencies(names) {
|
|
1743
1766
|
const uniqueNames = [...new Set(names.map(toPackageName))];
|
|
1744
1767
|
const peerDependencies = {};
|
|
@@ -1856,12 +1879,12 @@ function setupEventsStream(ws$5, hooks, jobId, options = {}) {
|
|
|
1856
1879
|
}]
|
|
1857
1880
|
});
|
|
1858
1881
|
});
|
|
1859
|
-
on("kubb:build:end", ({ files, outputDir }) => {
|
|
1882
|
+
on("kubb:build:end", ({ files, config, outputDir }) => {
|
|
1860
1883
|
sendDataMessage({
|
|
1861
1884
|
type: "kubb:build:end",
|
|
1862
1885
|
data: [{
|
|
1863
1886
|
files: files.map((file) => ({
|
|
1864
|
-
path: file.path,
|
|
1887
|
+
path: relativeStoragePath(config.root, file.path),
|
|
1865
1888
|
name: file.name
|
|
1866
1889
|
})),
|
|
1867
1890
|
outputDir
|
|
@@ -1915,25 +1938,18 @@ function setupEventsStream(ws$5, hooks, jobId, options = {}) {
|
|
|
1915
1938
|
});
|
|
1916
1939
|
on("kubb:generation:end", async ({ config, storage, diagnostics = [], status, hrStart, filesCreated }) => {
|
|
1917
1940
|
const { peerDependencies, missingDependencies } = await resolvePeerDependencies(config.plugins.map(({ name }) => name));
|
|
1918
|
-
const
|
|
1919
|
-
const
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
}
|
|
1941
|
+
const keys = await storage.readKeys();
|
|
1942
|
+
const paths = new Set(keys.map((key) => relativeStoragePath(config.root, key)));
|
|
1943
|
+
options.onGenerationEnd?.({
|
|
1944
|
+
storage,
|
|
1945
|
+
root: config.root,
|
|
1946
|
+
paths,
|
|
1947
|
+
peerDependencies,
|
|
1948
|
+
missingDependencies
|
|
1927
1949
|
});
|
|
1928
|
-
options.onGenerationEnd?.(files);
|
|
1929
1950
|
sendDataMessage({
|
|
1930
1951
|
type: "kubb:generation:end",
|
|
1931
|
-
data: [
|
|
1932
|
-
config,
|
|
1933
|
-
storage: options.skipStorage ? {} : files,
|
|
1934
|
-
peerDependencies,
|
|
1935
|
-
missingDependencies
|
|
1936
|
-
}]
|
|
1952
|
+
data: []
|
|
1937
1953
|
});
|
|
1938
1954
|
if (!hrStart) return;
|
|
1939
1955
|
sendDataMessage({
|
|
@@ -2011,6 +2027,11 @@ function setupEventsStream(ws$5, hooks, jobId, options = {}) {
|
|
|
2011
2027
|
//#endregion
|
|
2012
2028
|
//#region src/StudioSession.ts
|
|
2013
2029
|
/**
|
|
2030
|
+
* How many files are read from storage at once when serving `studio:files` or packing a
|
|
2031
|
+
* `studio:snapshot`.
|
|
2032
|
+
*/
|
|
2033
|
+
const FILE_READ_CONCURRENCY = 50;
|
|
2034
|
+
/**
|
|
2014
2035
|
* How long the `agent:connect` handshake may go unacknowledged before `studio:ready` is given up
|
|
2015
2036
|
* on for this open. A Studio that predates the ack never sends one, so this only ever produces a
|
|
2016
2037
|
* warning, not a reconnect.
|
|
@@ -2033,6 +2054,7 @@ function applyStudioDefaults(options) {
|
|
|
2033
2054
|
allowConfigEdit: false,
|
|
2034
2055
|
allowInput: false,
|
|
2035
2056
|
allowExec: false,
|
|
2057
|
+
allowRead: false,
|
|
2036
2058
|
...options.permissions
|
|
2037
2059
|
},
|
|
2038
2060
|
retryInterval: options.retryInterval ?? agentDefaults.retryIntervalMs,
|
|
@@ -2113,6 +2135,12 @@ var StudioSession = class {
|
|
|
2113
2135
|
get #canUseInput() {
|
|
2114
2136
|
return this.#isSandbox || this.#options.permissions.allowInput;
|
|
2115
2137
|
}
|
|
2138
|
+
/**
|
|
2139
|
+
* A sandbox agent always allows reading its output back; a local agent only when opted in.
|
|
2140
|
+
*/
|
|
2141
|
+
get #canRead() {
|
|
2142
|
+
return this.#isSandbox || this.#options.permissions.allowRead;
|
|
2143
|
+
}
|
|
2116
2144
|
async connect() {
|
|
2117
2145
|
const { token, studioUrl, signal, heartbeatInterval, installLogger } = this.#options;
|
|
2118
2146
|
await installLogger?.(this.#hooks);
|
|
@@ -2206,7 +2234,8 @@ var StudioSession = class {
|
|
|
2206
2234
|
...permissions,
|
|
2207
2235
|
allowWrite: this.#canWrite,
|
|
2208
2236
|
allowInput: this.#canUseInput,
|
|
2209
|
-
allowConfigEdit: this.#canEditConfig
|
|
2237
|
+
allowConfigEdit: this.#canEditConfig,
|
|
2238
|
+
allowRead: this.#canRead
|
|
2210
2239
|
}
|
|
2211
2240
|
}
|
|
2212
2241
|
});
|
|
@@ -2348,6 +2377,9 @@ var StudioSession = class {
|
|
|
2348
2377
|
case "studio:snapshot":
|
|
2349
2378
|
await this.#handleSnapshot(ws, data, command);
|
|
2350
2379
|
return;
|
|
2380
|
+
case "studio:files":
|
|
2381
|
+
await this.#handleFiles(ws, data, command);
|
|
2382
|
+
return;
|
|
2351
2383
|
}
|
|
2352
2384
|
}
|
|
2353
2385
|
async #handleGenerate(ws, data, command) {
|
|
@@ -2371,13 +2403,10 @@ var StudioSession = class {
|
|
|
2371
2403
|
await this.#warn(`Ignored the spec from Studio; set ${remedy} to generate from it`);
|
|
2372
2404
|
}
|
|
2373
2405
|
const resolvedPlugins = plugins ?? config.plugins;
|
|
2374
|
-
|
|
2375
|
-
const detach = [setupHookListener(this.#hooks, root), setupEventsStream(ws, this.#hooks, data.jobId, {
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
generatedFiles = files;
|
|
2379
|
-
}
|
|
2380
|
-
})];
|
|
2406
|
+
this.#lastGeneration = void 0;
|
|
2407
|
+
const detach = [setupHookListener(this.#hooks, root), setupEventsStream(ws, this.#hooks, data.jobId, { onGenerationEnd: (result) => {
|
|
2408
|
+
this.#lastGeneration = result;
|
|
2409
|
+
} })];
|
|
2381
2410
|
try {
|
|
2382
2411
|
await generate({
|
|
2383
2412
|
config: {
|
|
@@ -2398,7 +2427,6 @@ var StudioSession = class {
|
|
|
2398
2427
|
});
|
|
2399
2428
|
} finally {
|
|
2400
2429
|
for (const remove of detach) remove();
|
|
2401
|
-
this.#lastGeneration = generatedFiles;
|
|
2402
2430
|
}
|
|
2403
2431
|
await this.#hooks.callHook("studio:command:end", {
|
|
2404
2432
|
command,
|
|
@@ -2481,23 +2509,39 @@ var StudioSession = class {
|
|
|
2481
2509
|
refuse("a sandbox agent has no project to build a package from");
|
|
2482
2510
|
return;
|
|
2483
2511
|
}
|
|
2484
|
-
const { name, version,
|
|
2512
|
+
const { name, version, bundledDependencies, uploadPath } = data.payload;
|
|
2485
2513
|
if (!name || !version || !uploadPath) {
|
|
2486
2514
|
await this.#warn("Ignored snapshot: the message was missing required fields");
|
|
2487
2515
|
refuse("the message was missing required fields");
|
|
2488
2516
|
return;
|
|
2489
2517
|
}
|
|
2490
|
-
const
|
|
2491
|
-
if (!
|
|
2518
|
+
const generation = this.#lastGeneration;
|
|
2519
|
+
if (!generation) {
|
|
2492
2520
|
await this.#warn("Ignored snapshot: no prior generation to pack");
|
|
2493
2521
|
refuse("no prior generation exists to pack, run a generation first");
|
|
2494
2522
|
return;
|
|
2495
2523
|
}
|
|
2524
|
+
const bundled = new Set(bundledDependencies ?? []);
|
|
2525
|
+
const missing = generation.missingDependencies.filter((dependency) => !bundled.has(dependency));
|
|
2526
|
+
if (missing.length) {
|
|
2527
|
+
await this.#warn(`Ignored snapshot: missing dependencies: ${missing.join(", ")}`);
|
|
2528
|
+
refuse(`missing dependencies: ${missing.join(", ")}`);
|
|
2529
|
+
return;
|
|
2530
|
+
}
|
|
2496
2531
|
try {
|
|
2532
|
+
const files = {};
|
|
2533
|
+
await inParallel({
|
|
2534
|
+
items: [...generation.paths],
|
|
2535
|
+
limit: FILE_READ_CONCURRENCY,
|
|
2536
|
+
run: async (relativePath) => {
|
|
2537
|
+
const content = await generation.storage.readItem(absoluteStoragePath(generation.root, relativePath));
|
|
2538
|
+
if (content !== null) files[relativePath] = content;
|
|
2539
|
+
}
|
|
2540
|
+
});
|
|
2497
2541
|
const { bytes, integrity } = await createSnapshotPackage(files, {
|
|
2498
2542
|
name,
|
|
2499
2543
|
version,
|
|
2500
|
-
peerDependencies: peerDependencies
|
|
2544
|
+
peerDependencies: generation.peerDependencies
|
|
2501
2545
|
});
|
|
2502
2546
|
const { token, studioUrl } = this.#options;
|
|
2503
2547
|
const redirect = await fetch(new URL(uploadPath, studioUrl), {
|
|
@@ -2517,7 +2561,8 @@ var StudioSession = class {
|
|
|
2517
2561
|
jobId: data.jobId,
|
|
2518
2562
|
payload: {
|
|
2519
2563
|
status: "ok",
|
|
2520
|
-
integrity
|
|
2564
|
+
integrity,
|
|
2565
|
+
peerDependencies: generation.peerDependencies
|
|
2521
2566
|
}
|
|
2522
2567
|
});
|
|
2523
2568
|
await this.#hooks.callHook("studio:command:end", {
|
|
@@ -2529,6 +2574,61 @@ var StudioSession = class {
|
|
|
2529
2574
|
refuse(getErrorMessage(error));
|
|
2530
2575
|
}
|
|
2531
2576
|
}
|
|
2577
|
+
async #handleFiles(ws, data, command) {
|
|
2578
|
+
const { client } = this.#options;
|
|
2579
|
+
const refuse = (message) => sendAgentMessage(ws, {
|
|
2580
|
+
type: "agent:files",
|
|
2581
|
+
jobId: data.jobId,
|
|
2582
|
+
payload: {
|
|
2583
|
+
status: "error",
|
|
2584
|
+
message
|
|
2585
|
+
}
|
|
2586
|
+
});
|
|
2587
|
+
if (!this.#canRead) {
|
|
2588
|
+
await this.#warn("Ignored files: reading generated files was not granted");
|
|
2589
|
+
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`);
|
|
2590
|
+
return;
|
|
2591
|
+
}
|
|
2592
|
+
if (!Array.isArray(data.payload?.paths)) {
|
|
2593
|
+
await this.#warn("Ignored files: the message carried no paths");
|
|
2594
|
+
refuse("the message carried no paths");
|
|
2595
|
+
return;
|
|
2596
|
+
}
|
|
2597
|
+
const { paths } = data.payload;
|
|
2598
|
+
if (paths.length > 50) {
|
|
2599
|
+
await this.#warn(`Ignored files: requested ${paths.length} paths, more than the 50 allowed per request`);
|
|
2600
|
+
refuse(`at most 50 paths may be requested at once`);
|
|
2601
|
+
return;
|
|
2602
|
+
}
|
|
2603
|
+
const generation = this.#lastGeneration;
|
|
2604
|
+
if (!generation) {
|
|
2605
|
+
await this.#warn("Ignored files: no prior generation to read from");
|
|
2606
|
+
refuse("no prior generation to read from, run a generation first");
|
|
2607
|
+
return;
|
|
2608
|
+
}
|
|
2609
|
+
const requested = paths.filter((path) => generation.paths.has(path));
|
|
2610
|
+
const files = {};
|
|
2611
|
+
await inParallel({
|
|
2612
|
+
items: requested,
|
|
2613
|
+
limit: FILE_READ_CONCURRENCY,
|
|
2614
|
+
run: async (path) => {
|
|
2615
|
+
const content = await generation.storage.readItem(absoluteStoragePath(generation.root, path));
|
|
2616
|
+
if (content !== null) files[path] = content;
|
|
2617
|
+
}
|
|
2618
|
+
});
|
|
2619
|
+
sendAgentMessage(ws, {
|
|
2620
|
+
type: "agent:files",
|
|
2621
|
+
jobId: data.jobId,
|
|
2622
|
+
payload: {
|
|
2623
|
+
status: "ok",
|
|
2624
|
+
files
|
|
2625
|
+
}
|
|
2626
|
+
});
|
|
2627
|
+
await this.#hooks.callHook("studio:command:end", {
|
|
2628
|
+
command,
|
|
2629
|
+
info: `read ${Object.keys(files).length}/${paths.length} requested file${paths.length === 1 ? "" : "s"}`
|
|
2630
|
+
});
|
|
2631
|
+
}
|
|
2532
2632
|
};
|
|
2533
2633
|
//#endregion
|
|
2534
2634
|
//#region src/client.ts
|