@kubb/studio 5.11.3 → 5.11.5
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 +184 -122
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +32 -1
- package/dist/index.d.ts +32 -1
- package/dist/index.js +187 -126
- package/dist/index.js.map +1 -1
- package/dist/{protocol-DpxvWX9E.d.ts → protocol--vpA4DkB.d.cts} +28 -42
- package/dist/{protocol-DpxvWX9E.d.cts → protocol--vpA4DkB.d.ts} +28 -42
- 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.5";
|
|
203
224
|
//#endregion
|
|
204
225
|
//#region src/resolveConfig.ts
|
|
205
226
|
/**
|
|
@@ -1061,13 +1082,17 @@ function applySet(source, call, path, value, quote) {
|
|
|
1061
1082
|
}
|
|
1062
1083
|
return { source: splice(source, rangeOf(target.value).start, rangeOf(target.value).end, printValue(value, quote)) };
|
|
1063
1084
|
}
|
|
1064
|
-
/**
|
|
1085
|
+
/**
|
|
1086
|
+
* Deletes the option at `path`, falling the plugin back to its default for it. An option the file
|
|
1087
|
+
* does not set already has its default, so removing it changes nothing.
|
|
1088
|
+
*/
|
|
1065
1089
|
function applyRemove(source, call, path) {
|
|
1066
1090
|
const options = call.arguments[0];
|
|
1067
|
-
if (options
|
|
1091
|
+
if (!options) return { source };
|
|
1092
|
+
if (options.type !== "ObjectExpression") return { reason: "the factory was not called with an object literal" };
|
|
1068
1093
|
const target = walk(options, path);
|
|
1069
1094
|
if ("reason" in target) return target;
|
|
1070
|
-
if ("missing" in target) return {
|
|
1095
|
+
if ("missing" in target) return { source };
|
|
1071
1096
|
return { source: removeMember(source, target.object, target.index) };
|
|
1072
1097
|
}
|
|
1073
1098
|
/**
|
|
@@ -1107,6 +1132,22 @@ function applyAddPlugin({ source, parsed, config, edit, quote, extensions }) {
|
|
|
1107
1132
|
const next = insertItem(source, plugins, `${importName}(${Object.keys(options).length ? printValue(options, quote) : ""})`);
|
|
1108
1133
|
return { source: taken ? next : insertImport(next, parsed, importName, edit.plugin) };
|
|
1109
1134
|
}
|
|
1135
|
+
/**
|
|
1136
|
+
* Rewrites the config's `plugins` array with one item per line, so a call can be commented out without
|
|
1137
|
+
* taking its neighbors with it. Not done when a comment sits between the items, which would have to move.
|
|
1138
|
+
*/
|
|
1139
|
+
function expandPluginsArray(source, config) {
|
|
1140
|
+
const array = property(config, "plugins");
|
|
1141
|
+
if (array?.type !== "ArrayExpression") return void 0;
|
|
1142
|
+
const items = array.elements.flatMap((element) => element ? [element] : []);
|
|
1143
|
+
const { start, end } = rangeOf(array);
|
|
1144
|
+
const ranges = items.map(rangeOf);
|
|
1145
|
+
const gaps = [source.slice(start + 1, ranges[0]?.start ?? end - 1), ...ranges.map((range, index) => source.slice(range.end, ranges[index + 1]?.start ?? end - 1))];
|
|
1146
|
+
if (!items.length || gaps.some((gap) => !/^[\s,]*$/.test(gap))) return void 0;
|
|
1147
|
+
const lineStart = source.lastIndexOf("\n", start - 1) + 1;
|
|
1148
|
+
const indent = source.slice(lineStart).match(/^[ \t]*/)?.[0] ?? "";
|
|
1149
|
+
return splice(source, start, end, `[\n${ranges.map((range) => `${indent} ${source.slice(range.start, range.end)},`).join("\n")}\n${indent}]`);
|
|
1150
|
+
}
|
|
1110
1151
|
/** Comments out a plugin call in place, keeping its options on disk so `enable-plugin` can restore them exactly. */
|
|
1111
1152
|
function disablePlugin(source, mod, config, plugin) {
|
|
1112
1153
|
const loc = factoryCalls(mod, config).find((entry) => entry.packageName === plugin)?.call.loc;
|
|
@@ -1134,9 +1175,17 @@ function applyEdit({ source, edit, quote, extensions }) {
|
|
|
1134
1175
|
const parsed = parseConfigs(source);
|
|
1135
1176
|
if ("reason" in parsed) return parsed;
|
|
1136
1177
|
const ref = edit.config;
|
|
1137
|
-
const
|
|
1178
|
+
const findConfig = (entries) => typeof ref === "string" ? entries.configs.find((entry) => configName(entry) === ref) : entries.configs[ref ?? 0];
|
|
1179
|
+
const config = findConfig(parsed);
|
|
1138
1180
|
if (!config) return { reason: `no config entry found for ${JSON.stringify(ref)}` };
|
|
1139
|
-
if (edit.operation === "disable-plugin")
|
|
1181
|
+
if (edit.operation === "disable-plugin") {
|
|
1182
|
+
const result = disablePlugin(source, parsed.mod, config, edit.plugin);
|
|
1183
|
+
if (!("reason" in result) || !result.reason.includes("shares a line")) return result;
|
|
1184
|
+
const expanded = expandPluginsArray(source, config);
|
|
1185
|
+
const reparsed = expanded === void 0 ? void 0 : parseConfigs(expanded);
|
|
1186
|
+
const reparsedConfig = reparsed && !("reason" in reparsed) ? findConfig(reparsed) : void 0;
|
|
1187
|
+
return expanded !== void 0 && reparsed && !("reason" in reparsed) && reparsedConfig ? disablePlugin(expanded, reparsed.mod, reparsedConfig, edit.plugin) : result;
|
|
1188
|
+
}
|
|
1140
1189
|
if (edit.operation === "enable-plugin") return enablePlugin(source, config, edit.plugin);
|
|
1141
1190
|
if (edit.operation === "add-plugin") return applyAddPlugin({
|
|
1142
1191
|
source,
|
|
@@ -1395,21 +1444,20 @@ const hashOf = (content) => (0, node_crypto.createHash)("sha1").update(content).
|
|
|
1395
1444
|
/**
|
|
1396
1445
|
* The bytes `entries` keep on disk: output, written files, and kept input.
|
|
1397
1446
|
*/
|
|
1398
|
-
const totalBytes = (entries) => entries.reduce((sum, { output,
|
|
1447
|
+
const totalBytes = (entries) => entries.reduce((sum, { output, inputBytes }) => sum + output.bytes + (inputBytes ?? 0), 0);
|
|
1399
1448
|
function positiveNumber(value) {
|
|
1400
1449
|
const parsed = Number(value);
|
|
1401
1450
|
return value && Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
|
|
1402
1451
|
}
|
|
1403
1452
|
/**
|
|
1404
1453
|
* How many generations an agent keeps and how large they may get, read from
|
|
1405
|
-
* `KUBB_AGENT_MAX_GENERATIONS
|
|
1454
|
+
* `KUBB_AGENT_MAX_GENERATIONS` and `KUBB_AGENT_MAX_GENERATIONS_MB`.
|
|
1406
1455
|
* An unset or invalid value keeps the default.
|
|
1407
1456
|
*/
|
|
1408
1457
|
function resolveGenerationLimits() {
|
|
1409
1458
|
return {
|
|
1410
1459
|
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
|
|
1460
|
+
maxMb: positiveNumber(node_process.default.env.KUBB_AGENT_MAX_GENERATIONS_MB) ?? 100
|
|
1413
1461
|
};
|
|
1414
1462
|
}
|
|
1415
1463
|
/**
|
|
@@ -1456,7 +1504,7 @@ async function generate({ config, hooks, signal, allowExec }) {
|
|
|
1456
1504
|
};
|
|
1457
1505
|
}
|
|
1458
1506
|
/**
|
|
1459
|
-
* What the output directory holds
|
|
1507
|
+
* What the output directory holds when the editor connects. `undefined` when `outputPath` is not a real
|
|
1460
1508
|
* subdirectory of `root` (listing the root would take in every source file) or holds more than `maxFiles`.
|
|
1461
1509
|
*/
|
|
1462
1510
|
async function listDisk({ root, outputPath, maxFiles }) {
|
|
@@ -1473,38 +1521,11 @@ async function listDisk({ root, outputPath, maxFiles }) {
|
|
|
1473
1521
|
paths: new Set(paths)
|
|
1474
1522
|
};
|
|
1475
1523
|
}
|
|
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
1524
|
/**
|
|
1504
1525
|
* Keeps recent generations by job id in `storage`, with an index next to them so a store on disk
|
|
1505
1526
|
* 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
|
|
1527
|
+
* tenant never reaches another's output. Initial disk output and generated jobs each get their
|
|
1528
|
+
* own `maxCount` and `maxMb` budget. Each budget always keeps its newest entry.
|
|
1508
1529
|
*/
|
|
1509
1530
|
function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now }) {
|
|
1510
1531
|
let index;
|
|
@@ -1523,7 +1544,7 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
|
|
|
1523
1544
|
/**
|
|
1524
1545
|
* Copies `files` into the store as one set of `jobId`. Above `maxSetMb` only the hashes are kept.
|
|
1525
1546
|
*/
|
|
1526
|
-
async function keep({ jobId,
|
|
1547
|
+
async function keep({ jobId, files, maxSetMb }) {
|
|
1527
1548
|
const maxSetBytes = maxSetMb * MB;
|
|
1528
1549
|
const hashes = {};
|
|
1529
1550
|
let bytes = 0;
|
|
@@ -1534,14 +1555,13 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
|
|
|
1534
1555
|
if (path.split("/").includes("..")) return;
|
|
1535
1556
|
const content = await files.storage.readItem((0, node_path.resolve)(files.root, path));
|
|
1536
1557
|
if (content === null) return;
|
|
1537
|
-
|
|
1538
|
-
hashes[path] = hashOf(normalize ? `${content.trim()}\n` : content);
|
|
1558
|
+
hashes[path] = hashOf(content);
|
|
1539
1559
|
bytes += Buffer.byteLength(content);
|
|
1540
|
-
if (bytes <= maxSetBytes) await storage.writeItem(`${dirOf(jobId)}
|
|
1560
|
+
if (bytes <= maxSetBytes) await storage.writeItem(`${dirOf(jobId)}output/${path}`, content);
|
|
1541
1561
|
}
|
|
1542
1562
|
});
|
|
1543
1563
|
if (bytes > maxSetBytes) {
|
|
1544
|
-
await storage.empty(`${dirOf(jobId)}
|
|
1564
|
+
await storage.empty(`${dirOf(jobId)}output/`);
|
|
1545
1565
|
return {
|
|
1546
1566
|
paths: [],
|
|
1547
1567
|
hashes,
|
|
@@ -1557,10 +1577,50 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
|
|
|
1557
1577
|
async function drop(jobId) {
|
|
1558
1578
|
await storage.empty(dirOf(jobId));
|
|
1559
1579
|
}
|
|
1580
|
+
async function add(generation) {
|
|
1581
|
+
const current = await load();
|
|
1582
|
+
for (const expired of current.filter((entry) => !isLive(entry))) await drop(expired.jobId);
|
|
1583
|
+
const live = current.filter((entry) => entry.jobId !== generation.jobId && isLive(entry));
|
|
1584
|
+
const sharesBudget = (entry) => Boolean(entry.initial) === Boolean(generation.initial);
|
|
1585
|
+
const entries = live.filter(sharesBudget);
|
|
1586
|
+
entries.push({
|
|
1587
|
+
...generation,
|
|
1588
|
+
keptAt: now()
|
|
1589
|
+
});
|
|
1590
|
+
while (entries.length > 1 && (entries.length > maxCount || totalBytes(entries) > maxMb * MB)) await drop(entries.shift().jobId);
|
|
1591
|
+
index = [...live.filter((entry) => !sharesBudget(entry)), ...entries];
|
|
1592
|
+
await storage.writeItem(INDEX_KEY, JSON.stringify(index));
|
|
1593
|
+
}
|
|
1560
1594
|
return {
|
|
1561
1595
|
keep,
|
|
1562
1596
|
drop,
|
|
1563
1597
|
/**
|
|
1598
|
+
* Captures initial disk output, reusing a retained ID when its hashes and readable paths match.
|
|
1599
|
+
* A temporary candidate is discarded on reuse, so reconnects do not consume retention slots.
|
|
1600
|
+
*/
|
|
1601
|
+
async keepInitial(files) {
|
|
1602
|
+
const jobId = (0, node_crypto.randomUUID)();
|
|
1603
|
+
const output = await keep({
|
|
1604
|
+
jobId,
|
|
1605
|
+
files,
|
|
1606
|
+
maxSetMb: maxMb
|
|
1607
|
+
});
|
|
1608
|
+
const hashes = Object.entries(output.hashes);
|
|
1609
|
+
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));
|
|
1610
|
+
if (previous) {
|
|
1611
|
+
await drop(jobId);
|
|
1612
|
+
await add(previous);
|
|
1613
|
+
return previous;
|
|
1614
|
+
}
|
|
1615
|
+
const generation = {
|
|
1616
|
+
jobId,
|
|
1617
|
+
output,
|
|
1618
|
+
initial: true
|
|
1619
|
+
};
|
|
1620
|
+
await add(generation);
|
|
1621
|
+
return generation;
|
|
1622
|
+
},
|
|
1623
|
+
/**
|
|
1564
1624
|
* Keeps the `InputNode` of `jobId` and returns its size in bytes, or 0 when it keeps none. A ref schema loses
|
|
1565
1625
|
* the copy of its target, which `input.schemas` holds: with it, a large spec outgrows what `JSON.stringify` can return.
|
|
1566
1626
|
*/
|
|
@@ -1588,30 +1648,19 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
|
|
|
1588
1648
|
return stored === null ? void 0 : JSON.parse(stored);
|
|
1589
1649
|
},
|
|
1590
1650
|
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
|
-
},
|
|
1651
|
+
add,
|
|
1603
1652
|
/**
|
|
1604
1653
|
* Reads the requested paths the set holds, skipping any it does not, one page of lines when `limit` is given.
|
|
1605
1654
|
*/
|
|
1606
|
-
async read({ generation,
|
|
1607
|
-
const kept = new Set(generation
|
|
1655
|
+
async read({ generation, paths, cursor = 0, limit }) {
|
|
1656
|
+
const kept = new Set(generation.output.paths);
|
|
1608
1657
|
const files = {};
|
|
1609
1658
|
const pages = {};
|
|
1610
1659
|
await inParallel({
|
|
1611
1660
|
items: paths.filter((path) => kept.has(path)),
|
|
1612
1661
|
limit: READ_CONCURRENCY,
|
|
1613
1662
|
run: async (path) => {
|
|
1614
|
-
const content = await storage.readItem(`${dirOf(generation.jobId)}
|
|
1663
|
+
const content = await storage.readItem(`${dirOf(generation.jobId)}output/${path}`);
|
|
1615
1664
|
if (content === null) return;
|
|
1616
1665
|
if (limit === void 0) {
|
|
1617
1666
|
files[path] = content;
|
|
@@ -1886,6 +1935,25 @@ function isValidPage({ cursor, limit }) {
|
|
|
1886
1935
|
if (cursor !== void 0 && (!Number.isSafeInteger(cursor) || cursor < 0)) return false;
|
|
1887
1936
|
return Number.isSafeInteger(limit) && limit >= 1 && limit <= 2e3;
|
|
1888
1937
|
}
|
|
1938
|
+
/**
|
|
1939
|
+
* Maps generated paths into a temporary directory while keeping the project root for input resolution.
|
|
1940
|
+
*/
|
|
1941
|
+
function stagingStorage({ root, directory }) {
|
|
1942
|
+
const storage = (0, _kubb_core.fsStorage)();
|
|
1943
|
+
const target = (key) => {
|
|
1944
|
+
const relative = node_path.default.relative(root, node_path.default.resolve(root, key));
|
|
1945
|
+
if (relative === ".." || relative.startsWith(`..${node_path.default.sep}`) || node_path.default.isAbsolute(relative)) throw new Error("Output path escapes the project root");
|
|
1946
|
+
return node_path.default.join(directory, relative);
|
|
1947
|
+
};
|
|
1948
|
+
return {
|
|
1949
|
+
name: "fs",
|
|
1950
|
+
readItem: (key) => storage.readItem(target(key)),
|
|
1951
|
+
writeItem: (key, value) => storage.writeItem(target(key), value),
|
|
1952
|
+
removeItem: (key) => storage.removeItem(target(key)),
|
|
1953
|
+
readKeys: (base) => storage.readKeys(base ? target(base) : directory),
|
|
1954
|
+
empty: (base) => base ? storage.empty(target(base)) : Promise.resolve()
|
|
1955
|
+
};
|
|
1956
|
+
}
|
|
1889
1957
|
var GenerationRunTarget = class extends capnweb.RpcTarget {
|
|
1890
1958
|
#run;
|
|
1891
1959
|
constructor(run) {
|
|
@@ -1910,6 +1978,7 @@ var Agent = class extends capnweb.RpcTarget {
|
|
|
1910
1978
|
#session;
|
|
1911
1979
|
#limits = resolveGenerationLimits();
|
|
1912
1980
|
#store;
|
|
1981
|
+
#sandboxCacheRoot = node_path.default.join((0, node_os.tmpdir)(), `kubb-studio-${(0, node_crypto.randomUUID)()}`);
|
|
1913
1982
|
#diffs = /* @__PURE__ */ new Map();
|
|
1914
1983
|
constructor({ options, session }) {
|
|
1915
1984
|
super();
|
|
@@ -1931,13 +2000,19 @@ var Agent = class extends capnweb.RpcTarget {
|
|
|
1931
2000
|
}
|
|
1932
2001
|
get #generations() {
|
|
1933
2002
|
this.#store ??= createGenerationStore({
|
|
1934
|
-
storage:
|
|
2003
|
+
storage: (0, _kubb_core.cacheStorage)({ root: this.#sandbox ? this.#sandboxCacheRoot : this.#options.root }),
|
|
1935
2004
|
maxCount: this.#limits.maxCount,
|
|
1936
2005
|
maxMb: this.#limits.maxMb,
|
|
1937
2006
|
ttlMs: this.#sandbox ? SANDBOX_GENERATION_TTL_MS : void 0
|
|
1938
2007
|
});
|
|
1939
2008
|
return this.#store;
|
|
1940
2009
|
}
|
|
2010
|
+
async clear() {
|
|
2011
|
+
if (this.#sandbox) await (0, node_fs_promises.rm)((0, _kubb_core.resolveCacheDir)(this.#sandboxCacheRoot), {
|
|
2012
|
+
recursive: true,
|
|
2013
|
+
force: true
|
|
2014
|
+
});
|
|
2015
|
+
}
|
|
1941
2016
|
#warn({ message, permission }) {
|
|
1942
2017
|
return emit(this.#options.hooks, "studio:warn", {
|
|
1943
2018
|
message,
|
|
@@ -1962,10 +2037,12 @@ var Agent = class extends capnweb.RpcTarget {
|
|
|
1962
2037
|
message: "Studio must call connect() before any other command"
|
|
1963
2038
|
});
|
|
1964
2039
|
}
|
|
1965
|
-
#logCommand(command, info) {
|
|
2040
|
+
#logCommand(command, info, requestedBy, failed) {
|
|
1966
2041
|
return emit(this.#options.hooks, "studio:command", {
|
|
1967
2042
|
command,
|
|
1968
|
-
info
|
|
2043
|
+
info,
|
|
2044
|
+
requestedBy,
|
|
2045
|
+
failed
|
|
1969
2046
|
});
|
|
1970
2047
|
}
|
|
1971
2048
|
async #readConfigFileView(source) {
|
|
@@ -2049,7 +2126,12 @@ var Agent = class extends capnweb.RpcTarget {
|
|
|
2049
2126
|
maxFiles: DISK_SNAPSHOT_MAX_FILES
|
|
2050
2127
|
});
|
|
2051
2128
|
if (!files) return void 0;
|
|
2052
|
-
|
|
2129
|
+
const { jobId, output } = await this.#generations.keepInitial(files);
|
|
2130
|
+
return {
|
|
2131
|
+
jobId,
|
|
2132
|
+
files: output.paths,
|
|
2133
|
+
hashes: output.hashes
|
|
2134
|
+
};
|
|
2053
2135
|
}
|
|
2054
2136
|
startGeneration(input) {
|
|
2055
2137
|
const stream = createGenerationStream(this.#options.hooks, input.jobId);
|
|
@@ -2068,7 +2150,9 @@ var Agent = class extends capnweb.RpcTarget {
|
|
|
2068
2150
|
}
|
|
2069
2151
|
});
|
|
2070
2152
|
}
|
|
2071
|
-
/**
|
|
2153
|
+
/**
|
|
2154
|
+
* Keeps a job's output and permitted parsed input, then cleans up its temporary generation directories.
|
|
2155
|
+
*/
|
|
2072
2156
|
async #runGeneration(input, controller) {
|
|
2073
2157
|
this.#assertConnected();
|
|
2074
2158
|
if (this.#session.closed) throw new _kubb_core.Diagnostics.Error({
|
|
@@ -2084,11 +2168,14 @@ var Agent = class extends capnweb.RpcTarget {
|
|
|
2084
2168
|
settled: settled.promise
|
|
2085
2169
|
};
|
|
2086
2170
|
let root = this.#options.root;
|
|
2171
|
+
let outputRoot;
|
|
2087
2172
|
try {
|
|
2088
2173
|
if (this.#sandbox) root = await (0, node_fs_promises.mkdtemp)(node_path.default.join((0, node_os.tmpdir)(), "kubb-job-"));
|
|
2174
|
+
if (!this.#sandbox && !this.#permissions.allowWrite) outputRoot = await (0, node_fs_promises.mkdtemp)(node_path.default.join((0, node_os.tmpdir)(), "kubb-output-"));
|
|
2089
2175
|
return await this.#generate({
|
|
2090
2176
|
input,
|
|
2091
2177
|
root,
|
|
2178
|
+
outputRoot,
|
|
2092
2179
|
signal: controller.signal
|
|
2093
2180
|
});
|
|
2094
2181
|
} finally {
|
|
@@ -2099,12 +2186,18 @@ var Agent = class extends capnweb.RpcTarget {
|
|
|
2099
2186
|
recursive: true,
|
|
2100
2187
|
force: true
|
|
2101
2188
|
})]).catch(() => {});
|
|
2189
|
+
if (outputRoot) await (0, node_fs_promises.rm)(outputRoot, {
|
|
2190
|
+
recursive: true,
|
|
2191
|
+
force: true
|
|
2192
|
+
});
|
|
2102
2193
|
this.#session.active = void 0;
|
|
2103
2194
|
settled.resolve();
|
|
2104
2195
|
}
|
|
2105
2196
|
}
|
|
2106
|
-
/**
|
|
2107
|
-
|
|
2197
|
+
/**
|
|
2198
|
+
* Merges Studio's config over the project config and confines generation to the configured output folder.
|
|
2199
|
+
*/
|
|
2200
|
+
async #generate({ input, root, outputRoot, signal }) {
|
|
2108
2201
|
const { jobId, config: patch } = input;
|
|
2109
2202
|
const studioInput = this.#sandbox ? patch?.input : void 0;
|
|
2110
2203
|
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 +2228,29 @@ var Agent = class extends capnweb.RpcTarget {
|
|
|
2135
2228
|
});
|
|
2136
2229
|
const activePlugins = plugins ?? config.plugins;
|
|
2137
2230
|
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
2231
|
const files = await generate({
|
|
2150
2232
|
config: {
|
|
2151
2233
|
...config,
|
|
2152
2234
|
root,
|
|
2153
2235
|
input: studioInput ?? config.input,
|
|
2154
|
-
storage:
|
|
2236
|
+
storage: outputRoot ? stagingStorage({
|
|
2237
|
+
root,
|
|
2238
|
+
directory: outputRoot
|
|
2239
|
+
}) : (0, _kubb_core.fsStorage)(),
|
|
2155
2240
|
plugins: activePlugins,
|
|
2156
2241
|
adapter,
|
|
2157
2242
|
parsers
|
|
2158
2243
|
},
|
|
2159
2244
|
hooks: this.#options.hooks,
|
|
2160
2245
|
signal,
|
|
2161
|
-
allowExec: this.#permissions.allowExec
|
|
2246
|
+
allowExec: this.#permissions.allowWrite && this.#permissions.allowExec
|
|
2162
2247
|
}).catch(async (error) => {
|
|
2163
2248
|
await this.#generations.drop(jobId);
|
|
2249
|
+
await this.#logCommand("generate", getErrorMessage(error), input.requestedBy, true);
|
|
2164
2250
|
throw error;
|
|
2165
2251
|
});
|
|
2166
2252
|
const output = await this.#generations.keep({
|
|
2167
2253
|
jobId,
|
|
2168
|
-
source: require_protocol.FileSource.OUTPUT,
|
|
2169
2254
|
files,
|
|
2170
2255
|
maxSetMb: this.#limits.maxMb
|
|
2171
2256
|
});
|
|
@@ -2178,16 +2263,14 @@ var Agent = class extends capnweb.RpcTarget {
|
|
|
2178
2263
|
await this.#generations.add({
|
|
2179
2264
|
jobId,
|
|
2180
2265
|
output,
|
|
2181
|
-
disk,
|
|
2182
2266
|
inputBytes
|
|
2183
2267
|
});
|
|
2184
|
-
await this.#logCommand("generate", `${activePlugins.length} plugin${activePlugins.length === 1 ? "" : "s"}, ${this.#permissions.allowWrite ? "written to disk" : "
|
|
2268
|
+
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" : ""}`, input.requestedBy);
|
|
2185
2269
|
return {
|
|
2186
2270
|
status: "success",
|
|
2187
2271
|
files: [...files.paths],
|
|
2188
2272
|
fileCount: files.paths.size,
|
|
2189
|
-
hashes: output.hashes
|
|
2190
|
-
disk: disk ? { hashes: disk.hashes } : void 0
|
|
2273
|
+
hashes: output.hashes
|
|
2191
2274
|
};
|
|
2192
2275
|
}
|
|
2193
2276
|
async saveConfig(input) {
|
|
@@ -2221,7 +2304,7 @@ var Agent = class extends capnweb.RpcTarget {
|
|
|
2221
2304
|
extensions: await this.#installedExtensions()
|
|
2222
2305
|
});
|
|
2223
2306
|
const applied = outcomes.filter((outcome) => outcome.applied).length;
|
|
2224
|
-
await this.#logCommand("saveConfig", `applied ${applied}/${outcomes.length} edits to ${configPath}
|
|
2307
|
+
await this.#logCommand("saveConfig", `applied ${applied}/${outcomes.length} edits to ${configPath}`, input.requestedBy);
|
|
2225
2308
|
return {
|
|
2226
2309
|
outcomes,
|
|
2227
2310
|
changed
|
|
@@ -2257,35 +2340,14 @@ var Agent = class extends capnweb.RpcTarget {
|
|
|
2257
2340
|
message: require_protocol.AgentMessage.READ_NOT_ALLOWED,
|
|
2258
2341
|
permission: require_protocol.AgentPermission.READ
|
|
2259
2342
|
});
|
|
2260
|
-
if (!Array.isArray(input.paths) || typeof input.jobId !== "string" || !input.jobId
|
|
2343
|
+
if (!Array.isArray(input.paths) || typeof input.jobId !== "string" || !input.jobId) return this.#refuse({ message: require_protocol.AgentMessage.FILE_REQUEST_INVALID });
|
|
2261
2344
|
const { paths } = input;
|
|
2262
2345
|
if (paths.length > 50) return this.#refuse({ message: require_protocol.AgentMessage.TOO_MANY_FILES });
|
|
2263
2346
|
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
2347
|
const generation = await this.#generations.get(input.jobId);
|
|
2283
2348
|
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
2349
|
const result = await this.#generations.read({
|
|
2287
2350
|
generation,
|
|
2288
|
-
source,
|
|
2289
2351
|
paths,
|
|
2290
2352
|
cursor: input.cursor,
|
|
2291
2353
|
limit: input.limit
|
|
@@ -2310,15 +2372,11 @@ var Agent = class extends capnweb.RpcTarget {
|
|
|
2310
2372
|
})) return this.#refuse({ message: require_protocol.AgentMessage.INVALID_PAGE });
|
|
2311
2373
|
const [generation, baseGeneration] = await Promise.all([this.#generations.get(jobId), this.#generations.get(base.jobId)]);
|
|
2312
2374
|
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
2375
|
const [after, before] = await Promise.all([this.#generations.read({
|
|
2316
2376
|
generation,
|
|
2317
|
-
source: require_protocol.FileSource.OUTPUT,
|
|
2318
2377
|
paths: [path]
|
|
2319
2378
|
}), this.#generations.read({
|
|
2320
2379
|
generation: baseGeneration,
|
|
2321
|
-
source: baseSource,
|
|
2322
2380
|
paths: [path]
|
|
2323
2381
|
})]);
|
|
2324
2382
|
const afterContent = after.files[path];
|
|
@@ -2339,7 +2397,6 @@ var Agent = class extends capnweb.RpcTarget {
|
|
|
2339
2397
|
const key = JSON.stringify([
|
|
2340
2398
|
jobId,
|
|
2341
2399
|
base.jobId,
|
|
2342
|
-
baseSource,
|
|
2343
2400
|
path
|
|
2344
2401
|
]);
|
|
2345
2402
|
let diff = this.#diffs.get(key);
|
|
@@ -2396,14 +2453,15 @@ async function runSession({ options, openRpc, url, instanceId }) {
|
|
|
2396
2453
|
connected: Promise.withResolvers(),
|
|
2397
2454
|
closed: false
|
|
2398
2455
|
};
|
|
2456
|
+
const agent = new Agent({
|
|
2457
|
+
options,
|
|
2458
|
+
session
|
|
2459
|
+
});
|
|
2399
2460
|
const rpc = openRpc({
|
|
2400
2461
|
url,
|
|
2401
2462
|
token,
|
|
2402
2463
|
instanceId,
|
|
2403
|
-
agent
|
|
2404
|
-
options,
|
|
2405
|
-
session
|
|
2406
|
-
})
|
|
2464
|
+
agent
|
|
2407
2465
|
});
|
|
2408
2466
|
signal?.addEventListener("abort", rpc.close, { once: true });
|
|
2409
2467
|
let timer;
|
|
@@ -2438,6 +2496,7 @@ async function runSession({ options, openRpc, url, instanceId }) {
|
|
|
2438
2496
|
await emit(hooks, "studio:warn", { message: "Canceled the running generation: the connection to Kubb Studio closed" });
|
|
2439
2497
|
}
|
|
2440
2498
|
await active?.settled;
|
|
2499
|
+
await agent.clear();
|
|
2441
2500
|
return {
|
|
2442
2501
|
...close,
|
|
2443
2502
|
connected: session.info !== void 0
|
|
@@ -2747,8 +2806,7 @@ async function createSnapshotPackage({ files, packageInfo }) {
|
|
|
2747
2806
|
try {
|
|
2748
2807
|
await (0, node_fs_promises.mkdir)(dist);
|
|
2749
2808
|
await Promise.all([...sources].map(async ([path, { content }]) => {
|
|
2750
|
-
await (
|
|
2751
|
-
await (0, node_fs_promises.writeFile)((0, node_path.join)(root, path), content);
|
|
2809
|
+
await writeRaw((0, node_path.join)(root, path), content);
|
|
2752
2810
|
}));
|
|
2753
2811
|
const entry = [...sources.keys()].filter((path) => /\.(?:[cm]?[jt]sx?)$/.test(path)).map((path) => (0, node_path.join)(root, path));
|
|
2754
2812
|
if (entry.length) {
|
|
@@ -2788,7 +2846,7 @@ async function createSnapshotPackage({ files, packageInfo }) {
|
|
|
2788
2846
|
const chunks = [...new Map([
|
|
2789
2847
|
["package.json", JSON.stringify(manifest, null, 2)],
|
|
2790
2848
|
...[...sources].map(([path, { content }]) => [path, content]),
|
|
2791
|
-
...await Promise.all(built.map(async (key) => [`dist/${key}`, await (
|
|
2849
|
+
...await Promise.all(built.map(async (key) => [`dist/${key}`, await read((0, node_path.join)(dist, key))]))
|
|
2792
2850
|
])].flatMap(([path, content]) => {
|
|
2793
2851
|
const bytes = Buffer.from(content);
|
|
2794
2852
|
return [
|
|
@@ -2819,7 +2877,10 @@ async function createSnapshot({ studioUrl = defaultStudioUrl, apiKey, config, ho
|
|
|
2819
2877
|
path: "/api/snapshots"
|
|
2820
2878
|
});
|
|
2821
2879
|
const generated = await generate({
|
|
2822
|
-
config
|
|
2880
|
+
config: {
|
|
2881
|
+
...config,
|
|
2882
|
+
storage: (0, _kubb_core.fsStorage)()
|
|
2883
|
+
},
|
|
2823
2884
|
hooks,
|
|
2824
2885
|
signal,
|
|
2825
2886
|
allowExec: true
|
|
@@ -2903,6 +2964,7 @@ exports.InvalidAgentTokenError = InvalidAgentTokenError;
|
|
|
2903
2964
|
exports.PairingCanceledError = PairingCanceledError;
|
|
2904
2965
|
exports.PairingDeniedError = PairingDeniedError;
|
|
2905
2966
|
exports.PairingExpiredError = PairingExpiredError;
|
|
2967
|
+
exports.applyConfigEdits = applyConfigEdits;
|
|
2906
2968
|
exports.connectAgent = connectAgent;
|
|
2907
2969
|
exports.createSnapshot = createSnapshot;
|
|
2908
2970
|
exports.defaultStudioUrl = defaultStudioUrl;
|