@kubb/studio 5.3.2 → 5.3.3
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 +220 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +224 -17
- 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
|
|
@@ -197,6 +197,13 @@ type StudioSessionOptions = {
|
|
|
197
197
|
* default to.
|
|
198
198
|
*/
|
|
199
199
|
installLogger?: (hooks: Hookable<KubbHooks>) => void | Promise<void>;
|
|
200
|
+
/**
|
|
201
|
+
* Threshold for the reconnect loop's own `console.error` lines, using the numeric constants
|
|
202
|
+
* `@kubb/core` exports as `logLevel`. Left out, those lines never print, the same silent default
|
|
203
|
+
* as an unset `installLogger` — a reconnect happens outside any one session's hooks, so it has no
|
|
204
|
+
* other way to ask a host how loud to be.
|
|
205
|
+
*/
|
|
206
|
+
logLevel?: number;
|
|
200
207
|
/**
|
|
201
208
|
* Called when this session's background reconnect is rejected with an invalid token. Unlike
|
|
202
209
|
* `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
|
/**
|
|
@@ -646,7 +650,7 @@ async function createAgent({ studioUrl, token, name, machineToken }) {
|
|
|
646
650
|
}
|
|
647
651
|
//#endregion
|
|
648
652
|
//#region package.json
|
|
649
|
-
var version = "5.3.
|
|
653
|
+
var version = "5.3.3";
|
|
650
654
|
//#endregion
|
|
651
655
|
//#region src/hooks.ts
|
|
652
656
|
/**
|
|
@@ -1579,6 +1583,134 @@ async function generate({ config, hooks }) {
|
|
|
1579
1583
|
}
|
|
1580
1584
|
}
|
|
1581
1585
|
//#endregion
|
|
1586
|
+
//#region src/snapshotPackage.ts
|
|
1587
|
+
const gzipAsync = promisify(gzip);
|
|
1588
|
+
/**
|
|
1589
|
+
* Maps a generated file's path to its place inside the tarball, stripping everything before a
|
|
1590
|
+
* `src`/`dist` segment and any `..`/empty path segment so a crafted file name cannot escape the
|
|
1591
|
+
* `package/` root.
|
|
1592
|
+
*/
|
|
1593
|
+
function packagePath(filePath) {
|
|
1594
|
+
const normalized = filePath.replaceAll("\\", "/");
|
|
1595
|
+
return `package/${(normalized.match(/\/(?:src|dist)\/.*$/)?.[0].slice(1) ?? normalized.replace(/^\/+/, "")).split("/").filter((part) => part && part !== "." && part !== "..").join("/")}`;
|
|
1596
|
+
}
|
|
1597
|
+
/**
|
|
1598
|
+
* Splits a tarball entry path into the legacy 100-byte `name` field and, when the path does not
|
|
1599
|
+
* fit, the 155-byte USTAR `prefix` field that extends it. Throws rather than silently truncating
|
|
1600
|
+
* a path the format cannot address (max 256 bytes: 100 name + 1 separator + 155 prefix).
|
|
1601
|
+
*/
|
|
1602
|
+
function splitEntryPath(path) {
|
|
1603
|
+
if (Buffer.byteLength(path, "utf8") <= 100) return {
|
|
1604
|
+
name: path,
|
|
1605
|
+
prefix: ""
|
|
1606
|
+
};
|
|
1607
|
+
for (let i = path.length - 1; i >= 0; i--) {
|
|
1608
|
+
if (path[i] !== "/") continue;
|
|
1609
|
+
const prefix = path.slice(0, i);
|
|
1610
|
+
const name = path.slice(i + 1);
|
|
1611
|
+
if (Buffer.byteLength(prefix, "utf8") <= 155 && Buffer.byteLength(name, "utf8") <= 100) return {
|
|
1612
|
+
name,
|
|
1613
|
+
prefix
|
|
1614
|
+
};
|
|
1615
|
+
}
|
|
1616
|
+
throw new Error(`Snapshot path is too long for a tar entry: ${path}`);
|
|
1617
|
+
}
|
|
1618
|
+
function header(path, size) {
|
|
1619
|
+
const { name, prefix } = splitEntryPath(path);
|
|
1620
|
+
const value = Buffer.alloc(512);
|
|
1621
|
+
value.write(name, 0, "utf8");
|
|
1622
|
+
value.write("0000644\0", 100, "ascii");
|
|
1623
|
+
value.write("0000000\0", 108, "ascii");
|
|
1624
|
+
value.write("0000000\0", 116, "ascii");
|
|
1625
|
+
value.write(`${size.toString(8).padStart(11, "0")}\0`, 124, "ascii");
|
|
1626
|
+
value.write(`${Math.floor(Date.now() / 1e3).toString(8).padStart(11, "0")}\0`, 136, "ascii");
|
|
1627
|
+
value.fill(32, 148, 156);
|
|
1628
|
+
value.write("0", 156, "ascii");
|
|
1629
|
+
value.write("ustar\0", 257, "ascii");
|
|
1630
|
+
value.write("00", 263, "ascii");
|
|
1631
|
+
value.write("0000000\0", 265, "ascii");
|
|
1632
|
+
value.write("0000000\0", 297, "ascii");
|
|
1633
|
+
value.write(prefix, 345, "utf8");
|
|
1634
|
+
const checksum = [...value].reduce((sum, byte) => sum + byte, 0);
|
|
1635
|
+
value.write(`${checksum.toString(8).padStart(6, "0")}\0 `, 148, "ascii");
|
|
1636
|
+
return value;
|
|
1637
|
+
}
|
|
1638
|
+
/**
|
|
1639
|
+
* Packs a generation's files into a gzipped, npm-installable tarball: a `package/` root with the
|
|
1640
|
+
* generated sources, a `tsdown`-built `dist/` (esm + cjs), and a `package.json` manifest.
|
|
1641
|
+
*/
|
|
1642
|
+
async function createSnapshotPackage(files, packageInfo) {
|
|
1643
|
+
const root = await mkdtemp(join(tmpdir(), "kubb-snapshot-"));
|
|
1644
|
+
const dist = join(root, "dist");
|
|
1645
|
+
const resolvedPaths = Object.entries(files).map(([name, content]) => ({
|
|
1646
|
+
name,
|
|
1647
|
+
content,
|
|
1648
|
+
target: packagePath(name)
|
|
1649
|
+
}));
|
|
1650
|
+
const targetOwners = /* @__PURE__ */ new Map();
|
|
1651
|
+
for (const { name, target } of resolvedPaths) {
|
|
1652
|
+
const owner = targetOwners.get(target);
|
|
1653
|
+
if (owner) throw new Error(`Snapshot has two generated files that sanitize to the same path "${target}": "${owner}" and "${name}"`);
|
|
1654
|
+
targetOwners.set(target, name);
|
|
1655
|
+
}
|
|
1656
|
+
try {
|
|
1657
|
+
await mkdir(dist);
|
|
1658
|
+
await Promise.all(resolvedPaths.map(async ({ content, target }) => {
|
|
1659
|
+
const path = join(root, target.slice(8));
|
|
1660
|
+
await mkdir(join(path, ".."), { recursive: true });
|
|
1661
|
+
await writeFile(path, content);
|
|
1662
|
+
}));
|
|
1663
|
+
const sourceEntries = resolvedPaths.filter(({ name }) => /\.(?:[cm]?[jt]sx?)$/.test(name)).map(({ target }) => join(root, target.slice(8)));
|
|
1664
|
+
if (sourceEntries.length) await build({
|
|
1665
|
+
entry: sourceEntries,
|
|
1666
|
+
outDir: dist,
|
|
1667
|
+
format: ["esm", "cjs"],
|
|
1668
|
+
dts: false,
|
|
1669
|
+
sourcemap: false,
|
|
1670
|
+
unbundle: true,
|
|
1671
|
+
report: false,
|
|
1672
|
+
logLevel: "silent",
|
|
1673
|
+
fixedExtension: true
|
|
1674
|
+
});
|
|
1675
|
+
const builtEntries = await Promise.all((await Array.fromAsync(glob("**/*", {
|
|
1676
|
+
cwd: dist,
|
|
1677
|
+
withFileTypes: true
|
|
1678
|
+
}))).filter((entry) => entry.isFile()).map(async (entry) => {
|
|
1679
|
+
const filePath = join(entry.parentPath, entry.name);
|
|
1680
|
+
return [`package/dist/${relative(dist, filePath).split(sep).join("/")}`, await readFile(filePath, "utf8")];
|
|
1681
|
+
}));
|
|
1682
|
+
const entries = {
|
|
1683
|
+
"package/package.json": JSON.stringify({
|
|
1684
|
+
...packageInfo,
|
|
1685
|
+
type: "module",
|
|
1686
|
+
main: "./dist/index.cjs",
|
|
1687
|
+
module: "./dist/index.mjs",
|
|
1688
|
+
exports: { ".": {
|
|
1689
|
+
import: "./dist/index.mjs",
|
|
1690
|
+
require: "./dist/index.cjs"
|
|
1691
|
+
} }
|
|
1692
|
+
}, null, 2),
|
|
1693
|
+
...Object.fromEntries(resolvedPaths.map(({ content, target }) => [target, content])),
|
|
1694
|
+
...Object.fromEntries(builtEntries)
|
|
1695
|
+
};
|
|
1696
|
+
const chunks = [];
|
|
1697
|
+
for (const [name, content] of Object.entries(entries)) {
|
|
1698
|
+
const bytes = Buffer.from(content);
|
|
1699
|
+
chunks.push(header(name, bytes.length), bytes, Buffer.alloc((512 - bytes.length % 512) % 512));
|
|
1700
|
+
}
|
|
1701
|
+
const bytes = await gzipAsync(Buffer.concat([...chunks, Buffer.alloc(1024)]));
|
|
1702
|
+
return {
|
|
1703
|
+
bytes,
|
|
1704
|
+
integrity: `sha512-${createHash("sha512").update(bytes).digest("base64")}`
|
|
1705
|
+
};
|
|
1706
|
+
} finally {
|
|
1707
|
+
await rm(root, {
|
|
1708
|
+
recursive: true,
|
|
1709
|
+
force: true
|
|
1710
|
+
});
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
//#endregion
|
|
1582
1714
|
//#region src/ws.ts
|
|
1583
1715
|
/**
|
|
1584
1716
|
* How many generated files are read from storage at once when building the
|
|
@@ -1674,7 +1806,7 @@ function sendErrorMessage(ws, error, jobId) {
|
|
|
1674
1806
|
/**
|
|
1675
1807
|
* Forwards selected Kubb lifecycle events to Studio as data messages for the active session.
|
|
1676
1808
|
*/
|
|
1677
|
-
function setupEventsStream(ws, hooks, jobId) {
|
|
1809
|
+
function setupEventsStream(ws, hooks, jobId, options = {}) {
|
|
1678
1810
|
const unhooks = [];
|
|
1679
1811
|
/**
|
|
1680
1812
|
* Registers a listener and keeps its remover, so one generation's listeners come off the session
|
|
@@ -1788,11 +1920,12 @@ function setupEventsStream(ws, hooks, jobId) {
|
|
|
1788
1920
|
if (content !== null) files[relativeStoragePath(config.root, path)] = content;
|
|
1789
1921
|
}
|
|
1790
1922
|
});
|
|
1923
|
+
options.onGenerationEnd?.(files);
|
|
1791
1924
|
sendDataMessage({
|
|
1792
1925
|
type: "kubb:generation:end",
|
|
1793
1926
|
data: [{
|
|
1794
1927
|
config,
|
|
1795
|
-
storage: files,
|
|
1928
|
+
storage: options.skipStorage ? {} : files,
|
|
1796
1929
|
peerDependencies,
|
|
1797
1930
|
missingDependencies
|
|
1798
1931
|
}]
|
|
@@ -1909,15 +2042,15 @@ function applyStudioDefaults(options) {
|
|
|
1909
2042
|
* socket, its hook emitter, or its session id alive for the length of the retry interval.
|
|
1910
2043
|
*/
|
|
1911
2044
|
function reconnect(options) {
|
|
1912
|
-
const { signal, retryInterval, onTokenRejected } = options;
|
|
2045
|
+
const { signal, retryInterval, onTokenRejected, logLevel: logLevel$1 } = options;
|
|
1913
2046
|
if (signal?.aborted) return;
|
|
1914
|
-
console.
|
|
2047
|
+
if (logLevel$1 !== void 0 && logLevel$1 > logLevel.silent) console.error(styleText("dim", `Retrying connection in ${retryInterval}ms to Kubb Studio ...`));
|
|
1915
2048
|
const cancel = () => clearTimeout(timer);
|
|
1916
2049
|
const timer = setTimeout(() => {
|
|
1917
2050
|
signal?.removeEventListener("abort", cancel);
|
|
1918
2051
|
if (signal?.aborted) return;
|
|
1919
2052
|
new StudioSession(options).connect().catch((error) => {
|
|
1920
|
-
console.error(styleText("red", `Reconnect attempt to Kubb Studio failed: ${getErrorMessage(error)}`));
|
|
2053
|
+
if (logLevel$1 !== void 0 && logLevel$1 > logLevel.silent) console.error(styleText("red", `Reconnect attempt to Kubb Studio failed: ${getErrorMessage(error)}`));
|
|
1921
2054
|
if (error instanceof InvalidAgentTokenError) {
|
|
1922
2055
|
onTokenRejected?.(error);
|
|
1923
2056
|
return;
|
|
@@ -1952,6 +2085,7 @@ var StudioSession = class {
|
|
|
1952
2085
|
#heartbeatTimer;
|
|
1953
2086
|
#readyTimer;
|
|
1954
2087
|
#lastPongAt = Date.now();
|
|
2088
|
+
#lastGeneration;
|
|
1955
2089
|
constructor(options) {
|
|
1956
2090
|
this.#options = applyStudioDefaults(options);
|
|
1957
2091
|
}
|
|
@@ -2134,7 +2268,7 @@ var StudioSession = class {
|
|
|
2134
2268
|
* `#disposed` keeps the close event from running this twice, and a shutdown from reconnecting.
|
|
2135
2269
|
*/
|
|
2136
2270
|
async #end({ reason, retry }) {
|
|
2137
|
-
const { studioUrl, token } = this.#options;
|
|
2271
|
+
const { studioUrl, token, logLevel } = this.#options;
|
|
2138
2272
|
if (this.#disposed) return;
|
|
2139
2273
|
this.#disposed = true;
|
|
2140
2274
|
if (reason === "shutdown" && this.#ws) sendAgentMessage(this.#ws, {
|
|
@@ -2146,7 +2280,8 @@ var StudioSession = class {
|
|
|
2146
2280
|
sessionId: this.#session.sessionId,
|
|
2147
2281
|
studioUrl,
|
|
2148
2282
|
token,
|
|
2149
|
-
slug: this.#session.slug
|
|
2283
|
+
slug: this.#session.slug,
|
|
2284
|
+
logLevel
|
|
2150
2285
|
}).catch(() => {});
|
|
2151
2286
|
if (retry) reconnect(this.#options);
|
|
2152
2287
|
}
|
|
@@ -2205,6 +2340,9 @@ var StudioSession = class {
|
|
|
2205
2340
|
case "studio:save":
|
|
2206
2341
|
await this.#handleSave(ws, data, command);
|
|
2207
2342
|
return;
|
|
2343
|
+
case "studio:snapshot":
|
|
2344
|
+
await this.#handleSnapshot(ws, data, command);
|
|
2345
|
+
return;
|
|
2208
2346
|
}
|
|
2209
2347
|
}
|
|
2210
2348
|
async #handleGenerate(ws, data, command) {
|
|
@@ -2228,7 +2366,13 @@ var StudioSession = class {
|
|
|
2228
2366
|
await this.#warn(`Ignored the spec from Studio; set ${remedy} to generate from it`);
|
|
2229
2367
|
}
|
|
2230
2368
|
const resolvedPlugins = plugins ?? config.plugins;
|
|
2231
|
-
|
|
2369
|
+
let generatedFiles;
|
|
2370
|
+
const detach = [setupHookListener(this.#hooks, root), setupEventsStream(ws, this.#hooks, data.jobId, {
|
|
2371
|
+
skipStorage: client?.kind === "ci",
|
|
2372
|
+
onGenerationEnd: (files) => {
|
|
2373
|
+
generatedFiles = files;
|
|
2374
|
+
}
|
|
2375
|
+
})];
|
|
2232
2376
|
try {
|
|
2233
2377
|
await generate({
|
|
2234
2378
|
config: {
|
|
@@ -2249,6 +2393,7 @@ var StudioSession = class {
|
|
|
2249
2393
|
});
|
|
2250
2394
|
} finally {
|
|
2251
2395
|
for (const remove of detach) remove();
|
|
2396
|
+
this.#lastGeneration = generatedFiles;
|
|
2252
2397
|
}
|
|
2253
2398
|
await this.#hooks.callHook("studio:command:end", {
|
|
2254
2399
|
command,
|
|
@@ -2317,6 +2462,68 @@ var StudioSession = class {
|
|
|
2317
2462
|
refuse(getErrorMessage(error));
|
|
2318
2463
|
}
|
|
2319
2464
|
}
|
|
2465
|
+
async #handleSnapshot(ws, data, command) {
|
|
2466
|
+
const refuse = (message) => sendAgentMessage(ws, {
|
|
2467
|
+
type: "agent:snapshot",
|
|
2468
|
+
jobId: data.jobId,
|
|
2469
|
+
payload: {
|
|
2470
|
+
status: "error",
|
|
2471
|
+
message
|
|
2472
|
+
}
|
|
2473
|
+
});
|
|
2474
|
+
if (this.#isSandbox) {
|
|
2475
|
+
await this.#warn("Ignored snapshot: a sandbox agent has no project to build a package from");
|
|
2476
|
+
refuse("a sandbox agent has no project to build a package from");
|
|
2477
|
+
return;
|
|
2478
|
+
}
|
|
2479
|
+
const { name, version, peerDependencies, uploadPath } = data.payload;
|
|
2480
|
+
if (!name || !version || !uploadPath) {
|
|
2481
|
+
await this.#warn("Ignored snapshot: the message was missing required fields");
|
|
2482
|
+
refuse("the message was missing required fields");
|
|
2483
|
+
return;
|
|
2484
|
+
}
|
|
2485
|
+
const files = this.#lastGeneration;
|
|
2486
|
+
if (!files) {
|
|
2487
|
+
await this.#warn("Ignored snapshot: no prior generation to pack");
|
|
2488
|
+
refuse("no prior generation exists to pack, run a generation first");
|
|
2489
|
+
return;
|
|
2490
|
+
}
|
|
2491
|
+
try {
|
|
2492
|
+
const { bytes, integrity } = await createSnapshotPackage(files, {
|
|
2493
|
+
name,
|
|
2494
|
+
version,
|
|
2495
|
+
peerDependencies: peerDependencies ?? {}
|
|
2496
|
+
});
|
|
2497
|
+
const { token, studioUrl } = this.#options;
|
|
2498
|
+
const redirect = await fetch(new URL(uploadPath, studioUrl), {
|
|
2499
|
+
method: "PUT",
|
|
2500
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
2501
|
+
redirect: "manual"
|
|
2502
|
+
});
|
|
2503
|
+
const storageUrl = redirect.headers.get("location");
|
|
2504
|
+
if (redirect.status !== 307 || !storageUrl) throw new Error(`Studio did not provide a storage URL (status ${redirect.status})`);
|
|
2505
|
+
const response = await fetch(storageUrl, {
|
|
2506
|
+
method: "PUT",
|
|
2507
|
+
body: new Uint8Array(bytes)
|
|
2508
|
+
});
|
|
2509
|
+
if (!response.ok) throw new Error(`Snapshot upload failed with status ${response.status}`);
|
|
2510
|
+
sendAgentMessage(ws, {
|
|
2511
|
+
type: "agent:snapshot",
|
|
2512
|
+
jobId: data.jobId,
|
|
2513
|
+
payload: {
|
|
2514
|
+
status: "ok",
|
|
2515
|
+
integrity
|
|
2516
|
+
}
|
|
2517
|
+
});
|
|
2518
|
+
await this.#hooks.callHook("studio:command:end", {
|
|
2519
|
+
command,
|
|
2520
|
+
info: `packed ${Object.keys(files).length} file${Object.keys(files).length === 1 ? "" : "s"}`
|
|
2521
|
+
});
|
|
2522
|
+
} catch (error) {
|
|
2523
|
+
await this.#hooks.callHook("studio:error", { error: toError(error) });
|
|
2524
|
+
refuse(getErrorMessage(error));
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2320
2527
|
};
|
|
2321
2528
|
//#endregion
|
|
2322
2529
|
//#region src/client.ts
|