@kubb/studio 5.11.3 → 5.11.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/dist/index.cjs +144 -114
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +147 -117
- package/dist/index.js.map +1 -1
- package/dist/{protocol-DpxvWX9E.d.ts → protocol-a_DcTFba.d.cts} +8 -40
- package/dist/{protocol-DpxvWX9E.d.cts → protocol-a_DcTFba.d.ts} +8 -40
- package/dist/protocol.cjs +1 -12
- package/dist/protocol.cjs.map +1 -1
- package/dist/protocol.d.cts +2 -2
- package/dist/protocol.d.ts +2 -2
- package/dist/protocol.js +2 -12
- package/dist/protocol.js.map +1 -1
- package/package.json +4 -4
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { E as InstalledExtensions, N as PublishedSnapshot, U as SnapshotUploadMetadata, a as AgentConnectInfo, c as AgentPermissions, d as ConfigEditOutcome, s as AgentPermission, u as ConfigEdit } from "./protocol-a_DcTFba.cjs";
|
|
2
2
|
import { Config, Hookable, KubbHooks } from "@kubb/core";
|
|
3
3
|
//#region src/connect.d.ts
|
|
4
4
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { E as InstalledExtensions, N as PublishedSnapshot, U as SnapshotUploadMetadata, a as AgentConnectInfo, c as AgentPermissions, d as ConfigEditOutcome, s as AgentPermission, u as ConfigEdit } from "./protocol-a_DcTFba.js";
|
|
2
2
|
import { Config, Hookable, KubbHooks } from "@kubb/core";
|
|
3
3
|
//#region src/connect.d.ts
|
|
4
4
|
/**
|
package/dist/index.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { AGENT_INSTANCE_HEADER, AGENT_SOCKET_PATH, AgentCloseCode, AgentMessage, AgentPermission,
|
|
1
|
+
import { AGENT_INSTANCE_HEADER, AGENT_SOCKET_PATH, AgentCloseCode, AgentMessage, AgentPermission, MAX_SNAPSHOT_BYTES, generationEventTypes } from "./protocol.js";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { createHash, hash, randomUUID } from "node:crypto";
|
|
4
|
-
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
5
4
|
import { tmpdir } from "node:os";
|
|
6
5
|
import path, { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
7
6
|
import process$1 from "node:process";
|
|
8
7
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
9
8
|
import { pathToFileURL } from "node:url";
|
|
10
9
|
import { existsSync } from "node:fs";
|
|
11
|
-
import {
|
|
10
|
+
import { mkdir, mkdir as mkdir$1, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
11
|
+
import { Diagnostics, Hookable, cacheStorage, createKubb, fsStorage, getInputKind, isPackageName, readInstalledPackages, resolveCacheDir, resolveMeta } from "@kubb/core";
|
|
12
12
|
import { RpcTarget, newWebSocketRpcSession } from "capnweb";
|
|
13
13
|
import { detectCodeFormat, parseExpression, parseModule } from "magicast";
|
|
14
14
|
import { structuredPatch } from "diff";
|
|
@@ -117,6 +117,27 @@ async function read(path) {
|
|
|
117
117
|
return readFile(path, { encoding: "utf8" });
|
|
118
118
|
}
|
|
119
119
|
/**
|
|
120
|
+
* Writes UTF-8 content verbatim, including empty files and surrounding whitespace.
|
|
121
|
+
* Uses Bun.write under Bun and creates missing parent directories under either runtime.
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* await writeRaw('./cache/output.ts', ' // keep these bytes\n\n')
|
|
125
|
+
*/
|
|
126
|
+
async function writeRaw(path, content) {
|
|
127
|
+
const resolved = resolve(path);
|
|
128
|
+
if (runtime.isBun) {
|
|
129
|
+
await Bun.write(resolved, content);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
await writeFile(resolved, content, { encoding: "utf-8" });
|
|
134
|
+
} catch (error) {
|
|
135
|
+
if (error.code !== "ENOENT") throw error;
|
|
136
|
+
await mkdir(dirname(resolved), { recursive: true });
|
|
137
|
+
await writeFile(resolved, content, { encoding: "utf-8" });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
120
141
|
* Resolves to `true` when `path` is `parent` itself or nested inside it. Both sides are resolved
|
|
121
142
|
* to absolute paths first, so relative and `..`-containing inputs compare correctly.
|
|
122
143
|
*
|
|
@@ -175,7 +196,7 @@ function getElapsedMs(hrStart) {
|
|
|
175
196
|
}
|
|
176
197
|
//#endregion
|
|
177
198
|
//#region package.json
|
|
178
|
-
var version = "5.11.
|
|
199
|
+
var version = "5.11.4";
|
|
179
200
|
//#endregion
|
|
180
201
|
//#region src/resolveConfig.ts
|
|
181
202
|
/**
|
|
@@ -1371,21 +1392,20 @@ const hashOf = (content) => createHash("sha1").update(content).digest("hex").sli
|
|
|
1371
1392
|
/**
|
|
1372
1393
|
* The bytes `entries` keep on disk: output, written files, and kept input.
|
|
1373
1394
|
*/
|
|
1374
|
-
const totalBytes = (entries) => entries.reduce((sum, { output,
|
|
1395
|
+
const totalBytes = (entries) => entries.reduce((sum, { output, inputBytes }) => sum + output.bytes + (inputBytes ?? 0), 0);
|
|
1375
1396
|
function positiveNumber(value) {
|
|
1376
1397
|
const parsed = Number(value);
|
|
1377
1398
|
return value && Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
|
|
1378
1399
|
}
|
|
1379
1400
|
/**
|
|
1380
1401
|
* How many generations an agent keeps and how large they may get, read from
|
|
1381
|
-
* `KUBB_AGENT_MAX_GENERATIONS
|
|
1402
|
+
* `KUBB_AGENT_MAX_GENERATIONS` and `KUBB_AGENT_MAX_GENERATIONS_MB`.
|
|
1382
1403
|
* An unset or invalid value keeps the default.
|
|
1383
1404
|
*/
|
|
1384
1405
|
function resolveGenerationLimits() {
|
|
1385
1406
|
return {
|
|
1386
1407
|
maxCount: Math.max(1, Math.floor(positiveNumber(process$1.env.KUBB_AGENT_MAX_GENERATIONS) ?? 8)),
|
|
1387
|
-
maxMb: positiveNumber(process$1.env.KUBB_AGENT_MAX_GENERATIONS_MB) ?? 100
|
|
1388
|
-
maxSnapshotMb: positiveNumber(process$1.env.KUBB_AGENT_MAX_SNAPSHOT_MB) ?? 50
|
|
1408
|
+
maxMb: positiveNumber(process$1.env.KUBB_AGENT_MAX_GENERATIONS_MB) ?? 100
|
|
1389
1409
|
};
|
|
1390
1410
|
}
|
|
1391
1411
|
/**
|
|
@@ -1432,7 +1452,7 @@ async function generate({ config, hooks, signal, allowExec }) {
|
|
|
1432
1452
|
};
|
|
1433
1453
|
}
|
|
1434
1454
|
/**
|
|
1435
|
-
* What the output directory holds
|
|
1455
|
+
* What the output directory holds when the editor connects. `undefined` when `outputPath` is not a real
|
|
1436
1456
|
* subdirectory of `root` (listing the root would take in every source file) or holds more than `maxFiles`.
|
|
1437
1457
|
*/
|
|
1438
1458
|
async function listDisk({ root, outputPath, maxFiles }) {
|
|
@@ -1449,38 +1469,11 @@ async function listDisk({ root, outputPath, maxFiles }) {
|
|
|
1449
1469
|
paths: new Set(paths)
|
|
1450
1470
|
};
|
|
1451
1471
|
}
|
|
1452
|
-
async function readDiskFiles({ files: disk, paths, cursor = 0, limit }) {
|
|
1453
|
-
const files = {};
|
|
1454
|
-
const pages = {};
|
|
1455
|
-
await inParallel({
|
|
1456
|
-
items: paths.filter((path) => disk.paths.has(path)),
|
|
1457
|
-
limit: READ_CONCURRENCY,
|
|
1458
|
-
run: async (path) => {
|
|
1459
|
-
const content = await disk.storage.readItem(resolve(disk.root, path));
|
|
1460
|
-
if (content === null) return;
|
|
1461
|
-
if (limit === void 0) {
|
|
1462
|
-
files[path] = content;
|
|
1463
|
-
return;
|
|
1464
|
-
}
|
|
1465
|
-
const lines = content.split(/\r\n|\r|\n/);
|
|
1466
|
-
const end = Math.min(cursor + limit, lines.length);
|
|
1467
|
-
files[path] = lines.slice(cursor, end).join("\n");
|
|
1468
|
-
pages[path] = {
|
|
1469
|
-
nextCursor: end < lines.length ? end : null,
|
|
1470
|
-
totalLines: lines.length
|
|
1471
|
-
};
|
|
1472
|
-
}
|
|
1473
|
-
});
|
|
1474
|
-
return limit === void 0 ? { files } : {
|
|
1475
|
-
files,
|
|
1476
|
-
pages
|
|
1477
|
-
};
|
|
1478
|
-
}
|
|
1479
1472
|
/**
|
|
1480
1473
|
* Keeps recent generations by job id in `storage`, with an index next to them so a store on disk
|
|
1481
1474
|
* survives a restart. A generation is only ever looked up by its job id, so on a pooled sandbox one
|
|
1482
|
-
* tenant never reaches another's output.
|
|
1483
|
-
* always
|
|
1475
|
+
* tenant never reaches another's output. Initial disk output and generated jobs each get their
|
|
1476
|
+
* own `maxCount` and `maxMb` budget. Each budget always keeps its newest entry.
|
|
1484
1477
|
*/
|
|
1485
1478
|
function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now }) {
|
|
1486
1479
|
let index;
|
|
@@ -1499,7 +1492,7 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
|
|
|
1499
1492
|
/**
|
|
1500
1493
|
* Copies `files` into the store as one set of `jobId`. Above `maxSetMb` only the hashes are kept.
|
|
1501
1494
|
*/
|
|
1502
|
-
async function keep({ jobId,
|
|
1495
|
+
async function keep({ jobId, files, maxSetMb }) {
|
|
1503
1496
|
const maxSetBytes = maxSetMb * MB;
|
|
1504
1497
|
const hashes = {};
|
|
1505
1498
|
let bytes = 0;
|
|
@@ -1510,14 +1503,13 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
|
|
|
1510
1503
|
if (path.split("/").includes("..")) return;
|
|
1511
1504
|
const content = await files.storage.readItem(resolve(files.root, path));
|
|
1512
1505
|
if (content === null) return;
|
|
1513
|
-
|
|
1514
|
-
hashes[path] = hashOf(normalize ? `${content.trim()}\n` : content);
|
|
1506
|
+
hashes[path] = hashOf(content);
|
|
1515
1507
|
bytes += Buffer.byteLength(content);
|
|
1516
|
-
if (bytes <= maxSetBytes) await storage.writeItem(`${dirOf(jobId)}
|
|
1508
|
+
if (bytes <= maxSetBytes) await storage.writeItem(`${dirOf(jobId)}output/${path}`, content);
|
|
1517
1509
|
}
|
|
1518
1510
|
});
|
|
1519
1511
|
if (bytes > maxSetBytes) {
|
|
1520
|
-
await storage.empty(`${dirOf(jobId)}
|
|
1512
|
+
await storage.empty(`${dirOf(jobId)}output/`);
|
|
1521
1513
|
return {
|
|
1522
1514
|
paths: [],
|
|
1523
1515
|
hashes,
|
|
@@ -1533,10 +1525,50 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
|
|
|
1533
1525
|
async function drop(jobId) {
|
|
1534
1526
|
await storage.empty(dirOf(jobId));
|
|
1535
1527
|
}
|
|
1528
|
+
async function add(generation) {
|
|
1529
|
+
const current = await load();
|
|
1530
|
+
for (const expired of current.filter((entry) => !isLive(entry))) await drop(expired.jobId);
|
|
1531
|
+
const live = current.filter((entry) => entry.jobId !== generation.jobId && isLive(entry));
|
|
1532
|
+
const sharesBudget = (entry) => Boolean(entry.initial) === Boolean(generation.initial);
|
|
1533
|
+
const entries = live.filter(sharesBudget);
|
|
1534
|
+
entries.push({
|
|
1535
|
+
...generation,
|
|
1536
|
+
keptAt: now()
|
|
1537
|
+
});
|
|
1538
|
+
while (entries.length > 1 && (entries.length > maxCount || totalBytes(entries) > maxMb * MB)) await drop(entries.shift().jobId);
|
|
1539
|
+
index = [...live.filter((entry) => !sharesBudget(entry)), ...entries];
|
|
1540
|
+
await storage.writeItem(INDEX_KEY, JSON.stringify(index));
|
|
1541
|
+
}
|
|
1536
1542
|
return {
|
|
1537
1543
|
keep,
|
|
1538
1544
|
drop,
|
|
1539
1545
|
/**
|
|
1546
|
+
* Captures initial disk output, reusing a retained ID when its hashes and readable paths match.
|
|
1547
|
+
* A temporary candidate is discarded on reuse, so reconnects do not consume retention slots.
|
|
1548
|
+
*/
|
|
1549
|
+
async keepInitial(files) {
|
|
1550
|
+
const jobId = randomUUID();
|
|
1551
|
+
const output = await keep({
|
|
1552
|
+
jobId,
|
|
1553
|
+
files,
|
|
1554
|
+
maxSetMb: maxMb
|
|
1555
|
+
});
|
|
1556
|
+
const hashes = Object.entries(output.hashes);
|
|
1557
|
+
const previous = (await load()).findLast((entry) => entry.initial && isLive(entry) && entry.output.paths.length === output.paths.length && Object.keys(entry.output.hashes).length === hashes.length && hashes.every(([path, hash]) => entry.output.hashes[path] === hash));
|
|
1558
|
+
if (previous) {
|
|
1559
|
+
await drop(jobId);
|
|
1560
|
+
await add(previous);
|
|
1561
|
+
return previous;
|
|
1562
|
+
}
|
|
1563
|
+
const generation = {
|
|
1564
|
+
jobId,
|
|
1565
|
+
output,
|
|
1566
|
+
initial: true
|
|
1567
|
+
};
|
|
1568
|
+
await add(generation);
|
|
1569
|
+
return generation;
|
|
1570
|
+
},
|
|
1571
|
+
/**
|
|
1540
1572
|
* Keeps the `InputNode` of `jobId` and returns its size in bytes, or 0 when it keeps none. A ref schema loses
|
|
1541
1573
|
* the copy of its target, which `input.schemas` holds: with it, a large spec outgrows what `JSON.stringify` can return.
|
|
1542
1574
|
*/
|
|
@@ -1564,30 +1596,19 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
|
|
|
1564
1596
|
return stored === null ? void 0 : JSON.parse(stored);
|
|
1565
1597
|
},
|
|
1566
1598
|
get: async (jobId) => (await load()).find((generation) => generation.jobId === jobId && isLive(generation)),
|
|
1567
|
-
|
|
1568
|
-
const current = await load();
|
|
1569
|
-
for (const expired of current.filter((entry) => !isLive(entry))) await drop(expired.jobId);
|
|
1570
|
-
const entries = current.filter((entry) => entry.jobId !== generation.jobId && isLive(entry));
|
|
1571
|
-
entries.push({
|
|
1572
|
-
...generation,
|
|
1573
|
-
keptAt: now()
|
|
1574
|
-
});
|
|
1575
|
-
while (entries.length > 1 && (entries.length > maxCount || totalBytes(entries) > maxMb * MB)) await drop(entries.shift().jobId);
|
|
1576
|
-
index = entries;
|
|
1577
|
-
await storage.writeItem(INDEX_KEY, JSON.stringify(entries));
|
|
1578
|
-
},
|
|
1599
|
+
add,
|
|
1579
1600
|
/**
|
|
1580
1601
|
* Reads the requested paths the set holds, skipping any it does not, one page of lines when `limit` is given.
|
|
1581
1602
|
*/
|
|
1582
|
-
async read({ generation,
|
|
1583
|
-
const kept = new Set(generation
|
|
1603
|
+
async read({ generation, paths, cursor = 0, limit }) {
|
|
1604
|
+
const kept = new Set(generation.output.paths);
|
|
1584
1605
|
const files = {};
|
|
1585
1606
|
const pages = {};
|
|
1586
1607
|
await inParallel({
|
|
1587
1608
|
items: paths.filter((path) => kept.has(path)),
|
|
1588
1609
|
limit: READ_CONCURRENCY,
|
|
1589
1610
|
run: async (path) => {
|
|
1590
|
-
const content = await storage.readItem(`${dirOf(generation.jobId)}
|
|
1611
|
+
const content = await storage.readItem(`${dirOf(generation.jobId)}output/${path}`);
|
|
1591
1612
|
if (content === null) return;
|
|
1592
1613
|
if (limit === void 0) {
|
|
1593
1614
|
files[path] = content;
|
|
@@ -1862,6 +1883,25 @@ function isValidPage({ cursor, limit }) {
|
|
|
1862
1883
|
if (cursor !== void 0 && (!Number.isSafeInteger(cursor) || cursor < 0)) return false;
|
|
1863
1884
|
return Number.isSafeInteger(limit) && limit >= 1 && limit <= 2e3;
|
|
1864
1885
|
}
|
|
1886
|
+
/**
|
|
1887
|
+
* Maps generated paths into a temporary directory while keeping the project root for input resolution.
|
|
1888
|
+
*/
|
|
1889
|
+
function stagingStorage({ root, directory }) {
|
|
1890
|
+
const storage = fsStorage();
|
|
1891
|
+
const target = (key) => {
|
|
1892
|
+
const relative = path.relative(root, path.resolve(root, key));
|
|
1893
|
+
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) throw new Error("Output path escapes the project root");
|
|
1894
|
+
return path.join(directory, relative);
|
|
1895
|
+
};
|
|
1896
|
+
return {
|
|
1897
|
+
name: "fs",
|
|
1898
|
+
readItem: (key) => storage.readItem(target(key)),
|
|
1899
|
+
writeItem: (key, value) => storage.writeItem(target(key), value),
|
|
1900
|
+
removeItem: (key) => storage.removeItem(target(key)),
|
|
1901
|
+
readKeys: (base) => storage.readKeys(base ? target(base) : directory),
|
|
1902
|
+
empty: (base) => base ? storage.empty(target(base)) : Promise.resolve()
|
|
1903
|
+
};
|
|
1904
|
+
}
|
|
1865
1905
|
var GenerationRunTarget = class extends RpcTarget {
|
|
1866
1906
|
#run;
|
|
1867
1907
|
constructor(run) {
|
|
@@ -1886,6 +1926,7 @@ var Agent = class extends RpcTarget {
|
|
|
1886
1926
|
#session;
|
|
1887
1927
|
#limits = resolveGenerationLimits();
|
|
1888
1928
|
#store;
|
|
1929
|
+
#sandboxCacheRoot = path.join(tmpdir(), `kubb-studio-${randomUUID()}`);
|
|
1889
1930
|
#diffs = /* @__PURE__ */ new Map();
|
|
1890
1931
|
constructor({ options, session }) {
|
|
1891
1932
|
super();
|
|
@@ -1907,13 +1948,19 @@ var Agent = class extends RpcTarget {
|
|
|
1907
1948
|
}
|
|
1908
1949
|
get #generations() {
|
|
1909
1950
|
this.#store ??= createGenerationStore({
|
|
1910
|
-
storage: this.#sandbox ?
|
|
1951
|
+
storage: cacheStorage({ root: this.#sandbox ? this.#sandboxCacheRoot : this.#options.root }),
|
|
1911
1952
|
maxCount: this.#limits.maxCount,
|
|
1912
1953
|
maxMb: this.#limits.maxMb,
|
|
1913
1954
|
ttlMs: this.#sandbox ? SANDBOX_GENERATION_TTL_MS : void 0
|
|
1914
1955
|
});
|
|
1915
1956
|
return this.#store;
|
|
1916
1957
|
}
|
|
1958
|
+
async [Symbol.asyncDispose]() {
|
|
1959
|
+
if (this.#sandbox) await rm(resolveCacheDir(this.#sandboxCacheRoot), {
|
|
1960
|
+
recursive: true,
|
|
1961
|
+
force: true
|
|
1962
|
+
});
|
|
1963
|
+
}
|
|
1917
1964
|
#warn({ message, permission }) {
|
|
1918
1965
|
return emit(this.#options.hooks, "studio:warn", {
|
|
1919
1966
|
message,
|
|
@@ -2025,7 +2072,12 @@ var Agent = class extends RpcTarget {
|
|
|
2025
2072
|
maxFiles: DISK_SNAPSHOT_MAX_FILES
|
|
2026
2073
|
});
|
|
2027
2074
|
if (!files) return void 0;
|
|
2028
|
-
|
|
2075
|
+
const { jobId, output } = await this.#generations.keepInitial(files);
|
|
2076
|
+
return {
|
|
2077
|
+
jobId,
|
|
2078
|
+
files: output.paths,
|
|
2079
|
+
hashes: output.hashes
|
|
2080
|
+
};
|
|
2029
2081
|
}
|
|
2030
2082
|
startGeneration(input) {
|
|
2031
2083
|
const stream = createGenerationStream(this.#options.hooks, input.jobId);
|
|
@@ -2044,7 +2096,9 @@ var Agent = class extends RpcTarget {
|
|
|
2044
2096
|
}
|
|
2045
2097
|
});
|
|
2046
2098
|
}
|
|
2047
|
-
/**
|
|
2099
|
+
/**
|
|
2100
|
+
* Keeps a job's output and permitted parsed input, then cleans up its temporary generation directories.
|
|
2101
|
+
*/
|
|
2048
2102
|
async #runGeneration(input, controller) {
|
|
2049
2103
|
this.#assertConnected();
|
|
2050
2104
|
if (this.#session.closed) throw new Diagnostics.Error({
|
|
@@ -2060,11 +2114,14 @@ var Agent = class extends RpcTarget {
|
|
|
2060
2114
|
settled: settled.promise
|
|
2061
2115
|
};
|
|
2062
2116
|
let root = this.#options.root;
|
|
2117
|
+
let outputRoot;
|
|
2063
2118
|
try {
|
|
2064
2119
|
if (this.#sandbox) root = await mkdtemp(path.join(tmpdir(), "kubb-job-"));
|
|
2120
|
+
if (!this.#sandbox && !this.#permissions.allowWrite) outputRoot = await mkdtemp(path.join(tmpdir(), "kubb-output-"));
|
|
2065
2121
|
return await this.#generate({
|
|
2066
2122
|
input,
|
|
2067
2123
|
root,
|
|
2124
|
+
outputRoot,
|
|
2068
2125
|
signal: controller.signal
|
|
2069
2126
|
});
|
|
2070
2127
|
} finally {
|
|
@@ -2075,12 +2132,18 @@ var Agent = class extends RpcTarget {
|
|
|
2075
2132
|
recursive: true,
|
|
2076
2133
|
force: true
|
|
2077
2134
|
})]).catch(() => {});
|
|
2135
|
+
if (outputRoot) await rm(outputRoot, {
|
|
2136
|
+
recursive: true,
|
|
2137
|
+
force: true
|
|
2138
|
+
});
|
|
2078
2139
|
this.#session.active = void 0;
|
|
2079
2140
|
settled.resolve();
|
|
2080
2141
|
}
|
|
2081
2142
|
}
|
|
2082
|
-
/**
|
|
2083
|
-
|
|
2143
|
+
/**
|
|
2144
|
+
* Merges Studio's config over the project config and confines generation to the configured output folder.
|
|
2145
|
+
*/
|
|
2146
|
+
async #generate({ input, root, outputRoot, signal }) {
|
|
2084
2147
|
const { jobId, config: patch } = input;
|
|
2085
2148
|
const studioInput = this.#sandbox ? patch?.input : void 0;
|
|
2086
2149
|
if (this.#sandbox && (typeof studioInput !== "string" || !studioInput.trim() || getInputKind(studioInput) !== "inline")) return this.#refuse({ message: AgentMessage.SANDBOX_INPUT_ONLY });
|
|
@@ -2111,37 +2174,28 @@ var Agent = class extends RpcTarget {
|
|
|
2111
2174
|
});
|
|
2112
2175
|
const activePlugins = plugins ?? config.plugins;
|
|
2113
2176
|
if (patch?.input && !this.#sandbox) await emit(this.#options.hooks, "kubb:warn", { message: "Ignored the spec from Studio: generating from a Studio spec is only available to a sandbox agent" });
|
|
2114
|
-
const diskFiles = !this.#sandbox && this.#permissions.allowRead ? await listDisk({
|
|
2115
|
-
root,
|
|
2116
|
-
outputPath: config.output.path,
|
|
2117
|
-
maxFiles: DISK_SNAPSHOT_MAX_FILES
|
|
2118
|
-
}) : void 0;
|
|
2119
|
-
const disk = diskFiles ? await this.#generations.keep({
|
|
2120
|
-
jobId,
|
|
2121
|
-
source: FileSource.DISK,
|
|
2122
|
-
files: diskFiles,
|
|
2123
|
-
maxSetMb: this.#limits.maxSnapshotMb
|
|
2124
|
-
}) : void 0;
|
|
2125
2177
|
const files = await generate({
|
|
2126
2178
|
config: {
|
|
2127
2179
|
...config,
|
|
2128
2180
|
root,
|
|
2129
2181
|
input: studioInput ?? config.input,
|
|
2130
|
-
storage:
|
|
2182
|
+
storage: outputRoot ? stagingStorage({
|
|
2183
|
+
root,
|
|
2184
|
+
directory: outputRoot
|
|
2185
|
+
}) : fsStorage(),
|
|
2131
2186
|
plugins: activePlugins,
|
|
2132
2187
|
adapter,
|
|
2133
2188
|
parsers
|
|
2134
2189
|
},
|
|
2135
2190
|
hooks: this.#options.hooks,
|
|
2136
2191
|
signal,
|
|
2137
|
-
allowExec: this.#permissions.allowExec
|
|
2192
|
+
allowExec: this.#permissions.allowWrite && this.#permissions.allowExec
|
|
2138
2193
|
}).catch(async (error) => {
|
|
2139
2194
|
await this.#generations.drop(jobId);
|
|
2140
2195
|
throw error;
|
|
2141
2196
|
});
|
|
2142
2197
|
const output = await this.#generations.keep({
|
|
2143
2198
|
jobId,
|
|
2144
|
-
source: FileSource.OUTPUT,
|
|
2145
2199
|
files,
|
|
2146
2200
|
maxSetMb: this.#limits.maxMb
|
|
2147
2201
|
});
|
|
@@ -2154,16 +2208,14 @@ var Agent = class extends RpcTarget {
|
|
|
2154
2208
|
await this.#generations.add({
|
|
2155
2209
|
jobId,
|
|
2156
2210
|
output,
|
|
2157
|
-
disk,
|
|
2158
2211
|
inputBytes
|
|
2159
2212
|
});
|
|
2160
|
-
await this.#logCommand("generate", `${activePlugins.length} plugin${activePlugins.length === 1 ? "" : "s"}, ${this.#permissions.allowWrite ? "written to disk" : "
|
|
2213
|
+
await this.#logCommand("generate", `${activePlugins.length} plugin${activePlugins.length === 1 ? "" : "s"}, ${this.#permissions.allowWrite ? "written to disk" : "staged on disk"}${studioInput !== void 0 ? ", from a Studio spec" : ""}`);
|
|
2161
2214
|
return {
|
|
2162
2215
|
status: "success",
|
|
2163
2216
|
files: [...files.paths],
|
|
2164
2217
|
fileCount: files.paths.size,
|
|
2165
|
-
hashes: output.hashes
|
|
2166
|
-
disk: disk ? { hashes: disk.hashes } : void 0
|
|
2218
|
+
hashes: output.hashes
|
|
2167
2219
|
};
|
|
2168
2220
|
}
|
|
2169
2221
|
async saveConfig(input) {
|
|
@@ -2233,35 +2285,14 @@ var Agent = class extends RpcTarget {
|
|
|
2233
2285
|
message: AgentMessage.READ_NOT_ALLOWED,
|
|
2234
2286
|
permission: AgentPermission.READ
|
|
2235
2287
|
});
|
|
2236
|
-
if (!Array.isArray(input.paths) || typeof input.jobId !== "string" || !input.jobId
|
|
2288
|
+
if (!Array.isArray(input.paths) || typeof input.jobId !== "string" || !input.jobId) return this.#refuse({ message: AgentMessage.FILE_REQUEST_INVALID });
|
|
2237
2289
|
const { paths } = input;
|
|
2238
2290
|
if (paths.length > 50) return this.#refuse({ message: AgentMessage.TOO_MANY_FILES });
|
|
2239
2291
|
if (!isValidPage(input)) return this.#refuse({ message: AgentMessage.INVALID_PAGE });
|
|
2240
|
-
if (input.jobType === "currentDisk") {
|
|
2241
|
-
if (this.#sandbox) return this.#refuse({ message: AgentMessage.SANDBOX_DISK_UNAVAILABLE });
|
|
2242
|
-
const config = await this.#options.loadConfig();
|
|
2243
|
-
const disk = await listDisk({
|
|
2244
|
-
root: this.#options.root,
|
|
2245
|
-
outputPath: config.output.path,
|
|
2246
|
-
maxFiles: DISK_SNAPSHOT_MAX_FILES
|
|
2247
|
-
});
|
|
2248
|
-
if (!disk) return this.#refuse({ message: AgentMessage.GENERATION_GONE });
|
|
2249
|
-
const result = await readDiskFiles({
|
|
2250
|
-
files: disk,
|
|
2251
|
-
paths,
|
|
2252
|
-
cursor: input.cursor,
|
|
2253
|
-
limit: input.limit
|
|
2254
|
-
});
|
|
2255
|
-
await this.#logCommand("readFiles", `read ${Object.keys(result.files).length}/${paths.length} requested file${paths.length === 1 ? "" : "s"}`);
|
|
2256
|
-
return result;
|
|
2257
|
-
}
|
|
2258
2292
|
const generation = await this.#generations.get(input.jobId);
|
|
2259
2293
|
if (!generation) return this.#refuse({ message: AgentMessage.GENERATION_GONE });
|
|
2260
|
-
const source = input.source === FileSource.DISK ? FileSource.DISK : FileSource.OUTPUT;
|
|
2261
|
-
if (!generation[source]) return this.#refuse({ message: AgentMessage.NO_DISK_SNAPSHOT });
|
|
2262
2294
|
const result = await this.#generations.read({
|
|
2263
2295
|
generation,
|
|
2264
|
-
source,
|
|
2265
2296
|
paths,
|
|
2266
2297
|
cursor: input.cursor,
|
|
2267
2298
|
limit: input.limit
|
|
@@ -2286,15 +2317,11 @@ var Agent = class extends RpcTarget {
|
|
|
2286
2317
|
})) return this.#refuse({ message: AgentMessage.INVALID_PAGE });
|
|
2287
2318
|
const [generation, baseGeneration] = await Promise.all([this.#generations.get(jobId), this.#generations.get(base.jobId)]);
|
|
2288
2319
|
if (!generation || !baseGeneration) return this.#refuse({ message: AgentMessage.GENERATION_GONE });
|
|
2289
|
-
const baseSource = base.source === FileSource.DISK ? FileSource.DISK : FileSource.OUTPUT;
|
|
2290
|
-
if (!baseGeneration[baseSource]) return this.#refuse({ message: AgentMessage.NO_DISK_SNAPSHOT });
|
|
2291
2320
|
const [after, before] = await Promise.all([this.#generations.read({
|
|
2292
2321
|
generation,
|
|
2293
|
-
source: FileSource.OUTPUT,
|
|
2294
2322
|
paths: [path]
|
|
2295
2323
|
}), this.#generations.read({
|
|
2296
2324
|
generation: baseGeneration,
|
|
2297
|
-
source: baseSource,
|
|
2298
2325
|
paths: [path]
|
|
2299
2326
|
})]);
|
|
2300
2327
|
const afterContent = after.files[path];
|
|
@@ -2315,7 +2342,6 @@ var Agent = class extends RpcTarget {
|
|
|
2315
2342
|
const key = JSON.stringify([
|
|
2316
2343
|
jobId,
|
|
2317
2344
|
base.jobId,
|
|
2318
|
-
baseSource,
|
|
2319
2345
|
path
|
|
2320
2346
|
]);
|
|
2321
2347
|
let diff = this.#diffs.get(key);
|
|
@@ -2372,14 +2398,15 @@ async function runSession({ options, openRpc, url, instanceId }) {
|
|
|
2372
2398
|
connected: Promise.withResolvers(),
|
|
2373
2399
|
closed: false
|
|
2374
2400
|
};
|
|
2401
|
+
const agent = new Agent({
|
|
2402
|
+
options,
|
|
2403
|
+
session
|
|
2404
|
+
});
|
|
2375
2405
|
const rpc = openRpc({
|
|
2376
2406
|
url,
|
|
2377
2407
|
token,
|
|
2378
2408
|
instanceId,
|
|
2379
|
-
agent
|
|
2380
|
-
options,
|
|
2381
|
-
session
|
|
2382
|
-
})
|
|
2409
|
+
agent
|
|
2383
2410
|
});
|
|
2384
2411
|
signal?.addEventListener("abort", rpc.close, { once: true });
|
|
2385
2412
|
let timer;
|
|
@@ -2414,6 +2441,7 @@ async function runSession({ options, openRpc, url, instanceId }) {
|
|
|
2414
2441
|
await emit(hooks, "studio:warn", { message: "Canceled the running generation: the connection to Kubb Studio closed" });
|
|
2415
2442
|
}
|
|
2416
2443
|
await active?.settled;
|
|
2444
|
+
await agent[Symbol.asyncDispose]();
|
|
2417
2445
|
return {
|
|
2418
2446
|
...close,
|
|
2419
2447
|
connected: session.info !== void 0
|
|
@@ -2721,10 +2749,9 @@ async function createSnapshotPackage({ files, packageInfo }) {
|
|
|
2721
2749
|
const root = await mkdtemp(join(tmpdir(), "kubb-snapshot-"));
|
|
2722
2750
|
const dist = join(root, "dist");
|
|
2723
2751
|
try {
|
|
2724
|
-
await mkdir(dist);
|
|
2752
|
+
await mkdir$1(dist);
|
|
2725
2753
|
await Promise.all([...sources].map(async ([path, { content }]) => {
|
|
2726
|
-
await
|
|
2727
|
-
await writeFile(join(root, path), content);
|
|
2754
|
+
await writeRaw(join(root, path), content);
|
|
2728
2755
|
}));
|
|
2729
2756
|
const entry = [...sources.keys()].filter((path) => /\.(?:[cm]?[jt]sx?)$/.test(path)).map((path) => join(root, path));
|
|
2730
2757
|
if (entry.length) {
|
|
@@ -2764,7 +2791,7 @@ async function createSnapshotPackage({ files, packageInfo }) {
|
|
|
2764
2791
|
const chunks = [...new Map([
|
|
2765
2792
|
["package.json", JSON.stringify(manifest, null, 2)],
|
|
2766
2793
|
...[...sources].map(([path, { content }]) => [path, content]),
|
|
2767
|
-
...await Promise.all(built.map(async (key) => [`dist/${key}`, await
|
|
2794
|
+
...await Promise.all(built.map(async (key) => [`dist/${key}`, await read(join(dist, key))]))
|
|
2768
2795
|
])].flatMap(([path, content]) => {
|
|
2769
2796
|
const bytes = Buffer.from(content);
|
|
2770
2797
|
return [
|
|
@@ -2795,7 +2822,10 @@ async function createSnapshot({ studioUrl = defaultStudioUrl, apiKey, config, ho
|
|
|
2795
2822
|
path: "/api/snapshots"
|
|
2796
2823
|
});
|
|
2797
2824
|
const generated = await generate({
|
|
2798
|
-
config
|
|
2825
|
+
config: {
|
|
2826
|
+
...config,
|
|
2827
|
+
storage: fsStorage()
|
|
2828
|
+
},
|
|
2799
2829
|
hooks,
|
|
2800
2830
|
signal,
|
|
2801
2831
|
allowExec: true
|