@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/README.md
CHANGED
|
@@ -128,6 +128,10 @@ if (finished.status === 'failed') throw new Error(finished.error)
|
|
|
128
128
|
const snapshot = finished.snapshot
|
|
129
129
|
```
|
|
130
130
|
|
|
131
|
+
A snapshot job packs the tarball on the agent, not on Studio. The agent `PUT`s an empty request to
|
|
132
|
+
a path Studio provides, gets back a redirect to a short-lived storage URL, and uploads the tarball
|
|
133
|
+
there. The storage URL never crosses the WebSocket.
|
|
134
|
+
|
|
131
135
|
## Protocol
|
|
132
136
|
|
|
133
137
|
`@kubb/studio/protocol` holds the WebSocket message types shared by both ends, so the agent and
|
package/dist/index.cjs
CHANGED
|
@@ -8,18 +8,21 @@ let node_child_process = require("node:child_process");
|
|
|
8
8
|
let node_fs_promises = require("node:fs/promises");
|
|
9
9
|
let node_path = require("node:path");
|
|
10
10
|
node_path = require_rolldown_runtime.__toESM(node_path, 1);
|
|
11
|
+
let _kubb_core = require("@kubb/core");
|
|
11
12
|
let ofetch = require("ofetch");
|
|
12
13
|
let node_crypto = require("node:crypto");
|
|
13
14
|
let unstorage = require("unstorage");
|
|
14
15
|
let unstorage_drivers_fs = require("unstorage/drivers/fs");
|
|
15
16
|
unstorage_drivers_fs = require_rolldown_runtime.__toESM(unstorage_drivers_fs, 1);
|
|
16
|
-
let _kubb_core = require("@kubb/core");
|
|
17
17
|
let tinyexec = require("tinyexec");
|
|
18
18
|
let magicast = require("magicast");
|
|
19
19
|
let node_fs = require("node:fs");
|
|
20
20
|
let node_module = require("node:module");
|
|
21
21
|
let node_url = require("node:url");
|
|
22
22
|
let remeda = require("remeda");
|
|
23
|
+
let node_os = require("node:os");
|
|
24
|
+
let node_zlib = require("node:zlib");
|
|
25
|
+
let tsdown = require("tsdown");
|
|
23
26
|
let ws = require("ws");
|
|
24
27
|
ws = require_rolldown_runtime.__toESM(ws, 1);
|
|
25
28
|
let node_timers_promises = require("node:timers/promises");
|
|
@@ -561,19 +564,20 @@ async function runRegistration({ token, studioUrl, poolSize }) {
|
|
|
561
564
|
* Called on process termination or server close. A failed notify is logged and swallowed: the
|
|
562
565
|
* local socket is already gone, and failing teardown must not block shutdown or reconnect.
|
|
563
566
|
*/
|
|
564
|
-
async function disconnect({ sessionId, token, studioUrl, slug }) {
|
|
567
|
+
async function disconnect({ sessionId, token, studioUrl, slug, logLevel }) {
|
|
565
568
|
const url = `${studioUrl}/api/agent/sessions/${sessionId}/disconnect`;
|
|
566
569
|
const tag = slug ?? "agent";
|
|
570
|
+
const canLog = logLevel !== void 0 && logLevel > _kubb_core.logLevel.silent;
|
|
567
571
|
try {
|
|
568
572
|
await (0, ofetch.ofetch)(url, {
|
|
569
573
|
method: "POST",
|
|
570
574
|
headers: { Authorization: `Bearer ${token}` }
|
|
571
575
|
});
|
|
572
|
-
console.
|
|
576
|
+
if (canLog) console.error((0, node_util.styleText)("green", `[${tag}] Disconnected from Studio`));
|
|
573
577
|
} catch (error) {
|
|
574
578
|
const statusCode = error?.statusCode;
|
|
575
579
|
if (statusCode !== void 0 && statusCode >= 400 && statusCode < 500) return;
|
|
576
|
-
console.warn((0, node_util.styleText)("yellow", `[${tag}] Failed to notify Studio of disconnection: ${getErrorMessage(error)}`));
|
|
580
|
+
if (canLog) console.warn((0, node_util.styleText)("yellow", `[${tag}] Failed to notify Studio of disconnection: ${getErrorMessage(error)}`));
|
|
577
581
|
}
|
|
578
582
|
}
|
|
579
583
|
/**
|
|
@@ -610,18 +614,41 @@ async function createJob({ studioUrl, token, type, agentId, name, version, confi
|
|
|
610
614
|
return job;
|
|
611
615
|
}
|
|
612
616
|
/**
|
|
613
|
-
*
|
|
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.
|
|
614
629
|
*
|
|
615
630
|
* A `failed` job resolves normally. Check `job.status` and `job.error`. Throws only when the
|
|
616
631
|
* deadline passes before Studio finishes.
|
|
617
632
|
*/
|
|
618
633
|
async function waitForJob({ studioUrl, token, id, timeoutMs = 6e4 }) {
|
|
619
634
|
const deadline = Date.now() + timeoutMs;
|
|
635
|
+
let interval = INITIAL_POLL_DELAY_MS;
|
|
620
636
|
for (;;) {
|
|
621
|
-
|
|
622
|
-
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)));
|
|
623
638
|
if (Date.now() >= deadline) throw new Error("Timed out waiting for the Studio job");
|
|
624
|
-
|
|
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
|
+
}
|
|
625
652
|
}
|
|
626
653
|
}
|
|
627
654
|
/**
|
|
@@ -651,7 +678,7 @@ async function createAgent({ studioUrl, token, name, machineToken }) {
|
|
|
651
678
|
}
|
|
652
679
|
//#endregion
|
|
653
680
|
//#region package.json
|
|
654
|
-
var version = "5.3.
|
|
681
|
+
var version = "5.3.4";
|
|
655
682
|
//#endregion
|
|
656
683
|
//#region src/hooks.ts
|
|
657
684
|
/**
|
|
@@ -1584,6 +1611,134 @@ async function generate({ config, hooks }) {
|
|
|
1584
1611
|
}
|
|
1585
1612
|
}
|
|
1586
1613
|
//#endregion
|
|
1614
|
+
//#region src/snapshotPackage.ts
|
|
1615
|
+
const gzipAsync = (0, node_util.promisify)(node_zlib.gzip);
|
|
1616
|
+
/**
|
|
1617
|
+
* Maps a generated file's path to its place inside the tarball, stripping everything before a
|
|
1618
|
+
* `src`/`dist` segment and any `..`/empty path segment so a crafted file name cannot escape the
|
|
1619
|
+
* `package/` root.
|
|
1620
|
+
*/
|
|
1621
|
+
function packagePath(filePath) {
|
|
1622
|
+
const normalized = filePath.replaceAll("\\", "/");
|
|
1623
|
+
return `package/${(normalized.match(/\/(?:src|dist)\/.*$/)?.[0].slice(1) ?? normalized.replace(/^\/+/, "")).split("/").filter((part) => part && part !== "." && part !== "..").join("/")}`;
|
|
1624
|
+
}
|
|
1625
|
+
/**
|
|
1626
|
+
* Splits a tarball entry path into the legacy 100-byte `name` field and, when the path does not
|
|
1627
|
+
* fit, the 155-byte USTAR `prefix` field that extends it. Throws rather than silently truncating
|
|
1628
|
+
* a path the format cannot address (max 256 bytes: 100 name + 1 separator + 155 prefix).
|
|
1629
|
+
*/
|
|
1630
|
+
function splitEntryPath(path) {
|
|
1631
|
+
if (Buffer.byteLength(path, "utf8") <= 100) return {
|
|
1632
|
+
name: path,
|
|
1633
|
+
prefix: ""
|
|
1634
|
+
};
|
|
1635
|
+
for (let i = path.length - 1; i >= 0; i--) {
|
|
1636
|
+
if (path[i] !== "/") continue;
|
|
1637
|
+
const prefix = path.slice(0, i);
|
|
1638
|
+
const name = path.slice(i + 1);
|
|
1639
|
+
if (Buffer.byteLength(prefix, "utf8") <= 155 && Buffer.byteLength(name, "utf8") <= 100) return {
|
|
1640
|
+
name,
|
|
1641
|
+
prefix
|
|
1642
|
+
};
|
|
1643
|
+
}
|
|
1644
|
+
throw new Error(`Snapshot path is too long for a tar entry: ${path}`);
|
|
1645
|
+
}
|
|
1646
|
+
function header(path, size) {
|
|
1647
|
+
const { name, prefix } = splitEntryPath(path);
|
|
1648
|
+
const value = Buffer.alloc(512);
|
|
1649
|
+
value.write(name, 0, "utf8");
|
|
1650
|
+
value.write("0000644\0", 100, "ascii");
|
|
1651
|
+
value.write("0000000\0", 108, "ascii");
|
|
1652
|
+
value.write("0000000\0", 116, "ascii");
|
|
1653
|
+
value.write(`${size.toString(8).padStart(11, "0")}\0`, 124, "ascii");
|
|
1654
|
+
value.write(`${Math.floor(Date.now() / 1e3).toString(8).padStart(11, "0")}\0`, 136, "ascii");
|
|
1655
|
+
value.fill(32, 148, 156);
|
|
1656
|
+
value.write("0", 156, "ascii");
|
|
1657
|
+
value.write("ustar\0", 257, "ascii");
|
|
1658
|
+
value.write("00", 263, "ascii");
|
|
1659
|
+
value.write("0000000\0", 265, "ascii");
|
|
1660
|
+
value.write("0000000\0", 297, "ascii");
|
|
1661
|
+
value.write(prefix, 345, "utf8");
|
|
1662
|
+
const checksum = [...value].reduce((sum, byte) => sum + byte, 0);
|
|
1663
|
+
value.write(`${checksum.toString(8).padStart(6, "0")}\0 `, 148, "ascii");
|
|
1664
|
+
return value;
|
|
1665
|
+
}
|
|
1666
|
+
/**
|
|
1667
|
+
* Packs a generation's files into a gzipped, npm-installable tarball: a `package/` root with the
|
|
1668
|
+
* generated sources, a `tsdown`-built `dist/` (esm + cjs), and a `package.json` manifest.
|
|
1669
|
+
*/
|
|
1670
|
+
async function createSnapshotPackage(files, packageInfo) {
|
|
1671
|
+
const root = await (0, node_fs_promises.mkdtemp)((0, node_path.join)((0, node_os.tmpdir)(), "kubb-snapshot-"));
|
|
1672
|
+
const dist = (0, node_path.join)(root, "dist");
|
|
1673
|
+
const resolvedPaths = Object.entries(files).map(([name, content]) => ({
|
|
1674
|
+
name,
|
|
1675
|
+
content,
|
|
1676
|
+
target: packagePath(name)
|
|
1677
|
+
}));
|
|
1678
|
+
const targetOwners = /* @__PURE__ */ new Map();
|
|
1679
|
+
for (const { name, target } of resolvedPaths) {
|
|
1680
|
+
const owner = targetOwners.get(target);
|
|
1681
|
+
if (owner) throw new Error(`Snapshot has two generated files that sanitize to the same path "${target}": "${owner}" and "${name}"`);
|
|
1682
|
+
targetOwners.set(target, name);
|
|
1683
|
+
}
|
|
1684
|
+
try {
|
|
1685
|
+
await (0, node_fs_promises.mkdir)(dist);
|
|
1686
|
+
await Promise.all(resolvedPaths.map(async ({ content, target }) => {
|
|
1687
|
+
const path = (0, node_path.join)(root, target.slice(8));
|
|
1688
|
+
await (0, node_fs_promises.mkdir)((0, node_path.join)(path, ".."), { recursive: true });
|
|
1689
|
+
await (0, node_fs_promises.writeFile)(path, content);
|
|
1690
|
+
}));
|
|
1691
|
+
const sourceEntries = resolvedPaths.filter(({ name }) => /\.(?:[cm]?[jt]sx?)$/.test(name)).map(({ target }) => (0, node_path.join)(root, target.slice(8)));
|
|
1692
|
+
if (sourceEntries.length) await (0, tsdown.build)({
|
|
1693
|
+
entry: sourceEntries,
|
|
1694
|
+
outDir: dist,
|
|
1695
|
+
format: ["esm", "cjs"],
|
|
1696
|
+
dts: false,
|
|
1697
|
+
sourcemap: false,
|
|
1698
|
+
unbundle: true,
|
|
1699
|
+
report: false,
|
|
1700
|
+
logLevel: "silent",
|
|
1701
|
+
fixedExtension: true
|
|
1702
|
+
});
|
|
1703
|
+
const builtEntries = await Promise.all((await Array.fromAsync((0, node_fs_promises.glob)("**/*", {
|
|
1704
|
+
cwd: dist,
|
|
1705
|
+
withFileTypes: true
|
|
1706
|
+
}))).filter((entry) => entry.isFile()).map(async (entry) => {
|
|
1707
|
+
const filePath = (0, node_path.join)(entry.parentPath, entry.name);
|
|
1708
|
+
return [`package/dist/${(0, node_path.relative)(dist, filePath).split(node_path.sep).join("/")}`, await (0, node_fs_promises.readFile)(filePath, "utf8")];
|
|
1709
|
+
}));
|
|
1710
|
+
const entries = {
|
|
1711
|
+
"package/package.json": JSON.stringify({
|
|
1712
|
+
...packageInfo,
|
|
1713
|
+
type: "module",
|
|
1714
|
+
main: "./dist/index.cjs",
|
|
1715
|
+
module: "./dist/index.mjs",
|
|
1716
|
+
exports: { ".": {
|
|
1717
|
+
import: "./dist/index.mjs",
|
|
1718
|
+
require: "./dist/index.cjs"
|
|
1719
|
+
} }
|
|
1720
|
+
}, null, 2),
|
|
1721
|
+
...Object.fromEntries(resolvedPaths.map(({ content, target }) => [target, content])),
|
|
1722
|
+
...Object.fromEntries(builtEntries)
|
|
1723
|
+
};
|
|
1724
|
+
const chunks = [];
|
|
1725
|
+
for (const [name, content] of Object.entries(entries)) {
|
|
1726
|
+
const bytes = Buffer.from(content);
|
|
1727
|
+
chunks.push(header(name, bytes.length), bytes, Buffer.alloc((512 - bytes.length % 512) % 512));
|
|
1728
|
+
}
|
|
1729
|
+
const bytes = await gzipAsync(Buffer.concat([...chunks, Buffer.alloc(1024)]));
|
|
1730
|
+
return {
|
|
1731
|
+
bytes,
|
|
1732
|
+
integrity: `sha512-${(0, node_crypto.createHash)("sha512").update(bytes).digest("base64")}`
|
|
1733
|
+
};
|
|
1734
|
+
} finally {
|
|
1735
|
+
await (0, node_fs_promises.rm)(root, {
|
|
1736
|
+
recursive: true,
|
|
1737
|
+
force: true
|
|
1738
|
+
});
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
//#endregion
|
|
1587
1742
|
//#region src/ws.ts
|
|
1588
1743
|
/**
|
|
1589
1744
|
* How many generated files are read from storage at once when building the
|
|
@@ -1679,7 +1834,7 @@ function sendErrorMessage(ws$4, error, jobId) {
|
|
|
1679
1834
|
/**
|
|
1680
1835
|
* Forwards selected Kubb lifecycle events to Studio as data messages for the active session.
|
|
1681
1836
|
*/
|
|
1682
|
-
function setupEventsStream(ws$5, hooks, jobId) {
|
|
1837
|
+
function setupEventsStream(ws$5, hooks, jobId, options = {}) {
|
|
1683
1838
|
const unhooks = [];
|
|
1684
1839
|
/**
|
|
1685
1840
|
* Registers a listener and keeps its remover, so one generation's listeners come off the session
|
|
@@ -1793,11 +1948,12 @@ function setupEventsStream(ws$5, hooks, jobId) {
|
|
|
1793
1948
|
if (content !== null) files[relativeStoragePath(config.root, path)] = content;
|
|
1794
1949
|
}
|
|
1795
1950
|
});
|
|
1951
|
+
options.onGenerationEnd?.(files);
|
|
1796
1952
|
sendDataMessage({
|
|
1797
1953
|
type: "kubb:generation:end",
|
|
1798
1954
|
data: [{
|
|
1799
1955
|
config,
|
|
1800
|
-
storage: files,
|
|
1956
|
+
storage: options.skipStorage ? {} : files,
|
|
1801
1957
|
peerDependencies,
|
|
1802
1958
|
missingDependencies
|
|
1803
1959
|
}]
|
|
@@ -1914,15 +2070,15 @@ function applyStudioDefaults(options) {
|
|
|
1914
2070
|
* socket, its hook emitter, or its session id alive for the length of the retry interval.
|
|
1915
2071
|
*/
|
|
1916
2072
|
function reconnect(options) {
|
|
1917
|
-
const { signal, retryInterval, onTokenRejected } = options;
|
|
2073
|
+
const { signal, retryInterval, onTokenRejected, logLevel } = options;
|
|
1918
2074
|
if (signal?.aborted) return;
|
|
1919
|
-
console.
|
|
2075
|
+
if (logLevel !== void 0 && logLevel > _kubb_core.logLevel.silent) console.error((0, node_util.styleText)("dim", `Retrying connection in ${retryInterval}ms to Kubb Studio ...`));
|
|
1920
2076
|
const cancel = () => clearTimeout(timer);
|
|
1921
2077
|
const timer = setTimeout(() => {
|
|
1922
2078
|
signal?.removeEventListener("abort", cancel);
|
|
1923
2079
|
if (signal?.aborted) return;
|
|
1924
2080
|
new StudioSession(options).connect().catch((error) => {
|
|
1925
|
-
console.error((0, node_util.styleText)("red", `Reconnect attempt to Kubb Studio failed: ${getErrorMessage(error)}`));
|
|
2081
|
+
if (logLevel !== void 0 && logLevel > _kubb_core.logLevel.silent) console.error((0, node_util.styleText)("red", `Reconnect attempt to Kubb Studio failed: ${getErrorMessage(error)}`));
|
|
1926
2082
|
if (error instanceof InvalidAgentTokenError) {
|
|
1927
2083
|
onTokenRejected?.(error);
|
|
1928
2084
|
return;
|
|
@@ -1957,6 +2113,7 @@ var StudioSession = class {
|
|
|
1957
2113
|
#heartbeatTimer;
|
|
1958
2114
|
#readyTimer;
|
|
1959
2115
|
#lastPongAt = Date.now();
|
|
2116
|
+
#lastGeneration;
|
|
1960
2117
|
constructor(options) {
|
|
1961
2118
|
this.#options = applyStudioDefaults(options);
|
|
1962
2119
|
}
|
|
@@ -2139,7 +2296,7 @@ var StudioSession = class {
|
|
|
2139
2296
|
* `#disposed` keeps the close event from running this twice, and a shutdown from reconnecting.
|
|
2140
2297
|
*/
|
|
2141
2298
|
async #end({ reason, retry }) {
|
|
2142
|
-
const { studioUrl, token } = this.#options;
|
|
2299
|
+
const { studioUrl, token, logLevel } = this.#options;
|
|
2143
2300
|
if (this.#disposed) return;
|
|
2144
2301
|
this.#disposed = true;
|
|
2145
2302
|
if (reason === "shutdown" && this.#ws) sendAgentMessage(this.#ws, {
|
|
@@ -2151,7 +2308,8 @@ var StudioSession = class {
|
|
|
2151
2308
|
sessionId: this.#session.sessionId,
|
|
2152
2309
|
studioUrl,
|
|
2153
2310
|
token,
|
|
2154
|
-
slug: this.#session.slug
|
|
2311
|
+
slug: this.#session.slug,
|
|
2312
|
+
logLevel
|
|
2155
2313
|
}).catch(() => {});
|
|
2156
2314
|
if (retry) reconnect(this.#options);
|
|
2157
2315
|
}
|
|
@@ -2210,6 +2368,9 @@ var StudioSession = class {
|
|
|
2210
2368
|
case "studio:save":
|
|
2211
2369
|
await this.#handleSave(ws, data, command);
|
|
2212
2370
|
return;
|
|
2371
|
+
case "studio:snapshot":
|
|
2372
|
+
await this.#handleSnapshot(ws, data, command);
|
|
2373
|
+
return;
|
|
2213
2374
|
}
|
|
2214
2375
|
}
|
|
2215
2376
|
async #handleGenerate(ws, data, command) {
|
|
@@ -2233,7 +2394,13 @@ var StudioSession = class {
|
|
|
2233
2394
|
await this.#warn(`Ignored the spec from Studio; set ${remedy} to generate from it`);
|
|
2234
2395
|
}
|
|
2235
2396
|
const resolvedPlugins = plugins ?? config.plugins;
|
|
2236
|
-
|
|
2397
|
+
let generatedFiles;
|
|
2398
|
+
const detach = [setupHookListener(this.#hooks, root), setupEventsStream(ws, this.#hooks, data.jobId, {
|
|
2399
|
+
skipStorage: client?.kind === "ci",
|
|
2400
|
+
onGenerationEnd: (files) => {
|
|
2401
|
+
generatedFiles = files;
|
|
2402
|
+
}
|
|
2403
|
+
})];
|
|
2237
2404
|
try {
|
|
2238
2405
|
await generate({
|
|
2239
2406
|
config: {
|
|
@@ -2254,6 +2421,7 @@ var StudioSession = class {
|
|
|
2254
2421
|
});
|
|
2255
2422
|
} finally {
|
|
2256
2423
|
for (const remove of detach) remove();
|
|
2424
|
+
this.#lastGeneration = generatedFiles;
|
|
2257
2425
|
}
|
|
2258
2426
|
await this.#hooks.callHook("studio:command:end", {
|
|
2259
2427
|
command,
|
|
@@ -2322,6 +2490,68 @@ var StudioSession = class {
|
|
|
2322
2490
|
refuse(getErrorMessage(error));
|
|
2323
2491
|
}
|
|
2324
2492
|
}
|
|
2493
|
+
async #handleSnapshot(ws, data, command) {
|
|
2494
|
+
const refuse = (message) => sendAgentMessage(ws, {
|
|
2495
|
+
type: "agent:snapshot",
|
|
2496
|
+
jobId: data.jobId,
|
|
2497
|
+
payload: {
|
|
2498
|
+
status: "error",
|
|
2499
|
+
message
|
|
2500
|
+
}
|
|
2501
|
+
});
|
|
2502
|
+
if (this.#isSandbox) {
|
|
2503
|
+
await this.#warn("Ignored snapshot: a sandbox agent has no project to build a package from");
|
|
2504
|
+
refuse("a sandbox agent has no project to build a package from");
|
|
2505
|
+
return;
|
|
2506
|
+
}
|
|
2507
|
+
const { name, version, peerDependencies, uploadPath } = data.payload;
|
|
2508
|
+
if (!name || !version || !uploadPath) {
|
|
2509
|
+
await this.#warn("Ignored snapshot: the message was missing required fields");
|
|
2510
|
+
refuse("the message was missing required fields");
|
|
2511
|
+
return;
|
|
2512
|
+
}
|
|
2513
|
+
const files = this.#lastGeneration;
|
|
2514
|
+
if (!files) {
|
|
2515
|
+
await this.#warn("Ignored snapshot: no prior generation to pack");
|
|
2516
|
+
refuse("no prior generation exists to pack, run a generation first");
|
|
2517
|
+
return;
|
|
2518
|
+
}
|
|
2519
|
+
try {
|
|
2520
|
+
const { bytes, integrity } = await createSnapshotPackage(files, {
|
|
2521
|
+
name,
|
|
2522
|
+
version,
|
|
2523
|
+
peerDependencies: peerDependencies ?? {}
|
|
2524
|
+
});
|
|
2525
|
+
const { token, studioUrl } = this.#options;
|
|
2526
|
+
const redirect = await fetch(new URL(uploadPath, studioUrl), {
|
|
2527
|
+
method: "PUT",
|
|
2528
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
2529
|
+
redirect: "manual"
|
|
2530
|
+
});
|
|
2531
|
+
const storageUrl = redirect.headers.get("location");
|
|
2532
|
+
if (redirect.status !== 307 || !storageUrl) throw new Error(`Studio did not provide a storage URL (status ${redirect.status})`);
|
|
2533
|
+
const response = await fetch(storageUrl, {
|
|
2534
|
+
method: "PUT",
|
|
2535
|
+
body: new Uint8Array(bytes)
|
|
2536
|
+
});
|
|
2537
|
+
if (!response.ok) throw new Error(`Snapshot upload failed with status ${response.status}`);
|
|
2538
|
+
sendAgentMessage(ws, {
|
|
2539
|
+
type: "agent:snapshot",
|
|
2540
|
+
jobId: data.jobId,
|
|
2541
|
+
payload: {
|
|
2542
|
+
status: "ok",
|
|
2543
|
+
integrity
|
|
2544
|
+
}
|
|
2545
|
+
});
|
|
2546
|
+
await this.#hooks.callHook("studio:command:end", {
|
|
2547
|
+
command,
|
|
2548
|
+
info: `packed ${Object.keys(files).length} file${Object.keys(files).length === 1 ? "" : "s"}`
|
|
2549
|
+
});
|
|
2550
|
+
} catch (error) {
|
|
2551
|
+
await this.#hooks.callHook("studio:error", { error: toError(error) });
|
|
2552
|
+
refuse(getErrorMessage(error));
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2325
2555
|
};
|
|
2326
2556
|
//#endregion
|
|
2327
2557
|
//#region src/client.ts
|