@kubb/studio 5.3.2 → 5.3.4
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 +4 -0
- package/dist/index.cjs +247 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +11 -2
- package/dist/index.js +251 -21
- package/dist/index.js.map +1 -1
- package/dist/protocol.cjs +2 -1
- package/dist/protocol.cjs.map +1 -1
- package/dist/protocol.d.ts +39 -3
- package/dist/protocol.js +2 -1
- package/dist/protocol.js.map +1 -1
- package/package.json +4 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { t as __name } from "./rolldown-runtime-CRm0XQPb.js";
|
|
2
2
|
import { AgentPermissions, ClientInfo, createJobId } from "./protocol.js";
|
|
3
|
-
import { Storage } from "unstorage";
|
|
4
3
|
import { Config, Hookable, KubbHooks } from "@kubb/core";
|
|
4
|
+
import { Storage } from "unstorage";
|
|
5
5
|
//#region src/api.d.ts
|
|
6
6
|
/**
|
|
7
7
|
* Thrown when Studio rejects the agent token itself (401). Retrying cannot help: the token was
|
|
@@ -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.
|
|
@@ -197,6 +199,13 @@ type StudioSessionOptions = {
|
|
|
197
199
|
* default to.
|
|
198
200
|
*/
|
|
199
201
|
installLogger?: (hooks: Hookable<KubbHooks>) => void | Promise<void>;
|
|
202
|
+
/**
|
|
203
|
+
* Threshold for the reconnect loop's own `console.error` lines, using the numeric constants
|
|
204
|
+
* `@kubb/core` exports as `logLevel`. Left out, those lines never print, the same silent default
|
|
205
|
+
* as an unset `installLogger` — a reconnect happens outside any one session's hooks, so it has no
|
|
206
|
+
* other way to ask a host how loud to be.
|
|
207
|
+
*/
|
|
208
|
+
logLevel?: number;
|
|
200
209
|
/**
|
|
201
210
|
* Called when this session's background reconnect is rejected with an invalid token. Unlike
|
|
202
211
|
* `ClientOptions.onAuthRequired`, this fires once per session rather than once per pool:
|
package/dist/index.js
CHANGED
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
import { t as __name } from "./rolldown-runtime-CRm0XQPb.js";
|
|
2
2
|
import { createJobId, isCommandMessage, isDisconnectMessage, isStudioPongMessage, isStudioReadyMessage } from "./protocol.js";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
|
-
import { styleText } from "node:util";
|
|
4
|
+
import { promisify, styleText } from "node:util";
|
|
5
5
|
import process$1 from "node:process";
|
|
6
6
|
import { spawn } from "node:child_process";
|
|
7
|
-
import { readFile, writeFile } from "node:fs/promises";
|
|
8
|
-
import path, { isAbsolute, relative, resolve } from "node:path";
|
|
7
|
+
import { glob, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
8
|
+
import path, { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
9
|
+
import { Diagnostics, Hookable, createKubb, fsStorage, logLevel, memoryStorage } from "@kubb/core";
|
|
9
10
|
import { FetchError, ofetch } from "ofetch";
|
|
10
|
-
import { hash, randomBytes } from "node:crypto";
|
|
11
|
+
import { createHash, hash, randomBytes } from "node:crypto";
|
|
11
12
|
import { createStorage } from "unstorage";
|
|
12
13
|
import fsDriver from "unstorage/drivers/fs";
|
|
13
|
-
import { Diagnostics, Hookable, createKubb, fsStorage, memoryStorage } from "@kubb/core";
|
|
14
14
|
import { x } from "tinyexec";
|
|
15
15
|
import { builders, detectCodeFormat, generateCode, parseModule } from "magicast";
|
|
16
16
|
import { existsSync } from "node:fs";
|
|
17
17
|
import { pathToFileURL } from "node:url";
|
|
18
18
|
import { mergeDeep } from "remeda";
|
|
19
|
+
import { tmpdir } from "node:os";
|
|
20
|
+
import { gzip } from "node:zlib";
|
|
21
|
+
import { build } from "tsdown";
|
|
19
22
|
import WebSocket from "ws";
|
|
20
23
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
21
24
|
//#region src/constants.ts
|
|
@@ -556,19 +559,20 @@ async function runRegistration({ token, studioUrl, poolSize }) {
|
|
|
556
559
|
* Called on process termination or server close. A failed notify is logged and swallowed: the
|
|
557
560
|
* local socket is already gone, and failing teardown must not block shutdown or reconnect.
|
|
558
561
|
*/
|
|
559
|
-
async function disconnect({ sessionId, token, studioUrl, slug }) {
|
|
562
|
+
async function disconnect({ sessionId, token, studioUrl, slug, logLevel: logLevel$2 }) {
|
|
560
563
|
const url = `${studioUrl}/api/agent/sessions/${sessionId}/disconnect`;
|
|
561
564
|
const tag = slug ?? "agent";
|
|
565
|
+
const canLog = logLevel$2 !== void 0 && logLevel$2 > logLevel.silent;
|
|
562
566
|
try {
|
|
563
567
|
await ofetch(url, {
|
|
564
568
|
method: "POST",
|
|
565
569
|
headers: { Authorization: `Bearer ${token}` }
|
|
566
570
|
});
|
|
567
|
-
console.
|
|
571
|
+
if (canLog) console.error(styleText("green", `[${tag}] Disconnected from Studio`));
|
|
568
572
|
} catch (error) {
|
|
569
573
|
const statusCode = error?.statusCode;
|
|
570
574
|
if (statusCode !== void 0 && statusCode >= 400 && statusCode < 500) return;
|
|
571
|
-
console.warn(styleText("yellow", `[${tag}] Failed to notify Studio of disconnection: ${getErrorMessage(error)}`));
|
|
575
|
+
if (canLog) console.warn(styleText("yellow", `[${tag}] Failed to notify Studio of disconnection: ${getErrorMessage(error)}`));
|
|
572
576
|
}
|
|
573
577
|
}
|
|
574
578
|
/**
|
|
@@ -605,18 +609,41 @@ async function createJob({ studioUrl, token, type, agentId, name, version, confi
|
|
|
605
609
|
return job;
|
|
606
610
|
}
|
|
607
611
|
/**
|
|
608
|
-
*
|
|
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.
|
|
609
624
|
*
|
|
610
625
|
* A `failed` job resolves normally. Check `job.status` and `job.error`. Throws only when the
|
|
611
626
|
* deadline passes before Studio finishes.
|
|
612
627
|
*/
|
|
613
628
|
async function waitForJob({ studioUrl, token, id, timeoutMs = 6e4 }) {
|
|
614
629
|
const deadline = Date.now() + timeoutMs;
|
|
630
|
+
let interval = INITIAL_POLL_DELAY_MS;
|
|
615
631
|
for (;;) {
|
|
616
|
-
|
|
617
|
-
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)));
|
|
618
633
|
if (Date.now() >= deadline) throw new Error("Timed out waiting for the Studio job");
|
|
619
|
-
|
|
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
|
+
}
|
|
620
647
|
}
|
|
621
648
|
}
|
|
622
649
|
/**
|
|
@@ -646,7 +673,7 @@ async function createAgent({ studioUrl, token, name, machineToken }) {
|
|
|
646
673
|
}
|
|
647
674
|
//#endregion
|
|
648
675
|
//#region package.json
|
|
649
|
-
var version = "5.3.
|
|
676
|
+
var version = "5.3.4";
|
|
650
677
|
//#endregion
|
|
651
678
|
//#region src/hooks.ts
|
|
652
679
|
/**
|
|
@@ -1579,6 +1606,134 @@ async function generate({ config, hooks }) {
|
|
|
1579
1606
|
}
|
|
1580
1607
|
}
|
|
1581
1608
|
//#endregion
|
|
1609
|
+
//#region src/snapshotPackage.ts
|
|
1610
|
+
const gzipAsync = promisify(gzip);
|
|
1611
|
+
/**
|
|
1612
|
+
* Maps a generated file's path to its place inside the tarball, stripping everything before a
|
|
1613
|
+
* `src`/`dist` segment and any `..`/empty path segment so a crafted file name cannot escape the
|
|
1614
|
+
* `package/` root.
|
|
1615
|
+
*/
|
|
1616
|
+
function packagePath(filePath) {
|
|
1617
|
+
const normalized = filePath.replaceAll("\\", "/");
|
|
1618
|
+
return `package/${(normalized.match(/\/(?:src|dist)\/.*$/)?.[0].slice(1) ?? normalized.replace(/^\/+/, "")).split("/").filter((part) => part && part !== "." && part !== "..").join("/")}`;
|
|
1619
|
+
}
|
|
1620
|
+
/**
|
|
1621
|
+
* Splits a tarball entry path into the legacy 100-byte `name` field and, when the path does not
|
|
1622
|
+
* fit, the 155-byte USTAR `prefix` field that extends it. Throws rather than silently truncating
|
|
1623
|
+
* a path the format cannot address (max 256 bytes: 100 name + 1 separator + 155 prefix).
|
|
1624
|
+
*/
|
|
1625
|
+
function splitEntryPath(path) {
|
|
1626
|
+
if (Buffer.byteLength(path, "utf8") <= 100) return {
|
|
1627
|
+
name: path,
|
|
1628
|
+
prefix: ""
|
|
1629
|
+
};
|
|
1630
|
+
for (let i = path.length - 1; i >= 0; i--) {
|
|
1631
|
+
if (path[i] !== "/") continue;
|
|
1632
|
+
const prefix = path.slice(0, i);
|
|
1633
|
+
const name = path.slice(i + 1);
|
|
1634
|
+
if (Buffer.byteLength(prefix, "utf8") <= 155 && Buffer.byteLength(name, "utf8") <= 100) return {
|
|
1635
|
+
name,
|
|
1636
|
+
prefix
|
|
1637
|
+
};
|
|
1638
|
+
}
|
|
1639
|
+
throw new Error(`Snapshot path is too long for a tar entry: ${path}`);
|
|
1640
|
+
}
|
|
1641
|
+
function header(path, size) {
|
|
1642
|
+
const { name, prefix } = splitEntryPath(path);
|
|
1643
|
+
const value = Buffer.alloc(512);
|
|
1644
|
+
value.write(name, 0, "utf8");
|
|
1645
|
+
value.write("0000644\0", 100, "ascii");
|
|
1646
|
+
value.write("0000000\0", 108, "ascii");
|
|
1647
|
+
value.write("0000000\0", 116, "ascii");
|
|
1648
|
+
value.write(`${size.toString(8).padStart(11, "0")}\0`, 124, "ascii");
|
|
1649
|
+
value.write(`${Math.floor(Date.now() / 1e3).toString(8).padStart(11, "0")}\0`, 136, "ascii");
|
|
1650
|
+
value.fill(32, 148, 156);
|
|
1651
|
+
value.write("0", 156, "ascii");
|
|
1652
|
+
value.write("ustar\0", 257, "ascii");
|
|
1653
|
+
value.write("00", 263, "ascii");
|
|
1654
|
+
value.write("0000000\0", 265, "ascii");
|
|
1655
|
+
value.write("0000000\0", 297, "ascii");
|
|
1656
|
+
value.write(prefix, 345, "utf8");
|
|
1657
|
+
const checksum = [...value].reduce((sum, byte) => sum + byte, 0);
|
|
1658
|
+
value.write(`${checksum.toString(8).padStart(6, "0")}\0 `, 148, "ascii");
|
|
1659
|
+
return value;
|
|
1660
|
+
}
|
|
1661
|
+
/**
|
|
1662
|
+
* Packs a generation's files into a gzipped, npm-installable tarball: a `package/` root with the
|
|
1663
|
+
* generated sources, a `tsdown`-built `dist/` (esm + cjs), and a `package.json` manifest.
|
|
1664
|
+
*/
|
|
1665
|
+
async function createSnapshotPackage(files, packageInfo) {
|
|
1666
|
+
const root = await mkdtemp(join(tmpdir(), "kubb-snapshot-"));
|
|
1667
|
+
const dist = join(root, "dist");
|
|
1668
|
+
const resolvedPaths = Object.entries(files).map(([name, content]) => ({
|
|
1669
|
+
name,
|
|
1670
|
+
content,
|
|
1671
|
+
target: packagePath(name)
|
|
1672
|
+
}));
|
|
1673
|
+
const targetOwners = /* @__PURE__ */ new Map();
|
|
1674
|
+
for (const { name, target } of resolvedPaths) {
|
|
1675
|
+
const owner = targetOwners.get(target);
|
|
1676
|
+
if (owner) throw new Error(`Snapshot has two generated files that sanitize to the same path "${target}": "${owner}" and "${name}"`);
|
|
1677
|
+
targetOwners.set(target, name);
|
|
1678
|
+
}
|
|
1679
|
+
try {
|
|
1680
|
+
await mkdir(dist);
|
|
1681
|
+
await Promise.all(resolvedPaths.map(async ({ content, target }) => {
|
|
1682
|
+
const path = join(root, target.slice(8));
|
|
1683
|
+
await mkdir(join(path, ".."), { recursive: true });
|
|
1684
|
+
await writeFile(path, content);
|
|
1685
|
+
}));
|
|
1686
|
+
const sourceEntries = resolvedPaths.filter(({ name }) => /\.(?:[cm]?[jt]sx?)$/.test(name)).map(({ target }) => join(root, target.slice(8)));
|
|
1687
|
+
if (sourceEntries.length) await build({
|
|
1688
|
+
entry: sourceEntries,
|
|
1689
|
+
outDir: dist,
|
|
1690
|
+
format: ["esm", "cjs"],
|
|
1691
|
+
dts: false,
|
|
1692
|
+
sourcemap: false,
|
|
1693
|
+
unbundle: true,
|
|
1694
|
+
report: false,
|
|
1695
|
+
logLevel: "silent",
|
|
1696
|
+
fixedExtension: true
|
|
1697
|
+
});
|
|
1698
|
+
const builtEntries = await Promise.all((await Array.fromAsync(glob("**/*", {
|
|
1699
|
+
cwd: dist,
|
|
1700
|
+
withFileTypes: true
|
|
1701
|
+
}))).filter((entry) => entry.isFile()).map(async (entry) => {
|
|
1702
|
+
const filePath = join(entry.parentPath, entry.name);
|
|
1703
|
+
return [`package/dist/${relative(dist, filePath).split(sep).join("/")}`, await readFile(filePath, "utf8")];
|
|
1704
|
+
}));
|
|
1705
|
+
const entries = {
|
|
1706
|
+
"package/package.json": JSON.stringify({
|
|
1707
|
+
...packageInfo,
|
|
1708
|
+
type: "module",
|
|
1709
|
+
main: "./dist/index.cjs",
|
|
1710
|
+
module: "./dist/index.mjs",
|
|
1711
|
+
exports: { ".": {
|
|
1712
|
+
import: "./dist/index.mjs",
|
|
1713
|
+
require: "./dist/index.cjs"
|
|
1714
|
+
} }
|
|
1715
|
+
}, null, 2),
|
|
1716
|
+
...Object.fromEntries(resolvedPaths.map(({ content, target }) => [target, content])),
|
|
1717
|
+
...Object.fromEntries(builtEntries)
|
|
1718
|
+
};
|
|
1719
|
+
const chunks = [];
|
|
1720
|
+
for (const [name, content] of Object.entries(entries)) {
|
|
1721
|
+
const bytes = Buffer.from(content);
|
|
1722
|
+
chunks.push(header(name, bytes.length), bytes, Buffer.alloc((512 - bytes.length % 512) % 512));
|
|
1723
|
+
}
|
|
1724
|
+
const bytes = await gzipAsync(Buffer.concat([...chunks, Buffer.alloc(1024)]));
|
|
1725
|
+
return {
|
|
1726
|
+
bytes,
|
|
1727
|
+
integrity: `sha512-${createHash("sha512").update(bytes).digest("base64")}`
|
|
1728
|
+
};
|
|
1729
|
+
} finally {
|
|
1730
|
+
await rm(root, {
|
|
1731
|
+
recursive: true,
|
|
1732
|
+
force: true
|
|
1733
|
+
});
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
//#endregion
|
|
1582
1737
|
//#region src/ws.ts
|
|
1583
1738
|
/**
|
|
1584
1739
|
* How many generated files are read from storage at once when building the
|
|
@@ -1674,7 +1829,7 @@ function sendErrorMessage(ws, error, jobId) {
|
|
|
1674
1829
|
/**
|
|
1675
1830
|
* Forwards selected Kubb lifecycle events to Studio as data messages for the active session.
|
|
1676
1831
|
*/
|
|
1677
|
-
function setupEventsStream(ws, hooks, jobId) {
|
|
1832
|
+
function setupEventsStream(ws, hooks, jobId, options = {}) {
|
|
1678
1833
|
const unhooks = [];
|
|
1679
1834
|
/**
|
|
1680
1835
|
* Registers a listener and keeps its remover, so one generation's listeners come off the session
|
|
@@ -1788,11 +1943,12 @@ function setupEventsStream(ws, hooks, jobId) {
|
|
|
1788
1943
|
if (content !== null) files[relativeStoragePath(config.root, path)] = content;
|
|
1789
1944
|
}
|
|
1790
1945
|
});
|
|
1946
|
+
options.onGenerationEnd?.(files);
|
|
1791
1947
|
sendDataMessage({
|
|
1792
1948
|
type: "kubb:generation:end",
|
|
1793
1949
|
data: [{
|
|
1794
1950
|
config,
|
|
1795
|
-
storage: files,
|
|
1951
|
+
storage: options.skipStorage ? {} : files,
|
|
1796
1952
|
peerDependencies,
|
|
1797
1953
|
missingDependencies
|
|
1798
1954
|
}]
|
|
@@ -1909,15 +2065,15 @@ function applyStudioDefaults(options) {
|
|
|
1909
2065
|
* socket, its hook emitter, or its session id alive for the length of the retry interval.
|
|
1910
2066
|
*/
|
|
1911
2067
|
function reconnect(options) {
|
|
1912
|
-
const { signal, retryInterval, onTokenRejected } = options;
|
|
2068
|
+
const { signal, retryInterval, onTokenRejected, logLevel: logLevel$1 } = options;
|
|
1913
2069
|
if (signal?.aborted) return;
|
|
1914
|
-
console.
|
|
2070
|
+
if (logLevel$1 !== void 0 && logLevel$1 > logLevel.silent) console.error(styleText("dim", `Retrying connection in ${retryInterval}ms to Kubb Studio ...`));
|
|
1915
2071
|
const cancel = () => clearTimeout(timer);
|
|
1916
2072
|
const timer = setTimeout(() => {
|
|
1917
2073
|
signal?.removeEventListener("abort", cancel);
|
|
1918
2074
|
if (signal?.aborted) return;
|
|
1919
2075
|
new StudioSession(options).connect().catch((error) => {
|
|
1920
|
-
console.error(styleText("red", `Reconnect attempt to Kubb Studio failed: ${getErrorMessage(error)}`));
|
|
2076
|
+
if (logLevel$1 !== void 0 && logLevel$1 > logLevel.silent) console.error(styleText("red", `Reconnect attempt to Kubb Studio failed: ${getErrorMessage(error)}`));
|
|
1921
2077
|
if (error instanceof InvalidAgentTokenError) {
|
|
1922
2078
|
onTokenRejected?.(error);
|
|
1923
2079
|
return;
|
|
@@ -1952,6 +2108,7 @@ var StudioSession = class {
|
|
|
1952
2108
|
#heartbeatTimer;
|
|
1953
2109
|
#readyTimer;
|
|
1954
2110
|
#lastPongAt = Date.now();
|
|
2111
|
+
#lastGeneration;
|
|
1955
2112
|
constructor(options) {
|
|
1956
2113
|
this.#options = applyStudioDefaults(options);
|
|
1957
2114
|
}
|
|
@@ -2134,7 +2291,7 @@ var StudioSession = class {
|
|
|
2134
2291
|
* `#disposed` keeps the close event from running this twice, and a shutdown from reconnecting.
|
|
2135
2292
|
*/
|
|
2136
2293
|
async #end({ reason, retry }) {
|
|
2137
|
-
const { studioUrl, token } = this.#options;
|
|
2294
|
+
const { studioUrl, token, logLevel } = this.#options;
|
|
2138
2295
|
if (this.#disposed) return;
|
|
2139
2296
|
this.#disposed = true;
|
|
2140
2297
|
if (reason === "shutdown" && this.#ws) sendAgentMessage(this.#ws, {
|
|
@@ -2146,7 +2303,8 @@ var StudioSession = class {
|
|
|
2146
2303
|
sessionId: this.#session.sessionId,
|
|
2147
2304
|
studioUrl,
|
|
2148
2305
|
token,
|
|
2149
|
-
slug: this.#session.slug
|
|
2306
|
+
slug: this.#session.slug,
|
|
2307
|
+
logLevel
|
|
2150
2308
|
}).catch(() => {});
|
|
2151
2309
|
if (retry) reconnect(this.#options);
|
|
2152
2310
|
}
|
|
@@ -2205,6 +2363,9 @@ var StudioSession = class {
|
|
|
2205
2363
|
case "studio:save":
|
|
2206
2364
|
await this.#handleSave(ws, data, command);
|
|
2207
2365
|
return;
|
|
2366
|
+
case "studio:snapshot":
|
|
2367
|
+
await this.#handleSnapshot(ws, data, command);
|
|
2368
|
+
return;
|
|
2208
2369
|
}
|
|
2209
2370
|
}
|
|
2210
2371
|
async #handleGenerate(ws, data, command) {
|
|
@@ -2228,7 +2389,13 @@ var StudioSession = class {
|
|
|
2228
2389
|
await this.#warn(`Ignored the spec from Studio; set ${remedy} to generate from it`);
|
|
2229
2390
|
}
|
|
2230
2391
|
const resolvedPlugins = plugins ?? config.plugins;
|
|
2231
|
-
|
|
2392
|
+
let generatedFiles;
|
|
2393
|
+
const detach = [setupHookListener(this.#hooks, root), setupEventsStream(ws, this.#hooks, data.jobId, {
|
|
2394
|
+
skipStorage: client?.kind === "ci",
|
|
2395
|
+
onGenerationEnd: (files) => {
|
|
2396
|
+
generatedFiles = files;
|
|
2397
|
+
}
|
|
2398
|
+
})];
|
|
2232
2399
|
try {
|
|
2233
2400
|
await generate({
|
|
2234
2401
|
config: {
|
|
@@ -2249,6 +2416,7 @@ var StudioSession = class {
|
|
|
2249
2416
|
});
|
|
2250
2417
|
} finally {
|
|
2251
2418
|
for (const remove of detach) remove();
|
|
2419
|
+
this.#lastGeneration = generatedFiles;
|
|
2252
2420
|
}
|
|
2253
2421
|
await this.#hooks.callHook("studio:command:end", {
|
|
2254
2422
|
command,
|
|
@@ -2317,6 +2485,68 @@ var StudioSession = class {
|
|
|
2317
2485
|
refuse(getErrorMessage(error));
|
|
2318
2486
|
}
|
|
2319
2487
|
}
|
|
2488
|
+
async #handleSnapshot(ws, data, command) {
|
|
2489
|
+
const refuse = (message) => sendAgentMessage(ws, {
|
|
2490
|
+
type: "agent:snapshot",
|
|
2491
|
+
jobId: data.jobId,
|
|
2492
|
+
payload: {
|
|
2493
|
+
status: "error",
|
|
2494
|
+
message
|
|
2495
|
+
}
|
|
2496
|
+
});
|
|
2497
|
+
if (this.#isSandbox) {
|
|
2498
|
+
await this.#warn("Ignored snapshot: a sandbox agent has no project to build a package from");
|
|
2499
|
+
refuse("a sandbox agent has no project to build a package from");
|
|
2500
|
+
return;
|
|
2501
|
+
}
|
|
2502
|
+
const { name, version, peerDependencies, uploadPath } = data.payload;
|
|
2503
|
+
if (!name || !version || !uploadPath) {
|
|
2504
|
+
await this.#warn("Ignored snapshot: the message was missing required fields");
|
|
2505
|
+
refuse("the message was missing required fields");
|
|
2506
|
+
return;
|
|
2507
|
+
}
|
|
2508
|
+
const files = this.#lastGeneration;
|
|
2509
|
+
if (!files) {
|
|
2510
|
+
await this.#warn("Ignored snapshot: no prior generation to pack");
|
|
2511
|
+
refuse("no prior generation exists to pack, run a generation first");
|
|
2512
|
+
return;
|
|
2513
|
+
}
|
|
2514
|
+
try {
|
|
2515
|
+
const { bytes, integrity } = await createSnapshotPackage(files, {
|
|
2516
|
+
name,
|
|
2517
|
+
version,
|
|
2518
|
+
peerDependencies: peerDependencies ?? {}
|
|
2519
|
+
});
|
|
2520
|
+
const { token, studioUrl } = this.#options;
|
|
2521
|
+
const redirect = await fetch(new URL(uploadPath, studioUrl), {
|
|
2522
|
+
method: "PUT",
|
|
2523
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
2524
|
+
redirect: "manual"
|
|
2525
|
+
});
|
|
2526
|
+
const storageUrl = redirect.headers.get("location");
|
|
2527
|
+
if (redirect.status !== 307 || !storageUrl) throw new Error(`Studio did not provide a storage URL (status ${redirect.status})`);
|
|
2528
|
+
const response = await fetch(storageUrl, {
|
|
2529
|
+
method: "PUT",
|
|
2530
|
+
body: new Uint8Array(bytes)
|
|
2531
|
+
});
|
|
2532
|
+
if (!response.ok) throw new Error(`Snapshot upload failed with status ${response.status}`);
|
|
2533
|
+
sendAgentMessage(ws, {
|
|
2534
|
+
type: "agent:snapshot",
|
|
2535
|
+
jobId: data.jobId,
|
|
2536
|
+
payload: {
|
|
2537
|
+
status: "ok",
|
|
2538
|
+
integrity
|
|
2539
|
+
}
|
|
2540
|
+
});
|
|
2541
|
+
await this.#hooks.callHook("studio:command:end", {
|
|
2542
|
+
command,
|
|
2543
|
+
info: `packed ${Object.keys(files).length} file${Object.keys(files).length === 1 ? "" : "s"}`
|
|
2544
|
+
});
|
|
2545
|
+
} catch (error) {
|
|
2546
|
+
await this.#hooks.callHook("studio:error", { error: toError(error) });
|
|
2547
|
+
refuse(getErrorMessage(error));
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2320
2550
|
};
|
|
2321
2551
|
//#endregion
|
|
2322
2552
|
//#region src/client.ts
|