@kubb/studio 5.3.4 → 5.3.6
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 +32 -11
- package/dist/index.cjs +508 -490
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +19 -5
- package/dist/index.js +472 -480
- package/dist/index.js.map +1 -1
- package/dist/protocol.cjs +33 -41
- package/dist/protocol.cjs.map +1 -1
- package/dist/protocol.d.ts +144 -220
- package/dist/protocol.js +32 -35
- package/dist/protocol.js.map +1 -1
- package/package.json +6 -4
- package/dist/rolldown-runtime-qbf5tadS.cjs +0 -38
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as __name } from "./rolldown-runtime-CRm0XQPb.js";
|
|
2
|
-
import {
|
|
2
|
+
import { generationEventTypes } from "./protocol.js";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { promisify, styleText } from "node:util";
|
|
5
5
|
import process$1 from "node:process";
|
|
@@ -19,6 +19,7 @@ import { mergeDeep } from "remeda";
|
|
|
19
19
|
import { tmpdir } from "node:os";
|
|
20
20
|
import { gzip } from "node:zlib";
|
|
21
21
|
import { build } from "tsdown";
|
|
22
|
+
import { RpcTarget, newWebSocketRpcSession } from "capnweb";
|
|
22
23
|
import WebSocket from "ws";
|
|
23
24
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
24
25
|
//#region src/constants.ts
|
|
@@ -41,6 +42,8 @@ const agentDefaults = {
|
|
|
41
42
|
* a slower cadence would make a healthy agent look dead after a single missed ping.
|
|
42
43
|
*/
|
|
43
44
|
maxHeartbeatIntervalMs: 6e4,
|
|
45
|
+
/** How long a heartbeat ping may take before the session is treated as dead. */
|
|
46
|
+
heartbeatTimeoutMs: 1e4,
|
|
44
47
|
poolSize: 1
|
|
45
48
|
};
|
|
46
49
|
//#endregion
|
|
@@ -618,7 +621,7 @@ const INITIAL_POLL_DELAY_MS = 2e3;
|
|
|
618
621
|
*/
|
|
619
622
|
const MAX_POLL_INTERVAL_MS = 3e4;
|
|
620
623
|
/**
|
|
621
|
-
* Polls `GET /api/jobs/{id}` until the job reaches
|
|
624
|
+
* Polls `GET /api/jobs/{id}` until the job reaches a terminal status, waiting
|
|
622
625
|
* {@link INITIAL_POLL_DELAY_MS} first and doubling up to {@link MAX_POLL_INTERVAL_MS} so a long
|
|
623
626
|
* job stays inside the API key's rate limit.
|
|
624
627
|
*
|
|
@@ -637,7 +640,7 @@ async function waitForJob({ studioUrl, token, id, timeoutMs = 6e4 }) {
|
|
|
637
640
|
headers: { "x-api-key": token },
|
|
638
641
|
retry: false
|
|
639
642
|
});
|
|
640
|
-
if (job.status === "success" || job.status === "failed") return job;
|
|
643
|
+
if (job.status === "success" || job.status === "failed" || job.status === "canceled") return job;
|
|
641
644
|
} catch (error) {
|
|
642
645
|
const response = error.response;
|
|
643
646
|
if (response?.status !== 429) throw error;
|
|
@@ -673,7 +676,7 @@ async function createAgent({ studioUrl, token, name, machineToken }) {
|
|
|
673
676
|
}
|
|
674
677
|
//#endregion
|
|
675
678
|
//#region package.json
|
|
676
|
-
var version = "5.3.
|
|
679
|
+
var version = "5.3.6";
|
|
677
680
|
//#endregion
|
|
678
681
|
//#region src/hooks.ts
|
|
679
682
|
/**
|
|
@@ -684,16 +687,19 @@ var version = "5.3.4";
|
|
|
684
687
|
* Returns a remover, so a session that runs one generation after another on the same emitter does
|
|
685
688
|
* not stack a listener per run.
|
|
686
689
|
*/
|
|
687
|
-
function setupHookListener(hooks, root) {
|
|
690
|
+
function setupHookListener(hooks, root, signal) {
|
|
688
691
|
return hooks.hook("kubb:hook:start", async (ctx) => {
|
|
689
692
|
const { id, command, args } = ctx;
|
|
690
693
|
if (!id) return;
|
|
691
694
|
const commandWithArgs = args?.length ? `${command} ${args.join(" ")}` : command;
|
|
692
695
|
try {
|
|
693
|
-
const proc = x(command, [...args ?? []], {
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
696
|
+
const proc = x(command, [...args ?? []], {
|
|
697
|
+
signal,
|
|
698
|
+
nodeOptions: {
|
|
699
|
+
cwd: root,
|
|
700
|
+
detached: true
|
|
701
|
+
}
|
|
702
|
+
});
|
|
697
703
|
for await (const line of proc) await hooks.callHook("kubb:hook:line", {
|
|
698
704
|
id,
|
|
699
705
|
line
|
|
@@ -742,8 +748,11 @@ function waitForHookEnd(hooks, hookId) {
|
|
|
742
748
|
const handleHookEnd = (ctx) => {
|
|
743
749
|
if (ctx.id !== hookId) return;
|
|
744
750
|
hooks.removeHook("kubb:hook:end", handleHookEnd);
|
|
745
|
-
if (ctx.success)
|
|
746
|
-
|
|
751
|
+
if (ctx.success) {
|
|
752
|
+
resolve();
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
reject(ctx.error);
|
|
747
756
|
};
|
|
748
757
|
hooks.hook("kubb:hook:end", handleHookEnd);
|
|
749
758
|
});
|
|
@@ -1282,14 +1291,14 @@ function applyRemove(call, path) {
|
|
|
1282
1291
|
});
|
|
1283
1292
|
}
|
|
1284
1293
|
/**
|
|
1285
|
-
* Adds a `pluginX(...)` call to a config's plugins array.
|
|
1286
|
-
*
|
|
1294
|
+
* Adds a `pluginX(...)` call to a config's plugins array. Replaying the same add is a no-op, while
|
|
1295
|
+
* an import name collision with an unrelated package remains an error.
|
|
1287
1296
|
*/
|
|
1288
1297
|
function applyAddPlugin(mod, config, edit) {
|
|
1289
1298
|
if (!isKubbPluginSpecifier(edit.plugin)) return { reason: `"${edit.plugin}" is not a @kubb/plugin-* package` };
|
|
1290
1299
|
const importName = edit.importName ?? toExportName(edit.plugin);
|
|
1291
1300
|
if (!IDENTIFIER.test(importName)) return { reason: `"${importName}" is not a valid import name` };
|
|
1292
|
-
if (pluginCalls(mod, config).some((plugin) => plugin.packageName === edit.plugin)) return {
|
|
1301
|
+
if (pluginCalls(mod, config).some((plugin) => plugin.packageName === edit.plugin)) return { noop: true };
|
|
1293
1302
|
const taken = importedFrom(mod).get(importName);
|
|
1294
1303
|
if (taken && taken !== edit.plugin) return { reason: `${importName} is already imported from ${taken}` };
|
|
1295
1304
|
const options = edit.options ?? {};
|
|
@@ -1410,6 +1419,10 @@ function applyConfigEdits(source, edits) {
|
|
|
1410
1419
|
applied: false,
|
|
1411
1420
|
reason: result.reason
|
|
1412
1421
|
};
|
|
1422
|
+
if ("noop" in result) return {
|
|
1423
|
+
edit,
|
|
1424
|
+
applied: true
|
|
1425
|
+
};
|
|
1413
1426
|
const afterLine = lastImportEndLine(mod);
|
|
1414
1427
|
let next = generateCode(mod, { format }).code;
|
|
1415
1428
|
if (result.addImport) next = insertImportLine({
|
|
@@ -1543,14 +1556,19 @@ function formatGenerationFailure(diagnostics) {
|
|
|
1543
1556
|
* can forward progress to connected clients. After a successful build, auto-formatting and
|
|
1544
1557
|
* linting are applied when configured, followed by any user-defined `hooks.done` commands.
|
|
1545
1558
|
*/
|
|
1546
|
-
async function generate({ config, hooks }) {
|
|
1559
|
+
async function generate({ config, hooks, signal }) {
|
|
1560
|
+
signal?.throwIfAborted();
|
|
1547
1561
|
const hrStart = process$1.hrtime();
|
|
1548
1562
|
await hooks.callHook("kubb:generation:start", { config });
|
|
1549
1563
|
await hooks.callHook("kubb:info", { message: config.name ? `Setup generation ${config.name}` : "Setup generation" });
|
|
1550
|
-
const kubb = createKubb(config, {
|
|
1564
|
+
const kubb = createKubb(config, {
|
|
1565
|
+
hooks,
|
|
1566
|
+
signal
|
|
1567
|
+
});
|
|
1551
1568
|
await kubb.setup();
|
|
1552
1569
|
await hooks.callHook("kubb:info", { message: config.name ? `Build generation ${config.name}` : "Build generation" });
|
|
1553
1570
|
const { files, diagnostics, storage } = await kubb.safeBuild();
|
|
1571
|
+
signal?.throwIfAborted();
|
|
1554
1572
|
await hooks.callHook("kubb:info", { message: "Load summary" });
|
|
1555
1573
|
for (const diagnostic of diagnostics.filter(isProblemErrorDiagnostic)) await hooks.callHook("kubb:error", { error: new Error(diagnostic.plugin ? `${diagnostic.plugin}: ${diagnostic.message}` : diagnostic.message) });
|
|
1556
1574
|
const status = Diagnostics.hasError(diagnostics) ? "failed" : "success";
|
|
@@ -1585,6 +1603,7 @@ async function generate({ config, hooks }) {
|
|
|
1585
1603
|
await hooks.callHook("kubb:success", { message: `${step.verbing} with ${tool} successfully` });
|
|
1586
1604
|
} catch (caughtError) {
|
|
1587
1605
|
await hooks.callHook("kubb:error", { error: new Error(command.errorMessage, { cause: caughtError }) });
|
|
1606
|
+
signal?.throwIfAborted();
|
|
1588
1607
|
}
|
|
1589
1608
|
await hooks.callHook(`kubb:${step.kind}:end`);
|
|
1590
1609
|
}
|
|
@@ -1736,27 +1755,20 @@ async function createSnapshotPackage(files, packageInfo) {
|
|
|
1736
1755
|
//#endregion
|
|
1737
1756
|
//#region src/ws.ts
|
|
1738
1757
|
/**
|
|
1739
|
-
* How many generated files are read from storage at once when building the
|
|
1740
|
-
* `kubb:generation:end` payload. A spec producing thousands of files would otherwise fire one
|
|
1741
|
-
* `storage.readItem` per file simultaneously.
|
|
1742
|
-
*/
|
|
1743
|
-
const FILE_READ_CONCURRENCY = 50;
|
|
1744
|
-
/**
|
|
1745
1758
|
* How long the initial handshake may take before the socket is closed and the reconnect loop
|
|
1746
1759
|
* takes over.
|
|
1747
1760
|
*/
|
|
1748
1761
|
const CONNECT_TIMEOUT_MS = 5e3;
|
|
1749
|
-
/**
|
|
1750
|
-
* Per-socket event counter. Every data message carries the next value so Studio can restore the
|
|
1751
|
-
* agent's emission order even when the relay delivers frames out of order. Keyed by the socket so
|
|
1752
|
-
* the count stays monotonic across every generation run on one connection, and is dropped
|
|
1753
|
-
* automatically once the socket is collected.
|
|
1754
|
-
*/
|
|
1755
|
-
const eventSeqCounters = /* @__PURE__ */ new WeakMap();
|
|
1756
1762
|
const require = createRequire(import.meta.url);
|
|
1757
1763
|
function relativeStoragePath(root, filePath) {
|
|
1758
1764
|
return (isAbsolute(filePath) ? relative(resolve(root), filePath) : filePath).replaceAll("\\", "/");
|
|
1759
1765
|
}
|
|
1766
|
+
/**
|
|
1767
|
+
* Inverse of {@link relativeStoragePath}: rebuilds the storage key a relative path came from.
|
|
1768
|
+
*/
|
|
1769
|
+
function absoluteStoragePath(root, relativePath) {
|
|
1770
|
+
return resolve(root, relativePath);
|
|
1771
|
+
}
|
|
1760
1772
|
async function resolvePeerDependencies(names) {
|
|
1761
1773
|
const uniqueNames = [...new Set(names.map(toPackageName))];
|
|
1762
1774
|
const peerDependencies = {};
|
|
@@ -1771,19 +1783,17 @@ async function resolvePeerDependencies(names) {
|
|
|
1771
1783
|
}));
|
|
1772
1784
|
for (const [index, name] of uniqueNames.entries()) {
|
|
1773
1785
|
const version = versions[index];
|
|
1774
|
-
if (version)
|
|
1775
|
-
|
|
1786
|
+
if (version) {
|
|
1787
|
+
peerDependencies[name] = version;
|
|
1788
|
+
continue;
|
|
1789
|
+
}
|
|
1790
|
+
missingDependencies.push(name);
|
|
1776
1791
|
}
|
|
1777
1792
|
return {
|
|
1778
1793
|
peerDependencies,
|
|
1779
1794
|
missingDependencies
|
|
1780
1795
|
};
|
|
1781
1796
|
}
|
|
1782
|
-
function nextEventSeq(ws) {
|
|
1783
|
-
const seq = eventSeqCounters.get(ws) ?? 0;
|
|
1784
|
-
eventSeqCounters.set(ws, seq + 1);
|
|
1785
|
-
return seq;
|
|
1786
|
-
}
|
|
1787
1797
|
/**
|
|
1788
1798
|
* Opens a Studio WebSocket connection and closes it when the initial handshake exceeds the configured timeout.
|
|
1789
1799
|
*/
|
|
@@ -1796,41 +1806,15 @@ function createWebsocket(url, options) {
|
|
|
1796
1806
|
ws.once("close", () => clearTimeout(timer));
|
|
1797
1807
|
return ws;
|
|
1798
1808
|
}
|
|
1799
|
-
/**
|
|
1800
|
-
|
|
1801
|
-
*/
|
|
1802
|
-
function sendAgentMessage(ws, message) {
|
|
1803
|
-
try {
|
|
1804
|
-
if (ws.readyState !== WebSocket.OPEN) return;
|
|
1805
|
-
ws.send(JSON.stringify(message));
|
|
1806
|
-
} catch (error) {
|
|
1807
|
-
throw new Error("Failed to send message to Kubb Studio", { cause: error });
|
|
1808
|
-
}
|
|
1809
|
-
}
|
|
1810
|
-
/**
|
|
1811
|
-
* Sends a single `kubb:error` payload to Studio, stamped from the same per-socket counter the event stream
|
|
1812
|
-
* uses so Studio can still order it against the generation events around it.
|
|
1813
|
-
*/
|
|
1814
|
-
function sendErrorMessage(ws, error, jobId) {
|
|
1815
|
-
sendAgentMessage(ws, {
|
|
1816
|
-
type: "agent:data",
|
|
1817
|
-
jobId,
|
|
1818
|
-
payload: {
|
|
1819
|
-
type: "kubb:error",
|
|
1820
|
-
data: [{
|
|
1821
|
-
message: error.message,
|
|
1822
|
-
stack: error.stack
|
|
1823
|
-
}],
|
|
1824
|
-
timestamp: Date.now(),
|
|
1825
|
-
seq: nextEventSeq(ws)
|
|
1826
|
-
}
|
|
1827
|
-
});
|
|
1828
|
-
}
|
|
1829
|
-
/**
|
|
1830
|
-
* Forwards selected Kubb lifecycle events to Studio as data messages for the active session.
|
|
1831
|
-
*/
|
|
1832
|
-
function setupEventsStream(ws, hooks, jobId, options = {}) {
|
|
1809
|
+
/** Forwards selected Kubb lifecycle events to a native Cap'n Web stream. */
|
|
1810
|
+
function createGenerationStream(hooks, jobId, options = {}) {
|
|
1833
1811
|
const unhooks = [];
|
|
1812
|
+
let root = "";
|
|
1813
|
+
const transform = new TransformStream(void 0, void 0, { highWaterMark: Infinity });
|
|
1814
|
+
const writer = transform.writable.getWriter();
|
|
1815
|
+
let writes = Promise.resolve();
|
|
1816
|
+
let closed = false;
|
|
1817
|
+
let streamError;
|
|
1834
1818
|
/**
|
|
1835
1819
|
* Registers a listener and keeps its remover, so one generation's listeners come off the session
|
|
1836
1820
|
* emitter again when that generation ends.
|
|
@@ -1838,140 +1822,111 @@ function setupEventsStream(ws, hooks, jobId, options = {}) {
|
|
|
1838
1822
|
function on(name, handler) {
|
|
1839
1823
|
unhooks.push(hooks.hook(name, handler));
|
|
1840
1824
|
}
|
|
1841
|
-
function
|
|
1842
|
-
|
|
1843
|
-
type: "agent:data",
|
|
1825
|
+
function emitEvent(type, data) {
|
|
1826
|
+
const event = {
|
|
1844
1827
|
jobId,
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1828
|
+
type,
|
|
1829
|
+
data,
|
|
1830
|
+
version: 1,
|
|
1831
|
+
timestamp: Date.now()
|
|
1832
|
+
};
|
|
1833
|
+
writes = writes.then(() => writer.write(event)).catch((error) => {
|
|
1834
|
+
streamError = error;
|
|
1850
1835
|
});
|
|
1851
1836
|
}
|
|
1852
1837
|
on("kubb:plugin:start", (ctx) => {
|
|
1853
|
-
|
|
1854
|
-
type: "kubb:plugin:start",
|
|
1855
|
-
data: [{ plugin: ctx.plugin }]
|
|
1856
|
-
});
|
|
1838
|
+
emitEvent("kubb:plugin:start", [{ plugin: { name: ctx.plugin.name } }]);
|
|
1857
1839
|
});
|
|
1858
1840
|
on("kubb:plugin:end", (ctx) => {
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
success: ctx.success
|
|
1865
|
-
}]
|
|
1866
|
-
});
|
|
1841
|
+
emitEvent("kubb:plugin:end", [{
|
|
1842
|
+
plugin: { name: ctx.plugin.name },
|
|
1843
|
+
duration: ctx.duration,
|
|
1844
|
+
success: ctx.success
|
|
1845
|
+
}]);
|
|
1867
1846
|
});
|
|
1868
1847
|
on("kubb:build:start", ({ config, adapter }) => {
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
}]
|
|
1875
|
-
});
|
|
1848
|
+
root = config.root;
|
|
1849
|
+
emitEvent("kubb:build:start", [{
|
|
1850
|
+
config: { name: config.name },
|
|
1851
|
+
adapter: { name: adapter.name }
|
|
1852
|
+
}]);
|
|
1876
1853
|
});
|
|
1877
|
-
on("kubb:build:end", ({ files, outputDir }) => {
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
outputDir
|
|
1886
|
-
}]
|
|
1887
|
-
});
|
|
1854
|
+
on("kubb:build:end", ({ files, config, outputDir }) => {
|
|
1855
|
+
emitEvent("kubb:build:end", [{
|
|
1856
|
+
files: files.map((file) => ({
|
|
1857
|
+
path: relativeStoragePath(config.root, file.path),
|
|
1858
|
+
name: file.name
|
|
1859
|
+
})),
|
|
1860
|
+
outputDir
|
|
1861
|
+
}]);
|
|
1888
1862
|
});
|
|
1889
1863
|
on("kubb:files:processing:start", ({ files }) => {
|
|
1890
|
-
|
|
1891
|
-
type: "kubb:files:processing:start",
|
|
1892
|
-
data: [{ total: files.length }]
|
|
1893
|
-
});
|
|
1864
|
+
emitEvent("kubb:files:processing:start", [{ total: files.length }]);
|
|
1894
1865
|
});
|
|
1895
1866
|
on("kubb:files:processing:update", ({ files }) => {
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
percentage
|
|
1903
|
-
})) }]
|
|
1904
|
-
});
|
|
1867
|
+
emitEvent("kubb:files:processing:update", [{ files: files.map(({ file, processed, total, percentage }) => ({
|
|
1868
|
+
file: relativeStoragePath(root, file.path),
|
|
1869
|
+
processed,
|
|
1870
|
+
total,
|
|
1871
|
+
percentage
|
|
1872
|
+
})) }]);
|
|
1905
1873
|
});
|
|
1906
1874
|
on("kubb:files:processing:end", ({ files }) => {
|
|
1907
|
-
|
|
1908
|
-
type: "kubb:files:processing:end",
|
|
1909
|
-
data: [{ total: files.length }]
|
|
1910
|
-
});
|
|
1875
|
+
emitEvent("kubb:files:processing:end", [{ total: files.length }]);
|
|
1911
1876
|
});
|
|
1912
1877
|
for (const type of [
|
|
1913
1878
|
"kubb:info",
|
|
1914
1879
|
"kubb:success",
|
|
1915
1880
|
"kubb:warn"
|
|
1916
1881
|
]) on(type, ({ message, info }) => {
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
info
|
|
1922
|
-
}]
|
|
1923
|
-
});
|
|
1882
|
+
emitEvent(type, [{
|
|
1883
|
+
message,
|
|
1884
|
+
info
|
|
1885
|
+
}]);
|
|
1924
1886
|
});
|
|
1925
1887
|
on("kubb:generation:start", ({ config }) => {
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
plugins: config.plugins.length
|
|
1931
|
-
}]
|
|
1932
|
-
});
|
|
1888
|
+
emitEvent("kubb:generation:start", [{
|
|
1889
|
+
name: config.name,
|
|
1890
|
+
plugins: config.plugins.length
|
|
1891
|
+
}]);
|
|
1933
1892
|
});
|
|
1934
1893
|
on("kubb:generation:end", async ({ config, storage, diagnostics = [], status, hrStart, filesCreated }) => {
|
|
1935
1894
|
const { peerDependencies, missingDependencies } = await resolvePeerDependencies(config.plugins.map(({ name }) => name));
|
|
1936
|
-
const
|
|
1937
|
-
const
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
}
|
|
1945
|
-
});
|
|
1946
|
-
options.onGenerationEnd?.(files);
|
|
1947
|
-
sendDataMessage({
|
|
1948
|
-
type: "kubb:generation:end",
|
|
1949
|
-
data: [{
|
|
1950
|
-
config,
|
|
1951
|
-
storage: options.skipStorage ? {} : files,
|
|
1952
|
-
peerDependencies,
|
|
1953
|
-
missingDependencies
|
|
1954
|
-
}]
|
|
1895
|
+
const keys = await storage.readKeys();
|
|
1896
|
+
const paths = new Set(keys.map((key) => relativeStoragePath(config.root, key)));
|
|
1897
|
+
options.onGenerationEnd?.({
|
|
1898
|
+
storage,
|
|
1899
|
+
root: config.root,
|
|
1900
|
+
paths,
|
|
1901
|
+
peerDependencies,
|
|
1902
|
+
missingDependencies
|
|
1955
1903
|
});
|
|
1904
|
+
emitEvent("kubb:generation:end", []);
|
|
1956
1905
|
if (!hrStart) return;
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
status: status ?? "success"
|
|
1964
|
-
}]
|
|
1965
|
-
});
|
|
1906
|
+
emitEvent("kubb:generation:summary", [{
|
|
1907
|
+
duration: Math.round(getElapsedMs(hrStart)),
|
|
1908
|
+
fileCount: filesCreated ?? 0,
|
|
1909
|
+
failedPlugins: Diagnostics.failedPlugins(diagnostics).length,
|
|
1910
|
+
status: status ?? "success"
|
|
1911
|
+
}]);
|
|
1966
1912
|
});
|
|
1967
1913
|
on("kubb:error", ({ error }) => {
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1914
|
+
emitEvent("kubb:error", [{
|
|
1915
|
+
message: error.message,
|
|
1916
|
+
stack: error.stack
|
|
1917
|
+
}]);
|
|
1918
|
+
});
|
|
1919
|
+
on("kubb:diagnostic", ({ diagnostic }) => {
|
|
1920
|
+
const cause = "cause" in diagnostic ? diagnostic.cause : void 0;
|
|
1921
|
+
emitEvent("kubb:diagnostic", [{
|
|
1922
|
+
code: diagnostic.code,
|
|
1923
|
+
message: diagnostic.message,
|
|
1924
|
+
severity: diagnostic.severity,
|
|
1925
|
+
location: "location" in diagnostic ? diagnostic.location : void 0,
|
|
1926
|
+
help: "help" in diagnostic ? diagnostic.help : void 0,
|
|
1927
|
+
plugin: "plugin" in diagnostic ? diagnostic.plugin : void 0,
|
|
1928
|
+
stack: cause?.stack
|
|
1929
|
+
}]);
|
|
1975
1930
|
});
|
|
1976
1931
|
for (const type of [
|
|
1977
1932
|
"kubb:lifecycle:start",
|
|
@@ -1983,57 +1938,144 @@ function setupEventsStream(ws, hooks, jobId, options = {}) {
|
|
|
1983
1938
|
"kubb:hooks:start",
|
|
1984
1939
|
"kubb:hooks:end"
|
|
1985
1940
|
]) on(type, () => {
|
|
1986
|
-
|
|
1987
|
-
type,
|
|
1988
|
-
data: []
|
|
1989
|
-
});
|
|
1941
|
+
emitEvent(type, []);
|
|
1990
1942
|
});
|
|
1991
1943
|
on("kubb:hook:start", ({ id, command, args }) => {
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
args: args ? [...args] : void 0
|
|
1998
|
-
}]
|
|
1999
|
-
});
|
|
1944
|
+
emitEvent("kubb:hook:start", [{
|
|
1945
|
+
id,
|
|
1946
|
+
command,
|
|
1947
|
+
args: args ? [...args] : void 0
|
|
1948
|
+
}]);
|
|
2000
1949
|
});
|
|
2001
1950
|
on("kubb:hook:line", ({ id, line }) => {
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
line
|
|
2007
|
-
}]
|
|
2008
|
-
});
|
|
1951
|
+
emitEvent("kubb:hook:line", [{
|
|
1952
|
+
id,
|
|
1953
|
+
line
|
|
1954
|
+
}]);
|
|
2009
1955
|
});
|
|
2010
1956
|
on("kubb:hook:end", ({ id, command, args, success, error }) => {
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
} : void 0
|
|
2022
|
-
}]
|
|
2023
|
-
});
|
|
1957
|
+
emitEvent("kubb:hook:end", [{
|
|
1958
|
+
id,
|
|
1959
|
+
command,
|
|
1960
|
+
args: args ? [...args] : void 0,
|
|
1961
|
+
success,
|
|
1962
|
+
error: error ? {
|
|
1963
|
+
message: error.message,
|
|
1964
|
+
stack: error.stack
|
|
1965
|
+
} : void 0
|
|
1966
|
+
}]);
|
|
2024
1967
|
});
|
|
2025
|
-
|
|
1968
|
+
/**
|
|
1969
|
+
* Takes this generation's listeners off the session emitter. Safe to call twice.
|
|
1970
|
+
*/
|
|
1971
|
+
function detach() {
|
|
2026
1972
|
for (const unhook of unhooks) unhook();
|
|
1973
|
+
unhooks.length = 0;
|
|
1974
|
+
}
|
|
1975
|
+
async function close() {
|
|
1976
|
+
if (closed) return;
|
|
1977
|
+
closed = true;
|
|
1978
|
+
detach();
|
|
1979
|
+
await writes;
|
|
1980
|
+
if (streamError) return;
|
|
1981
|
+
await writer.close().catch(() => void 0);
|
|
1982
|
+
}
|
|
1983
|
+
function fail(error) {
|
|
1984
|
+
detach();
|
|
1985
|
+
if (closed) return;
|
|
1986
|
+
closed = true;
|
|
1987
|
+
writer.abort(error).catch(() => void 0);
|
|
1988
|
+
}
|
|
1989
|
+
return {
|
|
1990
|
+
stream: transform.readable,
|
|
1991
|
+
close,
|
|
1992
|
+
dispose: () => fail(),
|
|
1993
|
+
fail
|
|
2027
1994
|
};
|
|
2028
1995
|
}
|
|
2029
1996
|
//#endregion
|
|
1997
|
+
//#region src/rpc.ts
|
|
1998
|
+
/**
|
|
1999
|
+
* The only methods Studio may call on an agent. A `StudioSession` carries far more than
|
|
2000
|
+
* {@link AgentApi}, so it is wrapped rather than exposed: what Cap'n Web can reach is exactly what
|
|
2001
|
+
* this class re-declares.
|
|
2002
|
+
*/
|
|
2003
|
+
var AgentRpcTarget = class extends RpcTarget {
|
|
2004
|
+
api;
|
|
2005
|
+
constructor(api) {
|
|
2006
|
+
super();
|
|
2007
|
+
this.api = api;
|
|
2008
|
+
}
|
|
2009
|
+
connect() {
|
|
2010
|
+
return this.api.connect();
|
|
2011
|
+
}
|
|
2012
|
+
startGeneration(input) {
|
|
2013
|
+
return this.api.startGeneration(input);
|
|
2014
|
+
}
|
|
2015
|
+
saveConfig(input) {
|
|
2016
|
+
return this.api.saveConfig(input);
|
|
2017
|
+
}
|
|
2018
|
+
publishSnapshot(input) {
|
|
2019
|
+
return this.api.publishSnapshot(input);
|
|
2020
|
+
}
|
|
2021
|
+
readFiles(input) {
|
|
2022
|
+
return this.api.readFiles(input);
|
|
2023
|
+
}
|
|
2024
|
+
};
|
|
2025
|
+
/**
|
|
2026
|
+
* Opens an authenticated Cap'n Web session to Studio over a WebSocket. Rejects an unencrypted URL
|
|
2027
|
+
* before opening the socket, so a bearer token never reaches a plaintext host.
|
|
2028
|
+
*
|
|
2029
|
+
* @example
|
|
2030
|
+
* ```ts
|
|
2031
|
+
* const rpc = await connectWebSocketRpc({ url: 'wss://studio.kubb.dev/s/1', token, local: session })
|
|
2032
|
+
* await rpc.studio.ping()
|
|
2033
|
+
* ```
|
|
2034
|
+
*/
|
|
2035
|
+
const connectWebSocketRpc = async ({ url, token, local }) => {
|
|
2036
|
+
const { protocol, hostname, host } = new URL(url);
|
|
2037
|
+
if (protocol !== "wss:" && !(protocol === "ws:" && (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]"))) throw new Error(`Refusing unencrypted WebSocket to ${host}`);
|
|
2038
|
+
const socket = createWebsocket(url, { headers: { Authorization: `Bearer ${token}` } });
|
|
2039
|
+
const closed = new Promise((resolve) => socket.once("close", resolve));
|
|
2040
|
+
const studio = newWebSocketRpcSession(socket, new AgentRpcTarget(local));
|
|
2041
|
+
studio.onRpcBroken(() => socket.close());
|
|
2042
|
+
return {
|
|
2043
|
+
studio,
|
|
2044
|
+
closed,
|
|
2045
|
+
close: () => studio[Symbol.dispose]()
|
|
2046
|
+
};
|
|
2047
|
+
};
|
|
2048
|
+
//#endregion
|
|
2030
2049
|
//#region src/StudioSession.ts
|
|
2031
2050
|
/**
|
|
2032
|
-
* How
|
|
2033
|
-
* on for this open. A Studio that predates the ack never sends one, so this only ever produces a
|
|
2034
|
-
* warning, not a reconnect.
|
|
2051
|
+
* How many files are read from storage at once when serving `readFiles` or packing a snapshot.
|
|
2035
2052
|
*/
|
|
2036
|
-
const
|
|
2053
|
+
const FILE_READ_CONCURRENCY = 50;
|
|
2054
|
+
var GenerationRunTarget = class extends RpcTarget {
|
|
2055
|
+
generationStream;
|
|
2056
|
+
generationResult;
|
|
2057
|
+
cancelGeneration;
|
|
2058
|
+
stopGeneration;
|
|
2059
|
+
constructor(generationStream, generationResult, cancelGeneration, stopGeneration) {
|
|
2060
|
+
super();
|
|
2061
|
+
this.generationStream = generationStream;
|
|
2062
|
+
this.generationResult = generationResult;
|
|
2063
|
+
this.cancelGeneration = cancelGeneration;
|
|
2064
|
+
this.stopGeneration = stopGeneration;
|
|
2065
|
+
}
|
|
2066
|
+
async events() {
|
|
2067
|
+
return this.generationStream;
|
|
2068
|
+
}
|
|
2069
|
+
result() {
|
|
2070
|
+
return this.generationResult;
|
|
2071
|
+
}
|
|
2072
|
+
cancel() {
|
|
2073
|
+
return this.cancelGeneration();
|
|
2074
|
+
}
|
|
2075
|
+
[Symbol.dispose]() {
|
|
2076
|
+
this.stopGeneration();
|
|
2077
|
+
}
|
|
2078
|
+
};
|
|
2037
2079
|
/**
|
|
2038
2080
|
* Fills in a host's options: the hosted Studio URL, the current working directory, and every
|
|
2039
2081
|
* permission off unless granted. Idempotent, so a reconnect can pass an already-resolved bag
|
|
@@ -2051,6 +2093,7 @@ function applyStudioDefaults(options) {
|
|
|
2051
2093
|
allowConfigEdit: false,
|
|
2052
2094
|
allowInput: false,
|
|
2053
2095
|
allowExec: false,
|
|
2096
|
+
allowRead: false,
|
|
2054
2097
|
...options.permissions
|
|
2055
2098
|
},
|
|
2056
2099
|
retryInterval: options.retryInterval ?? agentDefaults.retryIntervalMs,
|
|
@@ -2072,7 +2115,7 @@ function reconnect(options) {
|
|
|
2072
2115
|
const timer = setTimeout(() => {
|
|
2073
2116
|
signal?.removeEventListener("abort", cancel);
|
|
2074
2117
|
if (signal?.aborted) return;
|
|
2075
|
-
new StudioSession(options).
|
|
2118
|
+
new StudioSession(options).start().catch((error) => {
|
|
2076
2119
|
if (logLevel$1 !== void 0 && logLevel$1 > logLevel.silent) console.error(styleText("red", `Reconnect attempt to Kubb Studio failed: ${getErrorMessage(error)}`));
|
|
2077
2120
|
if (error instanceof InvalidAgentTokenError) {
|
|
2078
2121
|
onTokenRejected?.(error);
|
|
@@ -2084,8 +2127,8 @@ function reconnect(options) {
|
|
|
2084
2127
|
signal?.addEventListener("abort", cancel, { once: true });
|
|
2085
2128
|
}
|
|
2086
2129
|
/**
|
|
2087
|
-
* One
|
|
2088
|
-
*
|
|
2130
|
+
* One agent-to-Studio RPC transport: opening it, keeping it alive, and serving remote methods.
|
|
2131
|
+
* `createClient` opens one per pool slot and is the only caller.
|
|
2089
2132
|
*/
|
|
2090
2133
|
var StudioSession = class {
|
|
2091
2134
|
#options;
|
|
@@ -2100,17 +2143,20 @@ var StudioSession = class {
|
|
|
2100
2143
|
* Before it resolves there is nothing to disconnect and no sandbox flag to read.
|
|
2101
2144
|
*/
|
|
2102
2145
|
#session;
|
|
2103
|
-
#
|
|
2146
|
+
#rpc;
|
|
2104
2147
|
#studioVersion;
|
|
2105
|
-
#activeJobId = null;
|
|
2106
2148
|
#disposed = false;
|
|
2107
2149
|
#isGenerating = false;
|
|
2108
2150
|
#heartbeatTimer;
|
|
2109
|
-
#readyTimer;
|
|
2110
|
-
#lastPongAt = Date.now();
|
|
2111
2151
|
#lastGeneration;
|
|
2152
|
+
/**
|
|
2153
|
+
* Resolves when Studio calls {@link StudioSession.connect}. `studio:ready` waits on this so the
|
|
2154
|
+
* host does not queue jobs before the agent session is registered.
|
|
2155
|
+
*/
|
|
2156
|
+
#connectAck = Promise.withResolvers();
|
|
2112
2157
|
constructor(options) {
|
|
2113
2158
|
this.#options = applyStudioDefaults(options);
|
|
2159
|
+
this.#connectAck.promise.catch(() => {});
|
|
2114
2160
|
}
|
|
2115
2161
|
/**
|
|
2116
2162
|
* A sandbox agent runs on Studio's own infrastructure, so it has no user project to touch.
|
|
@@ -2131,7 +2177,13 @@ var StudioSession = class {
|
|
|
2131
2177
|
get #canUseInput() {
|
|
2132
2178
|
return this.#isSandbox || this.#options.permissions.allowInput;
|
|
2133
2179
|
}
|
|
2134
|
-
|
|
2180
|
+
/**
|
|
2181
|
+
* A sandbox agent always allows reading its output back; a local agent only when opted in.
|
|
2182
|
+
*/
|
|
2183
|
+
get #canRead() {
|
|
2184
|
+
return this.#isSandbox || this.#options.permissions.allowRead;
|
|
2185
|
+
}
|
|
2186
|
+
async start() {
|
|
2135
2187
|
const { token, studioUrl, signal, heartbeatInterval, installLogger } = this.#options;
|
|
2136
2188
|
await installLogger?.(this.#hooks);
|
|
2137
2189
|
try {
|
|
@@ -2142,48 +2194,65 @@ var StudioSession = class {
|
|
|
2142
2194
|
});
|
|
2143
2195
|
this.#session = session;
|
|
2144
2196
|
this.#studioVersion = session.version;
|
|
2145
|
-
const
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
this.#
|
|
2197
|
+
const rpc = await (this.#options.connector ?? connectWebSocketRpc)({
|
|
2198
|
+
url: session.url,
|
|
2199
|
+
token,
|
|
2200
|
+
local: this
|
|
2201
|
+
});
|
|
2202
|
+
this.#rpc = rpc;
|
|
2203
|
+
rpc.closed.then(this.#onClose);
|
|
2151
2204
|
signal?.addEventListener("abort", this.#onAbort, { once: true });
|
|
2152
2205
|
this.#unhooks.push(() => signal?.removeEventListener("abort", this.#onAbort));
|
|
2153
|
-
this.#
|
|
2154
|
-
this.#
|
|
2206
|
+
this.#scheduleHeartbeat(heartbeatInterval);
|
|
2207
|
+
await this.#hooks.callHook("studio:connected", {
|
|
2208
|
+
url: studioUrl,
|
|
2209
|
+
versions: {
|
|
2210
|
+
studio: this.#studioVersion,
|
|
2211
|
+
kubb: version,
|
|
2212
|
+
agent: this.#options.version
|
|
2213
|
+
}
|
|
2214
|
+
});
|
|
2215
|
+
await this.#connectAck.promise;
|
|
2216
|
+
await this.#hooks.callHook("studio:ready", {});
|
|
2155
2217
|
} catch (error) {
|
|
2218
|
+
this.#disposed = true;
|
|
2219
|
+
this.dispose();
|
|
2156
2220
|
await this.#hooks.callHook("studio:error", { error: toError(error) });
|
|
2157
2221
|
if (error instanceof InvalidAgentTokenError) throw error;
|
|
2158
2222
|
reconnect(this.#options);
|
|
2159
2223
|
}
|
|
2160
2224
|
}
|
|
2161
|
-
/**
|
|
2162
|
-
* Adds a socket listener and tracks its remover, so `dispose` detaches every listener at once.
|
|
2163
|
-
*/
|
|
2164
|
-
#listen(ws, event, listener) {
|
|
2165
|
-
ws.addEventListener(event, listener);
|
|
2166
|
-
this.#unhooks.push(() => ws.removeEventListener(event, listener));
|
|
2167
|
-
}
|
|
2168
2225
|
#warn(message) {
|
|
2169
2226
|
return this.#hooks.callHook("studio:warn", { message });
|
|
2170
2227
|
}
|
|
2171
2228
|
/**
|
|
2172
|
-
*
|
|
2173
|
-
*
|
|
2229
|
+
* Declines a request: logs why locally, then tells Studio. The two wordings differ on purpose,
|
|
2230
|
+
* since the log names the request that was ignored and the error names what the caller can do.
|
|
2174
2231
|
*/
|
|
2175
|
-
#
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2232
|
+
async #refuse(reason, message) {
|
|
2233
|
+
await this.#warn(reason);
|
|
2234
|
+
throw new Error(message);
|
|
2235
|
+
}
|
|
2236
|
+
#scheduleHeartbeat(interval) {
|
|
2237
|
+
const rpc = this.#rpc;
|
|
2238
|
+
if (!rpc) return;
|
|
2239
|
+
this.#heartbeatTimer = setTimeout(async () => {
|
|
2240
|
+
try {
|
|
2241
|
+
await this.#ping(rpc);
|
|
2242
|
+
} catch {
|
|
2243
|
+
if (this.#rpc === rpc) rpc.close();
|
|
2244
|
+
return;
|
|
2245
|
+
}
|
|
2246
|
+
if (this.#rpc === rpc) this.#scheduleHeartbeat(interval);
|
|
2247
|
+
}, interval);
|
|
2248
|
+
}
|
|
2249
|
+
/**
|
|
2250
|
+
* Races `studio.ping()` against a deadline, so a half-open socket can't hang it forever.
|
|
2251
|
+
* */
|
|
2252
|
+
#ping(rpc) {
|
|
2253
|
+
const { promise: timedOut, reject: onTimeout } = Promise.withResolvers();
|
|
2254
|
+
const timer = setTimeout(() => onTimeout(/* @__PURE__ */ new Error("Heartbeat ping timed out")), agentDefaults.heartbeatTimeoutMs);
|
|
2255
|
+
return Promise.race([rpc.studio.ping(), timedOut]).finally(() => clearTimeout(timer));
|
|
2187
2256
|
}
|
|
2188
2257
|
/**
|
|
2189
2258
|
* Reads `kubb.config.ts` and reports which plugin options Studio may edit.
|
|
@@ -2200,89 +2269,48 @@ var StudioSession = class {
|
|
|
2200
2269
|
return;
|
|
2201
2270
|
}
|
|
2202
2271
|
}
|
|
2203
|
-
async
|
|
2272
|
+
async connect() {
|
|
2204
2273
|
const { configPath, root, version: version$1, loadConfig, permissions } = this.#options;
|
|
2205
|
-
|
|
2206
|
-
const
|
|
2207
|
-
sendAgentMessage(this.#ws, {
|
|
2208
|
-
type: "agent:connect",
|
|
2209
|
-
payload: {
|
|
2210
|
-
versions: {
|
|
2211
|
-
kubb: version,
|
|
2212
|
-
agent: version$1
|
|
2213
|
-
},
|
|
2214
|
-
root,
|
|
2215
|
-
config: {
|
|
2216
|
-
path: configPath,
|
|
2217
|
-
file: await this.#readConfigFileView(),
|
|
2218
|
-
plugins: config.plugins.map((plugin) => ({
|
|
2219
|
-
name: toPackageName(plugin.name),
|
|
2220
|
-
options: plugin.options ?? {}
|
|
2221
|
-
}))
|
|
2222
|
-
},
|
|
2223
|
-
permissions: {
|
|
2224
|
-
...permissions,
|
|
2225
|
-
allowWrite: this.#canWrite,
|
|
2226
|
-
allowInput: this.#canUseInput,
|
|
2227
|
-
allowConfigEdit: this.#canEditConfig
|
|
2228
|
-
}
|
|
2229
|
-
}
|
|
2230
|
-
});
|
|
2231
|
-
}
|
|
2232
|
-
async #handleOpen() {
|
|
2233
|
-
this.#lastPongAt = Date.now();
|
|
2234
|
-
await this.#hooks.callHook("studio:connected", {
|
|
2235
|
-
url: this.#options.studioUrl,
|
|
2274
|
+
const [config, file] = await Promise.all([loadConfig(), this.#readConfigFileView()]);
|
|
2275
|
+
const payload = {
|
|
2236
2276
|
versions: {
|
|
2237
|
-
studio: this.#studioVersion,
|
|
2238
2277
|
kubb: version,
|
|
2239
|
-
agent:
|
|
2278
|
+
agent: version$1
|
|
2279
|
+
},
|
|
2280
|
+
root,
|
|
2281
|
+
config: {
|
|
2282
|
+
path: configPath,
|
|
2283
|
+
file,
|
|
2284
|
+
plugins: config.plugins.map((plugin) => ({
|
|
2285
|
+
name: toPackageName(plugin.name),
|
|
2286
|
+
options: plugin.options ?? {}
|
|
2287
|
+
}))
|
|
2288
|
+
},
|
|
2289
|
+
permissions: {
|
|
2290
|
+
...permissions,
|
|
2291
|
+
allowWrite: this.#canWrite,
|
|
2292
|
+
allowInput: this.#canUseInput,
|
|
2293
|
+
allowConfigEdit: this.#canEditConfig,
|
|
2294
|
+
allowRead: this.#canRead
|
|
2240
2295
|
}
|
|
2241
|
-
}
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
this.#waitForReady();
|
|
2245
|
-
} catch (error) {
|
|
2246
|
-
await this.#warn(`Failed to send the connect payload: ${getErrorMessage(error)}`);
|
|
2247
|
-
}
|
|
2296
|
+
};
|
|
2297
|
+
this.#connectAck.resolve();
|
|
2298
|
+
return payload;
|
|
2248
2299
|
}
|
|
2249
|
-
|
|
2250
|
-
* Arms the timeout for Studio's `studio:ready` acknowledgement. Re-armed on every open, since a
|
|
2251
|
-
* command sent while the agent is not attached is lost either way (see `#sendConnectedPayload`),
|
|
2252
|
-
* so each reconnect needs its own fresh wait.
|
|
2253
|
-
*/
|
|
2254
|
-
#waitForReady() {
|
|
2255
|
-
if (this.#disposed) return;
|
|
2256
|
-
clearTimeout(this.#readyTimer);
|
|
2257
|
-
this.#readyTimer = setTimeout(() => {
|
|
2258
|
-
this.#readyTimer = void 0;
|
|
2259
|
-
Promise.resolve(this.#warn(`Kubb Studio did not confirm the connection was ready within ${READY_TIMEOUT_MS}ms`)).catch(() => {});
|
|
2260
|
-
}, READY_TIMEOUT_MS);
|
|
2261
|
-
}
|
|
2262
|
-
#onOpen = () => void this.#handleOpen().catch(() => {});
|
|
2263
|
-
#onAbort = () => void this.#end({
|
|
2264
|
-
reason: "shutdown",
|
|
2265
|
-
retry: false
|
|
2266
|
-
});
|
|
2300
|
+
#onAbort = () => void this.#end({ retry: false });
|
|
2267
2301
|
#onClose = () => void this.#end({ retry: true });
|
|
2268
|
-
#onError = () => {
|
|
2269
|
-
this.#hooks.callHook("studio:error", { error: /* @__PURE__ */ new Error("Failed to connect to Kubb Studio") });
|
|
2270
|
-
this.#onClose();
|
|
2271
|
-
};
|
|
2272
2302
|
/**
|
|
2273
2303
|
* Drops the socket and detaches every listener and timer this session added. Idempotent, and
|
|
2274
2304
|
* safe before `connect` opened anything.
|
|
2275
2305
|
*
|
|
2276
2306
|
* @internal
|
|
2277
2307
|
*/
|
|
2278
|
-
dispose(
|
|
2279
|
-
|
|
2308
|
+
dispose() {
|
|
2309
|
+
clearTimeout(this.#heartbeatTimer);
|
|
2280
2310
|
this.#heartbeatTimer = void 0;
|
|
2281
|
-
|
|
2282
|
-
this.#
|
|
2283
|
-
|
|
2284
|
-
this.#ws?.close(1e3, reason);
|
|
2285
|
-
} catch {}
|
|
2311
|
+
this.#rpc?.close();
|
|
2312
|
+
this.#rpc = void 0;
|
|
2313
|
+
this.#connectAck.reject(/* @__PURE__ */ new Error("Session ended before Studio called connect()"));
|
|
2286
2314
|
for (const unhook of this.#unhooks) unhook();
|
|
2287
2315
|
this.#unhooks.length = 0;
|
|
2288
2316
|
}
|
|
@@ -2290,15 +2318,12 @@ var StudioSession = class {
|
|
|
2290
2318
|
* Ends the session: tells Studio it is over, drops the socket, and optionally reconnects.
|
|
2291
2319
|
* `#disposed` keeps the close event from running this twice, and a shutdown from reconnecting.
|
|
2292
2320
|
*/
|
|
2293
|
-
async #end({
|
|
2321
|
+
async #end({ retry }) {
|
|
2294
2322
|
const { studioUrl, token, logLevel } = this.#options;
|
|
2295
2323
|
if (this.#disposed) return;
|
|
2296
2324
|
this.#disposed = true;
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
reason: "shutdown"
|
|
2300
|
-
});
|
|
2301
|
-
this.dispose(reason);
|
|
2325
|
+
this.dispose();
|
|
2326
|
+
await this.#hooks.callHook("studio:disconnected", { reason: retry ? "connection closed" : "shutdown" });
|
|
2302
2327
|
if (this.#session) await disconnect({
|
|
2303
2328
|
sessionId: this.#session.sessionId,
|
|
2304
2329
|
studioUrl,
|
|
@@ -2308,78 +2333,35 @@ var StudioSession = class {
|
|
|
2308
2333
|
}).catch(() => {});
|
|
2309
2334
|
if (retry) reconnect(this.#options);
|
|
2310
2335
|
}
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
return;
|
|
2331
|
-
}
|
|
2332
|
-
await this.#warn(`Ignored an unknown message from Kubb Studio: ${data.type}`);
|
|
2333
|
-
} catch (error) {
|
|
2334
|
-
await this.#hooks.callHook("studio:error", { error: toError(error) });
|
|
2335
|
-
await this.#emitError(toError(error));
|
|
2336
|
-
}
|
|
2337
|
-
};
|
|
2338
|
-
/**
|
|
2339
|
-
* Studio ended the session itself. A revoked one stays ended, an expired one gets a fresh
|
|
2340
|
-
* session, and anything else is left to the socket's own close event.
|
|
2341
|
-
*/
|
|
2342
|
-
async #handleDisconnect(reason) {
|
|
2343
|
-
await this.#hooks.callHook("studio:disconnected", { reason });
|
|
2344
|
-
if (reason !== "revoked" && reason !== "expired") return;
|
|
2345
|
-
this.#disposed = true;
|
|
2346
|
-
this.dispose(`session_${reason}`);
|
|
2347
|
-
if (reason === "expired") reconnect(this.#options);
|
|
2348
|
-
}
|
|
2349
|
-
async #handleCommand(data) {
|
|
2350
|
-
const ws = this.#ws;
|
|
2351
|
-
if (!ws) return;
|
|
2352
|
-
const command = data.type.slice(7);
|
|
2353
|
-
await this.#hooks.callHook("studio:command:start", { command });
|
|
2354
|
-
switch (data.type) {
|
|
2355
|
-
case "studio:generate":
|
|
2356
|
-
await this.#handleGenerate(ws, data, command);
|
|
2357
|
-
return;
|
|
2358
|
-
case "studio:connect":
|
|
2359
|
-
this.#studioVersion = data.version ?? this.#studioVersion;
|
|
2360
|
-
await this.#sendConnectedPayload();
|
|
2361
|
-
await this.#hooks.callHook("studio:command:end", { command });
|
|
2362
|
-
return;
|
|
2363
|
-
case "studio:save":
|
|
2364
|
-
await this.#handleSave(ws, data, command);
|
|
2365
|
-
return;
|
|
2366
|
-
case "studio:snapshot":
|
|
2367
|
-
await this.#handleSnapshot(ws, data, command);
|
|
2368
|
-
return;
|
|
2369
|
-
}
|
|
2336
|
+
startGeneration(data) {
|
|
2337
|
+
const generationStream = createGenerationStream(this.#hooks, data.jobId, { onGenerationEnd: (result) => {
|
|
2338
|
+
this.#lastGeneration = result;
|
|
2339
|
+
} });
|
|
2340
|
+
const controller = new AbortController();
|
|
2341
|
+
const result = this.#runGeneration(data, controller).then(async (value) => {
|
|
2342
|
+
await generationStream.close();
|
|
2343
|
+
return value;
|
|
2344
|
+
}).catch((error) => {
|
|
2345
|
+
generationStream.fail(error);
|
|
2346
|
+
throw error;
|
|
2347
|
+
});
|
|
2348
|
+
result.catch(() => {});
|
|
2349
|
+
return new GenerationRunTarget(generationStream.stream, result, async () => {
|
|
2350
|
+
controller.abort(/* @__PURE__ */ new Error("Generation canceled"));
|
|
2351
|
+
}, () => {
|
|
2352
|
+
controller.abort(/* @__PURE__ */ new Error("Generation canceled"));
|
|
2353
|
+
generationStream.dispose();
|
|
2354
|
+
});
|
|
2370
2355
|
}
|
|
2371
|
-
async #
|
|
2372
|
-
|
|
2373
|
-
if (this.#isGenerating) {
|
|
2374
|
-
await this.#warn("Ignored generate: a generation is already in progress");
|
|
2375
|
-
await this.#emitError(/* @__PURE__ */ new Error("A generation is already in progress, please wait for it to finish"));
|
|
2376
|
-
return;
|
|
2377
|
-
}
|
|
2356
|
+
async #runGeneration(data, controller) {
|
|
2357
|
+
if (this.#isGenerating) return this.#refuse("Ignored generate: a generation is already in progress", "A generation is already in progress, please wait for it to finish");
|
|
2378
2358
|
this.#isGenerating = true;
|
|
2379
|
-
|
|
2359
|
+
const command = "generate";
|
|
2360
|
+
const { root, loadConfig, permissions, client } = this.#options;
|
|
2380
2361
|
try {
|
|
2362
|
+
await this.#hooks.callHook("studio:command:start", { command });
|
|
2381
2363
|
const config = await loadConfig();
|
|
2382
|
-
const patch = data.
|
|
2364
|
+
const patch = data.config;
|
|
2383
2365
|
const plugins = await mergePlugins(config.plugins, patch?.plugins);
|
|
2384
2366
|
const adapter = await mergeAdapter(config.adapter, patch?.adapter);
|
|
2385
2367
|
const inputOverride = this.#isSandbox ? patch?.input ?? "" : permissions.allowInput && patch?.input || void 0;
|
|
@@ -2389,13 +2371,8 @@ var StudioSession = class {
|
|
|
2389
2371
|
await this.#warn(`Ignored the spec from Studio; set ${remedy} to generate from it`);
|
|
2390
2372
|
}
|
|
2391
2373
|
const resolvedPlugins = plugins ?? config.plugins;
|
|
2392
|
-
|
|
2393
|
-
const detach = [setupHookListener(this.#hooks, root
|
|
2394
|
-
skipStorage: client?.kind === "ci",
|
|
2395
|
-
onGenerationEnd: (files) => {
|
|
2396
|
-
generatedFiles = files;
|
|
2397
|
-
}
|
|
2398
|
-
})];
|
|
2374
|
+
this.#lastGeneration = void 0;
|
|
2375
|
+
const detach = [setupHookListener(this.#hooks, root, controller.signal)];
|
|
2399
2376
|
try {
|
|
2400
2377
|
await generate({
|
|
2401
2378
|
config: {
|
|
@@ -2412,140 +2389,155 @@ var StudioSession = class {
|
|
|
2412
2389
|
plugins: resolvedPlugins,
|
|
2413
2390
|
adapter
|
|
2414
2391
|
},
|
|
2415
|
-
hooks: this.#hooks
|
|
2392
|
+
hooks: this.#hooks,
|
|
2393
|
+
signal: controller.signal
|
|
2416
2394
|
});
|
|
2417
2395
|
} finally {
|
|
2418
2396
|
for (const remove of detach) remove();
|
|
2419
|
-
this.#lastGeneration = generatedFiles;
|
|
2420
2397
|
}
|
|
2421
2398
|
await this.#hooks.callHook("studio:command:end", {
|
|
2422
2399
|
command,
|
|
2423
2400
|
info: `${resolvedPlugins.length} plugin${resolvedPlugins.length === 1 ? "" : "s"}, ${this.#canWrite ? "written to disk" : "in memory"}${inputOverride !== void 0 ? ", from a Studio spec" : ""}`
|
|
2424
2401
|
});
|
|
2402
|
+
const files = [...this.#lastGeneration?.paths ?? []];
|
|
2403
|
+
return {
|
|
2404
|
+
status: "success",
|
|
2405
|
+
files,
|
|
2406
|
+
fileCount: files.length
|
|
2407
|
+
};
|
|
2425
2408
|
} finally {
|
|
2426
2409
|
this.#isGenerating = false;
|
|
2427
|
-
this.#activeJobId = null;
|
|
2428
2410
|
}
|
|
2429
2411
|
}
|
|
2430
|
-
async
|
|
2412
|
+
async saveConfig(data) {
|
|
2413
|
+
const command = "saveConfig";
|
|
2414
|
+
await this.#hooks.callHook("studio:command:start", { command });
|
|
2431
2415
|
const { configPath, configFile } = this.#options;
|
|
2432
2416
|
if (!Array.isArray(data.edits)) {
|
|
2433
2417
|
await this.#warn("Ignored save: the message carried no edits");
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
outcomes: [],
|
|
2439
|
-
changed: false
|
|
2440
|
-
}
|
|
2441
|
-
});
|
|
2442
|
-
return;
|
|
2418
|
+
return {
|
|
2419
|
+
outcomes: [],
|
|
2420
|
+
changed: false
|
|
2421
|
+
};
|
|
2443
2422
|
}
|
|
2444
2423
|
const edits = data.edits;
|
|
2445
|
-
const refuse = (reason) =>
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
reason
|
|
2453
|
-
})),
|
|
2454
|
-
changed: false
|
|
2455
|
-
}
|
|
2424
|
+
const refuse = (reason) => ({
|
|
2425
|
+
outcomes: edits.map((edit) => ({
|
|
2426
|
+
edit,
|
|
2427
|
+
applied: false,
|
|
2428
|
+
reason
|
|
2429
|
+
})),
|
|
2430
|
+
changed: false
|
|
2456
2431
|
});
|
|
2457
2432
|
if (!this.#canEditConfig) {
|
|
2458
2433
|
await this.#warn("Ignored save: editing kubb.config.ts was not granted");
|
|
2459
|
-
refuse("the agent was not granted permission to edit kubb.config.ts");
|
|
2460
|
-
return;
|
|
2461
|
-
}
|
|
2462
|
-
if (this.#isGenerating) {
|
|
2463
|
-
refuse("a generation is in progress");
|
|
2464
|
-
return;
|
|
2434
|
+
return refuse("the agent was not granted permission to edit kubb.config.ts");
|
|
2465
2435
|
}
|
|
2436
|
+
if (this.#isGenerating) return refuse("a generation is in progress");
|
|
2466
2437
|
try {
|
|
2467
2438
|
const { source: patched, outcomes, changed } = applyConfigEdits(await read(configFile), edits);
|
|
2468
2439
|
if (changed) await writeFile(configFile, patched, "utf-8");
|
|
2469
|
-
sendAgentMessage(ws, {
|
|
2470
|
-
type: "agent:save",
|
|
2471
|
-
jobId: data.jobId,
|
|
2472
|
-
payload: {
|
|
2473
|
-
outcomes,
|
|
2474
|
-
changed,
|
|
2475
|
-
file: changed ? await this.#readConfigFileView(patched) : void 0
|
|
2476
|
-
}
|
|
2477
|
-
});
|
|
2478
2440
|
const applied = outcomes.filter((outcome) => outcome.applied).length;
|
|
2479
2441
|
await this.#hooks.callHook("studio:command:end", {
|
|
2480
2442
|
command,
|
|
2481
2443
|
info: `applied ${applied}/${outcomes.length} edits to ${configPath}`
|
|
2482
2444
|
});
|
|
2445
|
+
return {
|
|
2446
|
+
outcomes,
|
|
2447
|
+
changed,
|
|
2448
|
+
file: changed ? await this.#readConfigFileView(patched) : void 0
|
|
2449
|
+
};
|
|
2483
2450
|
} catch (error) {
|
|
2484
2451
|
await this.#hooks.callHook("studio:error", { error: toError(error) });
|
|
2485
|
-
refuse(getErrorMessage(error));
|
|
2452
|
+
return refuse(getErrorMessage(error));
|
|
2486
2453
|
}
|
|
2487
2454
|
}
|
|
2488
|
-
async
|
|
2489
|
-
const
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
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
|
-
}
|
|
2455
|
+
async publishSnapshot(data) {
|
|
2456
|
+
const command = "snapshot";
|
|
2457
|
+
await this.#hooks.callHook("studio:command:start", { command });
|
|
2458
|
+
if (this.#isSandbox) return this.#refuse("Ignored snapshot: a sandbox agent has no project to build a package from", "A sandbox agent has no project to build a package from");
|
|
2459
|
+
const { name, version, bundledDependencies, uploadPath } = data;
|
|
2460
|
+
if (!name || !version || !uploadPath) return this.#refuse("Ignored snapshot: the message was missing required fields", "The request was missing required fields");
|
|
2461
|
+
const generation = this.#lastGeneration;
|
|
2462
|
+
if (!generation) return this.#refuse("Ignored snapshot: no prior generation to pack", "No prior generation exists to pack, run a generation first");
|
|
2463
|
+
const bundled = new Set(bundledDependencies ?? []);
|
|
2464
|
+
const missing = generation.missingDependencies.filter((dependency) => !bundled.has(dependency));
|
|
2465
|
+
if (missing.length) return this.#refuse(`Ignored snapshot: missing dependencies: ${missing.join(", ")}`, `Missing dependencies: ${missing.join(", ")}`);
|
|
2514
2466
|
try {
|
|
2467
|
+
const files = {};
|
|
2468
|
+
await inParallel({
|
|
2469
|
+
items: [...generation.paths],
|
|
2470
|
+
limit: FILE_READ_CONCURRENCY,
|
|
2471
|
+
run: async (relativePath) => {
|
|
2472
|
+
const content = await generation.storage.readItem(absoluteStoragePath(generation.root, relativePath));
|
|
2473
|
+
if (content !== null) files[relativePath] = content;
|
|
2474
|
+
}
|
|
2475
|
+
});
|
|
2515
2476
|
const { bytes, integrity } = await createSnapshotPackage(files, {
|
|
2516
2477
|
name,
|
|
2517
2478
|
version,
|
|
2518
|
-
peerDependencies: peerDependencies
|
|
2479
|
+
peerDependencies: generation.peerDependencies
|
|
2519
2480
|
});
|
|
2520
2481
|
const { token, studioUrl } = this.#options;
|
|
2521
|
-
const
|
|
2482
|
+
const uploadUrl = new URL(uploadPath, studioUrl);
|
|
2483
|
+
if (uploadUrl.origin !== new URL(studioUrl).origin) throw new Error("Snapshot upload path must stay on the Studio origin");
|
|
2484
|
+
const redirect = await fetch(uploadUrl, {
|
|
2522
2485
|
method: "PUT",
|
|
2523
2486
|
headers: { Authorization: `Bearer ${token}` },
|
|
2524
2487
|
redirect: "manual"
|
|
2525
2488
|
});
|
|
2526
2489
|
const storageUrl = redirect.headers.get("location");
|
|
2527
2490
|
if (redirect.status !== 307 || !storageUrl) throw new Error(`Studio did not provide a storage URL (status ${redirect.status})`);
|
|
2528
|
-
const
|
|
2491
|
+
const storage = new URL(storageUrl);
|
|
2492
|
+
if (storage.protocol !== "https:" && storage.hostname !== "localhost" && storage.hostname !== "127.0.0.1") throw new Error(`Refusing snapshot upload to ${storage.origin}`);
|
|
2493
|
+
const response = await fetch(storage, {
|
|
2529
2494
|
method: "PUT",
|
|
2530
|
-
body: new Uint8Array(bytes)
|
|
2495
|
+
body: new Uint8Array(bytes),
|
|
2496
|
+
redirect: "error"
|
|
2531
2497
|
});
|
|
2532
2498
|
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
2499
|
await this.#hooks.callHook("studio:command:end", {
|
|
2542
2500
|
command,
|
|
2543
2501
|
info: `packed ${Object.keys(files).length} file${Object.keys(files).length === 1 ? "" : "s"}`
|
|
2544
2502
|
});
|
|
2503
|
+
return {
|
|
2504
|
+
integrity,
|
|
2505
|
+
peerDependencies: generation.peerDependencies
|
|
2506
|
+
};
|
|
2545
2507
|
} catch (error) {
|
|
2546
2508
|
await this.#hooks.callHook("studio:error", { error: toError(error) });
|
|
2547
|
-
|
|
2509
|
+
throw error;
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
async readFiles(data) {
|
|
2513
|
+
const command = "readFiles";
|
|
2514
|
+
await this.#hooks.callHook("studio:command:start", { command });
|
|
2515
|
+
const { client } = this.#options;
|
|
2516
|
+
if (!this.#canRead) {
|
|
2517
|
+
await this.#warn("Ignored files: reading generated files was not granted");
|
|
2518
|
+
const remedy = client?.kind === "cli" ? "--allow-read, or answer yes when kubb studio asks," : "KUBB_AGENT_ALLOW_READ=true";
|
|
2519
|
+
throw new Error(`The agent was not granted permission to read generated files; set ${remedy} to allow it`);
|
|
2548
2520
|
}
|
|
2521
|
+
if (!Array.isArray(data.paths)) return this.#refuse("Ignored files: the message carried no paths", "The request carried no paths");
|
|
2522
|
+
const { paths } = data;
|
|
2523
|
+
if (paths.length > 50) return this.#refuse(`Ignored files: requested ${paths.length} paths, more than the 50 allowed per request`, `At most 50 paths may be requested at once`);
|
|
2524
|
+
const generation = this.#lastGeneration;
|
|
2525
|
+
if (!generation) return this.#refuse("Ignored files: no prior generation to read from", "No prior generation to read from, run a generation first");
|
|
2526
|
+
const requested = paths.filter((path) => generation.paths.has(path));
|
|
2527
|
+
const files = {};
|
|
2528
|
+
await inParallel({
|
|
2529
|
+
items: requested,
|
|
2530
|
+
limit: FILE_READ_CONCURRENCY,
|
|
2531
|
+
run: async (path) => {
|
|
2532
|
+
const content = await generation.storage.readItem(absoluteStoragePath(generation.root, path));
|
|
2533
|
+
if (content !== null) files[path] = content;
|
|
2534
|
+
}
|
|
2535
|
+
});
|
|
2536
|
+
await this.#hooks.callHook("studio:command:end", {
|
|
2537
|
+
command,
|
|
2538
|
+
info: `read ${Object.keys(files).length}/${paths.length} requested file${paths.length === 1 ? "" : "s"}`
|
|
2539
|
+
});
|
|
2540
|
+
return { files };
|
|
2549
2541
|
}
|
|
2550
2542
|
};
|
|
2551
2543
|
//#endregion
|
|
@@ -2582,7 +2574,7 @@ function createClient({ storage, onAuthRequired, ...options }) {
|
|
|
2582
2574
|
...options,
|
|
2583
2575
|
signal: controller.signal,
|
|
2584
2576
|
onTokenRejected: notifyAuthRequired
|
|
2585
|
-
}).
|
|
2577
|
+
}).start()));
|
|
2586
2578
|
},
|
|
2587
2579
|
disconnect() {
|
|
2588
2580
|
controller.abort();
|
|
@@ -2752,6 +2744,6 @@ async function pollForPairingToken({ studioUrl = agentDefaults.studioUrl, sessio
|
|
|
2752
2744
|
throw new Error("The pairing code expired, pair again");
|
|
2753
2745
|
}
|
|
2754
2746
|
//#endregion
|
|
2755
|
-
export { InvalidAgentTokenError, PairingCanceledError, createAgent, createClient, createFileStorage, createJob,
|
|
2747
|
+
export { InvalidAgentTokenError, PairingCanceledError, connectWebSocketRpc, createAgent, createClient, createFileStorage, createJob, defaultStudioUrl, generationEventTypes, machineTokenFrom, pollForPairingToken, runConnection, setStorage, startPairing, waitForJob };
|
|
2756
2748
|
|
|
2757
2749
|
//# sourceMappingURL=index.js.map
|