@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.cjs
CHANGED
|
@@ -23,7 +23,6 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
23
23
|
//#endregion
|
|
24
24
|
const require_protocol = require("./protocol.cjs");
|
|
25
25
|
let node_crypto = require("node:crypto");
|
|
26
|
-
let node_fs_promises = require("node:fs/promises");
|
|
27
26
|
let node_os = require("node:os");
|
|
28
27
|
let node_path = require("node:path");
|
|
29
28
|
node_path = __toESM(node_path, 1);
|
|
@@ -32,6 +31,7 @@ node_process = __toESM(node_process, 1);
|
|
|
32
31
|
let node_timers_promises = require("node:timers/promises");
|
|
33
32
|
let node_url = require("node:url");
|
|
34
33
|
let node_fs = require("node:fs");
|
|
34
|
+
let node_fs_promises = require("node:fs/promises");
|
|
35
35
|
let _kubb_core = require("@kubb/core");
|
|
36
36
|
let capnweb = require("capnweb");
|
|
37
37
|
let magicast = require("magicast");
|
|
@@ -141,6 +141,27 @@ async function read(path) {
|
|
|
141
141
|
return (0, node_fs_promises.readFile)(path, { encoding: "utf8" });
|
|
142
142
|
}
|
|
143
143
|
/**
|
|
144
|
+
* Writes UTF-8 content verbatim, including empty files and surrounding whitespace.
|
|
145
|
+
* Uses Bun.write under Bun and creates missing parent directories under either runtime.
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* await writeRaw('./cache/output.ts', ' // keep these bytes\n\n')
|
|
149
|
+
*/
|
|
150
|
+
async function writeRaw(path, content) {
|
|
151
|
+
const resolved = (0, node_path.resolve)(path);
|
|
152
|
+
if (runtime.isBun) {
|
|
153
|
+
await Bun.write(resolved, content);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
await (0, node_fs_promises.writeFile)(resolved, content, { encoding: "utf-8" });
|
|
158
|
+
} catch (error) {
|
|
159
|
+
if (error.code !== "ENOENT") throw error;
|
|
160
|
+
await (0, node_fs_promises.mkdir)((0, node_path.dirname)(resolved), { recursive: true });
|
|
161
|
+
await (0, node_fs_promises.writeFile)(resolved, content, { encoding: "utf-8" });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
144
165
|
* Resolves to `true` when `path` is `parent` itself or nested inside it. Both sides are resolved
|
|
145
166
|
* to absolute paths first, so relative and `..`-containing inputs compare correctly.
|
|
146
167
|
*
|
|
@@ -199,7 +220,7 @@ function getElapsedMs(hrStart) {
|
|
|
199
220
|
}
|
|
200
221
|
//#endregion
|
|
201
222
|
//#region package.json
|
|
202
|
-
var version = "5.11.
|
|
223
|
+
var version = "5.11.4";
|
|
203
224
|
//#endregion
|
|
204
225
|
//#region src/resolveConfig.ts
|
|
205
226
|
/**
|
|
@@ -1395,21 +1416,20 @@ const hashOf = (content) => (0, node_crypto.createHash)("sha1").update(content).
|
|
|
1395
1416
|
/**
|
|
1396
1417
|
* The bytes `entries` keep on disk: output, written files, and kept input.
|
|
1397
1418
|
*/
|
|
1398
|
-
const totalBytes = (entries) => entries.reduce((sum, { output,
|
|
1419
|
+
const totalBytes = (entries) => entries.reduce((sum, { output, inputBytes }) => sum + output.bytes + (inputBytes ?? 0), 0);
|
|
1399
1420
|
function positiveNumber(value) {
|
|
1400
1421
|
const parsed = Number(value);
|
|
1401
1422
|
return value && Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
|
|
1402
1423
|
}
|
|
1403
1424
|
/**
|
|
1404
1425
|
* How many generations an agent keeps and how large they may get, read from
|
|
1405
|
-
* `KUBB_AGENT_MAX_GENERATIONS
|
|
1426
|
+
* `KUBB_AGENT_MAX_GENERATIONS` and `KUBB_AGENT_MAX_GENERATIONS_MB`.
|
|
1406
1427
|
* An unset or invalid value keeps the default.
|
|
1407
1428
|
*/
|
|
1408
1429
|
function resolveGenerationLimits() {
|
|
1409
1430
|
return {
|
|
1410
1431
|
maxCount: Math.max(1, Math.floor(positiveNumber(node_process.default.env.KUBB_AGENT_MAX_GENERATIONS) ?? 8)),
|
|
1411
|
-
maxMb: positiveNumber(node_process.default.env.KUBB_AGENT_MAX_GENERATIONS_MB) ?? 100
|
|
1412
|
-
maxSnapshotMb: positiveNumber(node_process.default.env.KUBB_AGENT_MAX_SNAPSHOT_MB) ?? 50
|
|
1432
|
+
maxMb: positiveNumber(node_process.default.env.KUBB_AGENT_MAX_GENERATIONS_MB) ?? 100
|
|
1413
1433
|
};
|
|
1414
1434
|
}
|
|
1415
1435
|
/**
|
|
@@ -1456,7 +1476,7 @@ async function generate({ config, hooks, signal, allowExec }) {
|
|
|
1456
1476
|
};
|
|
1457
1477
|
}
|
|
1458
1478
|
/**
|
|
1459
|
-
* What the output directory holds
|
|
1479
|
+
* What the output directory holds when the editor connects. `undefined` when `outputPath` is not a real
|
|
1460
1480
|
* subdirectory of `root` (listing the root would take in every source file) or holds more than `maxFiles`.
|
|
1461
1481
|
*/
|
|
1462
1482
|
async function listDisk({ root, outputPath, maxFiles }) {
|
|
@@ -1473,38 +1493,11 @@ async function listDisk({ root, outputPath, maxFiles }) {
|
|
|
1473
1493
|
paths: new Set(paths)
|
|
1474
1494
|
};
|
|
1475
1495
|
}
|
|
1476
|
-
async function readDiskFiles({ files: disk, paths, cursor = 0, limit }) {
|
|
1477
|
-
const files = {};
|
|
1478
|
-
const pages = {};
|
|
1479
|
-
await inParallel({
|
|
1480
|
-
items: paths.filter((path) => disk.paths.has(path)),
|
|
1481
|
-
limit: READ_CONCURRENCY,
|
|
1482
|
-
run: async (path) => {
|
|
1483
|
-
const content = await disk.storage.readItem((0, node_path.resolve)(disk.root, path));
|
|
1484
|
-
if (content === null) return;
|
|
1485
|
-
if (limit === void 0) {
|
|
1486
|
-
files[path] = content;
|
|
1487
|
-
return;
|
|
1488
|
-
}
|
|
1489
|
-
const lines = content.split(/\r\n|\r|\n/);
|
|
1490
|
-
const end = Math.min(cursor + limit, lines.length);
|
|
1491
|
-
files[path] = lines.slice(cursor, end).join("\n");
|
|
1492
|
-
pages[path] = {
|
|
1493
|
-
nextCursor: end < lines.length ? end : null,
|
|
1494
|
-
totalLines: lines.length
|
|
1495
|
-
};
|
|
1496
|
-
}
|
|
1497
|
-
});
|
|
1498
|
-
return limit === void 0 ? { files } : {
|
|
1499
|
-
files,
|
|
1500
|
-
pages
|
|
1501
|
-
};
|
|
1502
|
-
}
|
|
1503
1496
|
/**
|
|
1504
1497
|
* Keeps recent generations by job id in `storage`, with an index next to them so a store on disk
|
|
1505
1498
|
* survives a restart. A generation is only ever looked up by its job id, so on a pooled sandbox one
|
|
1506
|
-
* tenant never reaches another's output.
|
|
1507
|
-
* always
|
|
1499
|
+
* tenant never reaches another's output. Initial disk output and generated jobs each get their
|
|
1500
|
+
* own `maxCount` and `maxMb` budget. Each budget always keeps its newest entry.
|
|
1508
1501
|
*/
|
|
1509
1502
|
function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now }) {
|
|
1510
1503
|
let index;
|
|
@@ -1523,7 +1516,7 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
|
|
|
1523
1516
|
/**
|
|
1524
1517
|
* Copies `files` into the store as one set of `jobId`. Above `maxSetMb` only the hashes are kept.
|
|
1525
1518
|
*/
|
|
1526
|
-
async function keep({ jobId,
|
|
1519
|
+
async function keep({ jobId, files, maxSetMb }) {
|
|
1527
1520
|
const maxSetBytes = maxSetMb * MB;
|
|
1528
1521
|
const hashes = {};
|
|
1529
1522
|
let bytes = 0;
|
|
@@ -1534,14 +1527,13 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
|
|
|
1534
1527
|
if (path.split("/").includes("..")) return;
|
|
1535
1528
|
const content = await files.storage.readItem((0, node_path.resolve)(files.root, path));
|
|
1536
1529
|
if (content === null) return;
|
|
1537
|
-
|
|
1538
|
-
hashes[path] = hashOf(normalize ? `${content.trim()}\n` : content);
|
|
1530
|
+
hashes[path] = hashOf(content);
|
|
1539
1531
|
bytes += Buffer.byteLength(content);
|
|
1540
|
-
if (bytes <= maxSetBytes) await storage.writeItem(`${dirOf(jobId)}
|
|
1532
|
+
if (bytes <= maxSetBytes) await storage.writeItem(`${dirOf(jobId)}output/${path}`, content);
|
|
1541
1533
|
}
|
|
1542
1534
|
});
|
|
1543
1535
|
if (bytes > maxSetBytes) {
|
|
1544
|
-
await storage.empty(`${dirOf(jobId)}
|
|
1536
|
+
await storage.empty(`${dirOf(jobId)}output/`);
|
|
1545
1537
|
return {
|
|
1546
1538
|
paths: [],
|
|
1547
1539
|
hashes,
|
|
@@ -1557,10 +1549,50 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
|
|
|
1557
1549
|
async function drop(jobId) {
|
|
1558
1550
|
await storage.empty(dirOf(jobId));
|
|
1559
1551
|
}
|
|
1552
|
+
async function add(generation) {
|
|
1553
|
+
const current = await load();
|
|
1554
|
+
for (const expired of current.filter((entry) => !isLive(entry))) await drop(expired.jobId);
|
|
1555
|
+
const live = current.filter((entry) => entry.jobId !== generation.jobId && isLive(entry));
|
|
1556
|
+
const sharesBudget = (entry) => Boolean(entry.initial) === Boolean(generation.initial);
|
|
1557
|
+
const entries = live.filter(sharesBudget);
|
|
1558
|
+
entries.push({
|
|
1559
|
+
...generation,
|
|
1560
|
+
keptAt: now()
|
|
1561
|
+
});
|
|
1562
|
+
while (entries.length > 1 && (entries.length > maxCount || totalBytes(entries) > maxMb * MB)) await drop(entries.shift().jobId);
|
|
1563
|
+
index = [...live.filter((entry) => !sharesBudget(entry)), ...entries];
|
|
1564
|
+
await storage.writeItem(INDEX_KEY, JSON.stringify(index));
|
|
1565
|
+
}
|
|
1560
1566
|
return {
|
|
1561
1567
|
keep,
|
|
1562
1568
|
drop,
|
|
1563
1569
|
/**
|
|
1570
|
+
* Captures initial disk output, reusing a retained ID when its hashes and readable paths match.
|
|
1571
|
+
* A temporary candidate is discarded on reuse, so reconnects do not consume retention slots.
|
|
1572
|
+
*/
|
|
1573
|
+
async keepInitial(files) {
|
|
1574
|
+
const jobId = (0, node_crypto.randomUUID)();
|
|
1575
|
+
const output = await keep({
|
|
1576
|
+
jobId,
|
|
1577
|
+
files,
|
|
1578
|
+
maxSetMb: maxMb
|
|
1579
|
+
});
|
|
1580
|
+
const hashes = Object.entries(output.hashes);
|
|
1581
|
+
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));
|
|
1582
|
+
if (previous) {
|
|
1583
|
+
await drop(jobId);
|
|
1584
|
+
await add(previous);
|
|
1585
|
+
return previous;
|
|
1586
|
+
}
|
|
1587
|
+
const generation = {
|
|
1588
|
+
jobId,
|
|
1589
|
+
output,
|
|
1590
|
+
initial: true
|
|
1591
|
+
};
|
|
1592
|
+
await add(generation);
|
|
1593
|
+
return generation;
|
|
1594
|
+
},
|
|
1595
|
+
/**
|
|
1564
1596
|
* Keeps the `InputNode` of `jobId` and returns its size in bytes, or 0 when it keeps none. A ref schema loses
|
|
1565
1597
|
* the copy of its target, which `input.schemas` holds: with it, a large spec outgrows what `JSON.stringify` can return.
|
|
1566
1598
|
*/
|
|
@@ -1588,30 +1620,19 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
|
|
|
1588
1620
|
return stored === null ? void 0 : JSON.parse(stored);
|
|
1589
1621
|
},
|
|
1590
1622
|
get: async (jobId) => (await load()).find((generation) => generation.jobId === jobId && isLive(generation)),
|
|
1591
|
-
|
|
1592
|
-
const current = await load();
|
|
1593
|
-
for (const expired of current.filter((entry) => !isLive(entry))) await drop(expired.jobId);
|
|
1594
|
-
const entries = current.filter((entry) => entry.jobId !== generation.jobId && isLive(entry));
|
|
1595
|
-
entries.push({
|
|
1596
|
-
...generation,
|
|
1597
|
-
keptAt: now()
|
|
1598
|
-
});
|
|
1599
|
-
while (entries.length > 1 && (entries.length > maxCount || totalBytes(entries) > maxMb * MB)) await drop(entries.shift().jobId);
|
|
1600
|
-
index = entries;
|
|
1601
|
-
await storage.writeItem(INDEX_KEY, JSON.stringify(entries));
|
|
1602
|
-
},
|
|
1623
|
+
add,
|
|
1603
1624
|
/**
|
|
1604
1625
|
* Reads the requested paths the set holds, skipping any it does not, one page of lines when `limit` is given.
|
|
1605
1626
|
*/
|
|
1606
|
-
async read({ generation,
|
|
1607
|
-
const kept = new Set(generation
|
|
1627
|
+
async read({ generation, paths, cursor = 0, limit }) {
|
|
1628
|
+
const kept = new Set(generation.output.paths);
|
|
1608
1629
|
const files = {};
|
|
1609
1630
|
const pages = {};
|
|
1610
1631
|
await inParallel({
|
|
1611
1632
|
items: paths.filter((path) => kept.has(path)),
|
|
1612
1633
|
limit: READ_CONCURRENCY,
|
|
1613
1634
|
run: async (path) => {
|
|
1614
|
-
const content = await storage.readItem(`${dirOf(generation.jobId)}
|
|
1635
|
+
const content = await storage.readItem(`${dirOf(generation.jobId)}output/${path}`);
|
|
1615
1636
|
if (content === null) return;
|
|
1616
1637
|
if (limit === void 0) {
|
|
1617
1638
|
files[path] = content;
|
|
@@ -1886,6 +1907,25 @@ function isValidPage({ cursor, limit }) {
|
|
|
1886
1907
|
if (cursor !== void 0 && (!Number.isSafeInteger(cursor) || cursor < 0)) return false;
|
|
1887
1908
|
return Number.isSafeInteger(limit) && limit >= 1 && limit <= 2e3;
|
|
1888
1909
|
}
|
|
1910
|
+
/**
|
|
1911
|
+
* Maps generated paths into a temporary directory while keeping the project root for input resolution.
|
|
1912
|
+
*/
|
|
1913
|
+
function stagingStorage({ root, directory }) {
|
|
1914
|
+
const storage = (0, _kubb_core.fsStorage)();
|
|
1915
|
+
const target = (key) => {
|
|
1916
|
+
const relative = node_path.default.relative(root, node_path.default.resolve(root, key));
|
|
1917
|
+
if (relative === ".." || relative.startsWith(`..${node_path.default.sep}`) || node_path.default.isAbsolute(relative)) throw new Error("Output path escapes the project root");
|
|
1918
|
+
return node_path.default.join(directory, relative);
|
|
1919
|
+
};
|
|
1920
|
+
return {
|
|
1921
|
+
name: "fs",
|
|
1922
|
+
readItem: (key) => storage.readItem(target(key)),
|
|
1923
|
+
writeItem: (key, value) => storage.writeItem(target(key), value),
|
|
1924
|
+
removeItem: (key) => storage.removeItem(target(key)),
|
|
1925
|
+
readKeys: (base) => storage.readKeys(base ? target(base) : directory),
|
|
1926
|
+
empty: (base) => base ? storage.empty(target(base)) : Promise.resolve()
|
|
1927
|
+
};
|
|
1928
|
+
}
|
|
1889
1929
|
var GenerationRunTarget = class extends capnweb.RpcTarget {
|
|
1890
1930
|
#run;
|
|
1891
1931
|
constructor(run) {
|
|
@@ -1910,6 +1950,7 @@ var Agent = class extends capnweb.RpcTarget {
|
|
|
1910
1950
|
#session;
|
|
1911
1951
|
#limits = resolveGenerationLimits();
|
|
1912
1952
|
#store;
|
|
1953
|
+
#sandboxCacheRoot = node_path.default.join((0, node_os.tmpdir)(), `kubb-studio-${(0, node_crypto.randomUUID)()}`);
|
|
1913
1954
|
#diffs = /* @__PURE__ */ new Map();
|
|
1914
1955
|
constructor({ options, session }) {
|
|
1915
1956
|
super();
|
|
@@ -1931,13 +1972,19 @@ var Agent = class extends capnweb.RpcTarget {
|
|
|
1931
1972
|
}
|
|
1932
1973
|
get #generations() {
|
|
1933
1974
|
this.#store ??= createGenerationStore({
|
|
1934
|
-
storage:
|
|
1975
|
+
storage: (0, _kubb_core.cacheStorage)({ root: this.#sandbox ? this.#sandboxCacheRoot : this.#options.root }),
|
|
1935
1976
|
maxCount: this.#limits.maxCount,
|
|
1936
1977
|
maxMb: this.#limits.maxMb,
|
|
1937
1978
|
ttlMs: this.#sandbox ? SANDBOX_GENERATION_TTL_MS : void 0
|
|
1938
1979
|
});
|
|
1939
1980
|
return this.#store;
|
|
1940
1981
|
}
|
|
1982
|
+
async [Symbol.asyncDispose]() {
|
|
1983
|
+
if (this.#sandbox) await (0, node_fs_promises.rm)((0, _kubb_core.resolveCacheDir)(this.#sandboxCacheRoot), {
|
|
1984
|
+
recursive: true,
|
|
1985
|
+
force: true
|
|
1986
|
+
});
|
|
1987
|
+
}
|
|
1941
1988
|
#warn({ message, permission }) {
|
|
1942
1989
|
return emit(this.#options.hooks, "studio:warn", {
|
|
1943
1990
|
message,
|
|
@@ -2049,7 +2096,12 @@ var Agent = class extends capnweb.RpcTarget {
|
|
|
2049
2096
|
maxFiles: DISK_SNAPSHOT_MAX_FILES
|
|
2050
2097
|
});
|
|
2051
2098
|
if (!files) return void 0;
|
|
2052
|
-
|
|
2099
|
+
const { jobId, output } = await this.#generations.keepInitial(files);
|
|
2100
|
+
return {
|
|
2101
|
+
jobId,
|
|
2102
|
+
files: output.paths,
|
|
2103
|
+
hashes: output.hashes
|
|
2104
|
+
};
|
|
2053
2105
|
}
|
|
2054
2106
|
startGeneration(input) {
|
|
2055
2107
|
const stream = createGenerationStream(this.#options.hooks, input.jobId);
|
|
@@ -2068,7 +2120,9 @@ var Agent = class extends capnweb.RpcTarget {
|
|
|
2068
2120
|
}
|
|
2069
2121
|
});
|
|
2070
2122
|
}
|
|
2071
|
-
/**
|
|
2123
|
+
/**
|
|
2124
|
+
* Keeps a job's output and permitted parsed input, then cleans up its temporary generation directories.
|
|
2125
|
+
*/
|
|
2072
2126
|
async #runGeneration(input, controller) {
|
|
2073
2127
|
this.#assertConnected();
|
|
2074
2128
|
if (this.#session.closed) throw new _kubb_core.Diagnostics.Error({
|
|
@@ -2084,11 +2138,14 @@ var Agent = class extends capnweb.RpcTarget {
|
|
|
2084
2138
|
settled: settled.promise
|
|
2085
2139
|
};
|
|
2086
2140
|
let root = this.#options.root;
|
|
2141
|
+
let outputRoot;
|
|
2087
2142
|
try {
|
|
2088
2143
|
if (this.#sandbox) root = await (0, node_fs_promises.mkdtemp)(node_path.default.join((0, node_os.tmpdir)(), "kubb-job-"));
|
|
2144
|
+
if (!this.#sandbox && !this.#permissions.allowWrite) outputRoot = await (0, node_fs_promises.mkdtemp)(node_path.default.join((0, node_os.tmpdir)(), "kubb-output-"));
|
|
2089
2145
|
return await this.#generate({
|
|
2090
2146
|
input,
|
|
2091
2147
|
root,
|
|
2148
|
+
outputRoot,
|
|
2092
2149
|
signal: controller.signal
|
|
2093
2150
|
});
|
|
2094
2151
|
} finally {
|
|
@@ -2099,12 +2156,18 @@ var Agent = class extends capnweb.RpcTarget {
|
|
|
2099
2156
|
recursive: true,
|
|
2100
2157
|
force: true
|
|
2101
2158
|
})]).catch(() => {});
|
|
2159
|
+
if (outputRoot) await (0, node_fs_promises.rm)(outputRoot, {
|
|
2160
|
+
recursive: true,
|
|
2161
|
+
force: true
|
|
2162
|
+
});
|
|
2102
2163
|
this.#session.active = void 0;
|
|
2103
2164
|
settled.resolve();
|
|
2104
2165
|
}
|
|
2105
2166
|
}
|
|
2106
|
-
/**
|
|
2107
|
-
|
|
2167
|
+
/**
|
|
2168
|
+
* Merges Studio's config over the project config and confines generation to the configured output folder.
|
|
2169
|
+
*/
|
|
2170
|
+
async #generate({ input, root, outputRoot, signal }) {
|
|
2108
2171
|
const { jobId, config: patch } = input;
|
|
2109
2172
|
const studioInput = this.#sandbox ? patch?.input : void 0;
|
|
2110
2173
|
if (this.#sandbox && (typeof studioInput !== "string" || !studioInput.trim() || (0, _kubb_core.getInputKind)(studioInput) !== "inline")) return this.#refuse({ message: require_protocol.AgentMessage.SANDBOX_INPUT_ONLY });
|
|
@@ -2135,37 +2198,28 @@ var Agent = class extends capnweb.RpcTarget {
|
|
|
2135
2198
|
});
|
|
2136
2199
|
const activePlugins = plugins ?? config.plugins;
|
|
2137
2200
|
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" });
|
|
2138
|
-
const diskFiles = !this.#sandbox && this.#permissions.allowRead ? await listDisk({
|
|
2139
|
-
root,
|
|
2140
|
-
outputPath: config.output.path,
|
|
2141
|
-
maxFiles: DISK_SNAPSHOT_MAX_FILES
|
|
2142
|
-
}) : void 0;
|
|
2143
|
-
const disk = diskFiles ? await this.#generations.keep({
|
|
2144
|
-
jobId,
|
|
2145
|
-
source: require_protocol.FileSource.DISK,
|
|
2146
|
-
files: diskFiles,
|
|
2147
|
-
maxSetMb: this.#limits.maxSnapshotMb
|
|
2148
|
-
}) : void 0;
|
|
2149
2201
|
const files = await generate({
|
|
2150
2202
|
config: {
|
|
2151
2203
|
...config,
|
|
2152
2204
|
root,
|
|
2153
2205
|
input: studioInput ?? config.input,
|
|
2154
|
-
storage:
|
|
2206
|
+
storage: outputRoot ? stagingStorage({
|
|
2207
|
+
root,
|
|
2208
|
+
directory: outputRoot
|
|
2209
|
+
}) : (0, _kubb_core.fsStorage)(),
|
|
2155
2210
|
plugins: activePlugins,
|
|
2156
2211
|
adapter,
|
|
2157
2212
|
parsers
|
|
2158
2213
|
},
|
|
2159
2214
|
hooks: this.#options.hooks,
|
|
2160
2215
|
signal,
|
|
2161
|
-
allowExec: this.#permissions.allowExec
|
|
2216
|
+
allowExec: this.#permissions.allowWrite && this.#permissions.allowExec
|
|
2162
2217
|
}).catch(async (error) => {
|
|
2163
2218
|
await this.#generations.drop(jobId);
|
|
2164
2219
|
throw error;
|
|
2165
2220
|
});
|
|
2166
2221
|
const output = await this.#generations.keep({
|
|
2167
2222
|
jobId,
|
|
2168
|
-
source: require_protocol.FileSource.OUTPUT,
|
|
2169
2223
|
files,
|
|
2170
2224
|
maxSetMb: this.#limits.maxMb
|
|
2171
2225
|
});
|
|
@@ -2178,16 +2232,14 @@ var Agent = class extends capnweb.RpcTarget {
|
|
|
2178
2232
|
await this.#generations.add({
|
|
2179
2233
|
jobId,
|
|
2180
2234
|
output,
|
|
2181
|
-
disk,
|
|
2182
2235
|
inputBytes
|
|
2183
2236
|
});
|
|
2184
|
-
await this.#logCommand("generate", `${activePlugins.length} plugin${activePlugins.length === 1 ? "" : "s"}, ${this.#permissions.allowWrite ? "written to disk" : "
|
|
2237
|
+
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" : ""}`);
|
|
2185
2238
|
return {
|
|
2186
2239
|
status: "success",
|
|
2187
2240
|
files: [...files.paths],
|
|
2188
2241
|
fileCount: files.paths.size,
|
|
2189
|
-
hashes: output.hashes
|
|
2190
|
-
disk: disk ? { hashes: disk.hashes } : void 0
|
|
2242
|
+
hashes: output.hashes
|
|
2191
2243
|
};
|
|
2192
2244
|
}
|
|
2193
2245
|
async saveConfig(input) {
|
|
@@ -2257,35 +2309,14 @@ var Agent = class extends capnweb.RpcTarget {
|
|
|
2257
2309
|
message: require_protocol.AgentMessage.READ_NOT_ALLOWED,
|
|
2258
2310
|
permission: require_protocol.AgentPermission.READ
|
|
2259
2311
|
});
|
|
2260
|
-
if (!Array.isArray(input.paths) || typeof input.jobId !== "string" || !input.jobId
|
|
2312
|
+
if (!Array.isArray(input.paths) || typeof input.jobId !== "string" || !input.jobId) return this.#refuse({ message: require_protocol.AgentMessage.FILE_REQUEST_INVALID });
|
|
2261
2313
|
const { paths } = input;
|
|
2262
2314
|
if (paths.length > 50) return this.#refuse({ message: require_protocol.AgentMessage.TOO_MANY_FILES });
|
|
2263
2315
|
if (!isValidPage(input)) return this.#refuse({ message: require_protocol.AgentMessage.INVALID_PAGE });
|
|
2264
|
-
if (input.jobType === "currentDisk") {
|
|
2265
|
-
if (this.#sandbox) return this.#refuse({ message: require_protocol.AgentMessage.SANDBOX_DISK_UNAVAILABLE });
|
|
2266
|
-
const config = await this.#options.loadConfig();
|
|
2267
|
-
const disk = await listDisk({
|
|
2268
|
-
root: this.#options.root,
|
|
2269
|
-
outputPath: config.output.path,
|
|
2270
|
-
maxFiles: DISK_SNAPSHOT_MAX_FILES
|
|
2271
|
-
});
|
|
2272
|
-
if (!disk) return this.#refuse({ message: require_protocol.AgentMessage.GENERATION_GONE });
|
|
2273
|
-
const result = await readDiskFiles({
|
|
2274
|
-
files: disk,
|
|
2275
|
-
paths,
|
|
2276
|
-
cursor: input.cursor,
|
|
2277
|
-
limit: input.limit
|
|
2278
|
-
});
|
|
2279
|
-
await this.#logCommand("readFiles", `read ${Object.keys(result.files).length}/${paths.length} requested file${paths.length === 1 ? "" : "s"}`);
|
|
2280
|
-
return result;
|
|
2281
|
-
}
|
|
2282
2316
|
const generation = await this.#generations.get(input.jobId);
|
|
2283
2317
|
if (!generation) return this.#refuse({ message: require_protocol.AgentMessage.GENERATION_GONE });
|
|
2284
|
-
const source = input.source === require_protocol.FileSource.DISK ? require_protocol.FileSource.DISK : require_protocol.FileSource.OUTPUT;
|
|
2285
|
-
if (!generation[source]) return this.#refuse({ message: require_protocol.AgentMessage.NO_DISK_SNAPSHOT });
|
|
2286
2318
|
const result = await this.#generations.read({
|
|
2287
2319
|
generation,
|
|
2288
|
-
source,
|
|
2289
2320
|
paths,
|
|
2290
2321
|
cursor: input.cursor,
|
|
2291
2322
|
limit: input.limit
|
|
@@ -2310,15 +2341,11 @@ var Agent = class extends capnweb.RpcTarget {
|
|
|
2310
2341
|
})) return this.#refuse({ message: require_protocol.AgentMessage.INVALID_PAGE });
|
|
2311
2342
|
const [generation, baseGeneration] = await Promise.all([this.#generations.get(jobId), this.#generations.get(base.jobId)]);
|
|
2312
2343
|
if (!generation || !baseGeneration) return this.#refuse({ message: require_protocol.AgentMessage.GENERATION_GONE });
|
|
2313
|
-
const baseSource = base.source === require_protocol.FileSource.DISK ? require_protocol.FileSource.DISK : require_protocol.FileSource.OUTPUT;
|
|
2314
|
-
if (!baseGeneration[baseSource]) return this.#refuse({ message: require_protocol.AgentMessage.NO_DISK_SNAPSHOT });
|
|
2315
2344
|
const [after, before] = await Promise.all([this.#generations.read({
|
|
2316
2345
|
generation,
|
|
2317
|
-
source: require_protocol.FileSource.OUTPUT,
|
|
2318
2346
|
paths: [path]
|
|
2319
2347
|
}), this.#generations.read({
|
|
2320
2348
|
generation: baseGeneration,
|
|
2321
|
-
source: baseSource,
|
|
2322
2349
|
paths: [path]
|
|
2323
2350
|
})]);
|
|
2324
2351
|
const afterContent = after.files[path];
|
|
@@ -2339,7 +2366,6 @@ var Agent = class extends capnweb.RpcTarget {
|
|
|
2339
2366
|
const key = JSON.stringify([
|
|
2340
2367
|
jobId,
|
|
2341
2368
|
base.jobId,
|
|
2342
|
-
baseSource,
|
|
2343
2369
|
path
|
|
2344
2370
|
]);
|
|
2345
2371
|
let diff = this.#diffs.get(key);
|
|
@@ -2396,14 +2422,15 @@ async function runSession({ options, openRpc, url, instanceId }) {
|
|
|
2396
2422
|
connected: Promise.withResolvers(),
|
|
2397
2423
|
closed: false
|
|
2398
2424
|
};
|
|
2425
|
+
const agent = new Agent({
|
|
2426
|
+
options,
|
|
2427
|
+
session
|
|
2428
|
+
});
|
|
2399
2429
|
const rpc = openRpc({
|
|
2400
2430
|
url,
|
|
2401
2431
|
token,
|
|
2402
2432
|
instanceId,
|
|
2403
|
-
agent
|
|
2404
|
-
options,
|
|
2405
|
-
session
|
|
2406
|
-
})
|
|
2433
|
+
agent
|
|
2407
2434
|
});
|
|
2408
2435
|
signal?.addEventListener("abort", rpc.close, { once: true });
|
|
2409
2436
|
let timer;
|
|
@@ -2438,6 +2465,7 @@ async function runSession({ options, openRpc, url, instanceId }) {
|
|
|
2438
2465
|
await emit(hooks, "studio:warn", { message: "Canceled the running generation: the connection to Kubb Studio closed" });
|
|
2439
2466
|
}
|
|
2440
2467
|
await active?.settled;
|
|
2468
|
+
await agent[Symbol.asyncDispose]();
|
|
2441
2469
|
return {
|
|
2442
2470
|
...close,
|
|
2443
2471
|
connected: session.info !== void 0
|
|
@@ -2747,8 +2775,7 @@ async function createSnapshotPackage({ files, packageInfo }) {
|
|
|
2747
2775
|
try {
|
|
2748
2776
|
await (0, node_fs_promises.mkdir)(dist);
|
|
2749
2777
|
await Promise.all([...sources].map(async ([path, { content }]) => {
|
|
2750
|
-
await (
|
|
2751
|
-
await (0, node_fs_promises.writeFile)((0, node_path.join)(root, path), content);
|
|
2778
|
+
await writeRaw((0, node_path.join)(root, path), content);
|
|
2752
2779
|
}));
|
|
2753
2780
|
const entry = [...sources.keys()].filter((path) => /\.(?:[cm]?[jt]sx?)$/.test(path)).map((path) => (0, node_path.join)(root, path));
|
|
2754
2781
|
if (entry.length) {
|
|
@@ -2788,7 +2815,7 @@ async function createSnapshotPackage({ files, packageInfo }) {
|
|
|
2788
2815
|
const chunks = [...new Map([
|
|
2789
2816
|
["package.json", JSON.stringify(manifest, null, 2)],
|
|
2790
2817
|
...[...sources].map(([path, { content }]) => [path, content]),
|
|
2791
|
-
...await Promise.all(built.map(async (key) => [`dist/${key}`, await (
|
|
2818
|
+
...await Promise.all(built.map(async (key) => [`dist/${key}`, await read((0, node_path.join)(dist, key))]))
|
|
2792
2819
|
])].flatMap(([path, content]) => {
|
|
2793
2820
|
const bytes = Buffer.from(content);
|
|
2794
2821
|
return [
|
|
@@ -2819,7 +2846,10 @@ async function createSnapshot({ studioUrl = defaultStudioUrl, apiKey, config, ho
|
|
|
2819
2846
|
path: "/api/snapshots"
|
|
2820
2847
|
});
|
|
2821
2848
|
const generated = await generate({
|
|
2822
|
-
config
|
|
2849
|
+
config: {
|
|
2850
|
+
...config,
|
|
2851
|
+
storage: (0, _kubb_core.fsStorage)()
|
|
2852
|
+
},
|
|
2823
2853
|
hooks,
|
|
2824
2854
|
signal,
|
|
2825
2855
|
allowExec: true
|