@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.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.5";
|
|
179
200
|
//#endregion
|
|
180
201
|
//#region src/resolveConfig.ts
|
|
181
202
|
/**
|
|
@@ -1037,13 +1058,17 @@ function applySet(source, call, path, value, quote) {
|
|
|
1037
1058
|
}
|
|
1038
1059
|
return { source: splice(source, rangeOf(target.value).start, rangeOf(target.value).end, printValue(value, quote)) };
|
|
1039
1060
|
}
|
|
1040
|
-
/**
|
|
1061
|
+
/**
|
|
1062
|
+
* Deletes the option at `path`, falling the plugin back to its default for it. An option the file
|
|
1063
|
+
* does not set already has its default, so removing it changes nothing.
|
|
1064
|
+
*/
|
|
1041
1065
|
function applyRemove(source, call, path) {
|
|
1042
1066
|
const options = call.arguments[0];
|
|
1043
|
-
if (options
|
|
1067
|
+
if (!options) return { source };
|
|
1068
|
+
if (options.type !== "ObjectExpression") return { reason: "the factory was not called with an object literal" };
|
|
1044
1069
|
const target = walk(options, path);
|
|
1045
1070
|
if ("reason" in target) return target;
|
|
1046
|
-
if ("missing" in target) return {
|
|
1071
|
+
if ("missing" in target) return { source };
|
|
1047
1072
|
return { source: removeMember(source, target.object, target.index) };
|
|
1048
1073
|
}
|
|
1049
1074
|
/**
|
|
@@ -1083,6 +1108,22 @@ function applyAddPlugin({ source, parsed, config, edit, quote, extensions }) {
|
|
|
1083
1108
|
const next = insertItem(source, plugins, `${importName}(${Object.keys(options).length ? printValue(options, quote) : ""})`);
|
|
1084
1109
|
return { source: taken ? next : insertImport(next, parsed, importName, edit.plugin) };
|
|
1085
1110
|
}
|
|
1111
|
+
/**
|
|
1112
|
+
* Rewrites the config's `plugins` array with one item per line, so a call can be commented out without
|
|
1113
|
+
* taking its neighbors with it. Not done when a comment sits between the items, which would have to move.
|
|
1114
|
+
*/
|
|
1115
|
+
function expandPluginsArray(source, config) {
|
|
1116
|
+
const array = property(config, "plugins");
|
|
1117
|
+
if (array?.type !== "ArrayExpression") return void 0;
|
|
1118
|
+
const items = array.elements.flatMap((element) => element ? [element] : []);
|
|
1119
|
+
const { start, end } = rangeOf(array);
|
|
1120
|
+
const ranges = items.map(rangeOf);
|
|
1121
|
+
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))];
|
|
1122
|
+
if (!items.length || gaps.some((gap) => !/^[\s,]*$/.test(gap))) return void 0;
|
|
1123
|
+
const lineStart = source.lastIndexOf("\n", start - 1) + 1;
|
|
1124
|
+
const indent = source.slice(lineStart).match(/^[ \t]*/)?.[0] ?? "";
|
|
1125
|
+
return splice(source, start, end, `[\n${ranges.map((range) => `${indent} ${source.slice(range.start, range.end)},`).join("\n")}\n${indent}]`);
|
|
1126
|
+
}
|
|
1086
1127
|
/** Comments out a plugin call in place, keeping its options on disk so `enable-plugin` can restore them exactly. */
|
|
1087
1128
|
function disablePlugin(source, mod, config, plugin) {
|
|
1088
1129
|
const loc = factoryCalls(mod, config).find((entry) => entry.packageName === plugin)?.call.loc;
|
|
@@ -1110,9 +1151,17 @@ function applyEdit({ source, edit, quote, extensions }) {
|
|
|
1110
1151
|
const parsed = parseConfigs(source);
|
|
1111
1152
|
if ("reason" in parsed) return parsed;
|
|
1112
1153
|
const ref = edit.config;
|
|
1113
|
-
const
|
|
1154
|
+
const findConfig = (entries) => typeof ref === "string" ? entries.configs.find((entry) => configName(entry) === ref) : entries.configs[ref ?? 0];
|
|
1155
|
+
const config = findConfig(parsed);
|
|
1114
1156
|
if (!config) return { reason: `no config entry found for ${JSON.stringify(ref)}` };
|
|
1115
|
-
if (edit.operation === "disable-plugin")
|
|
1157
|
+
if (edit.operation === "disable-plugin") {
|
|
1158
|
+
const result = disablePlugin(source, parsed.mod, config, edit.plugin);
|
|
1159
|
+
if (!("reason" in result) || !result.reason.includes("shares a line")) return result;
|
|
1160
|
+
const expanded = expandPluginsArray(source, config);
|
|
1161
|
+
const reparsed = expanded === void 0 ? void 0 : parseConfigs(expanded);
|
|
1162
|
+
const reparsedConfig = reparsed && !("reason" in reparsed) ? findConfig(reparsed) : void 0;
|
|
1163
|
+
return expanded !== void 0 && reparsed && !("reason" in reparsed) && reparsedConfig ? disablePlugin(expanded, reparsed.mod, reparsedConfig, edit.plugin) : result;
|
|
1164
|
+
}
|
|
1116
1165
|
if (edit.operation === "enable-plugin") return enablePlugin(source, config, edit.plugin);
|
|
1117
1166
|
if (edit.operation === "add-plugin") return applyAddPlugin({
|
|
1118
1167
|
source,
|
|
@@ -1371,21 +1420,20 @@ const hashOf = (content) => createHash("sha1").update(content).digest("hex").sli
|
|
|
1371
1420
|
/**
|
|
1372
1421
|
* The bytes `entries` keep on disk: output, written files, and kept input.
|
|
1373
1422
|
*/
|
|
1374
|
-
const totalBytes = (entries) => entries.reduce((sum, { output,
|
|
1423
|
+
const totalBytes = (entries) => entries.reduce((sum, { output, inputBytes }) => sum + output.bytes + (inputBytes ?? 0), 0);
|
|
1375
1424
|
function positiveNumber(value) {
|
|
1376
1425
|
const parsed = Number(value);
|
|
1377
1426
|
return value && Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
|
|
1378
1427
|
}
|
|
1379
1428
|
/**
|
|
1380
1429
|
* How many generations an agent keeps and how large they may get, read from
|
|
1381
|
-
* `KUBB_AGENT_MAX_GENERATIONS
|
|
1430
|
+
* `KUBB_AGENT_MAX_GENERATIONS` and `KUBB_AGENT_MAX_GENERATIONS_MB`.
|
|
1382
1431
|
* An unset or invalid value keeps the default.
|
|
1383
1432
|
*/
|
|
1384
1433
|
function resolveGenerationLimits() {
|
|
1385
1434
|
return {
|
|
1386
1435
|
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
|
|
1436
|
+
maxMb: positiveNumber(process$1.env.KUBB_AGENT_MAX_GENERATIONS_MB) ?? 100
|
|
1389
1437
|
};
|
|
1390
1438
|
}
|
|
1391
1439
|
/**
|
|
@@ -1432,7 +1480,7 @@ async function generate({ config, hooks, signal, allowExec }) {
|
|
|
1432
1480
|
};
|
|
1433
1481
|
}
|
|
1434
1482
|
/**
|
|
1435
|
-
* What the output directory holds
|
|
1483
|
+
* What the output directory holds when the editor connects. `undefined` when `outputPath` is not a real
|
|
1436
1484
|
* subdirectory of `root` (listing the root would take in every source file) or holds more than `maxFiles`.
|
|
1437
1485
|
*/
|
|
1438
1486
|
async function listDisk({ root, outputPath, maxFiles }) {
|
|
@@ -1449,38 +1497,11 @@ async function listDisk({ root, outputPath, maxFiles }) {
|
|
|
1449
1497
|
paths: new Set(paths)
|
|
1450
1498
|
};
|
|
1451
1499
|
}
|
|
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
1500
|
/**
|
|
1480
1501
|
* Keeps recent generations by job id in `storage`, with an index next to them so a store on disk
|
|
1481
1502
|
* 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
|
|
1503
|
+
* tenant never reaches another's output. Initial disk output and generated jobs each get their
|
|
1504
|
+
* own `maxCount` and `maxMb` budget. Each budget always keeps its newest entry.
|
|
1484
1505
|
*/
|
|
1485
1506
|
function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now }) {
|
|
1486
1507
|
let index;
|
|
@@ -1499,7 +1520,7 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
|
|
|
1499
1520
|
/**
|
|
1500
1521
|
* Copies `files` into the store as one set of `jobId`. Above `maxSetMb` only the hashes are kept.
|
|
1501
1522
|
*/
|
|
1502
|
-
async function keep({ jobId,
|
|
1523
|
+
async function keep({ jobId, files, maxSetMb }) {
|
|
1503
1524
|
const maxSetBytes = maxSetMb * MB;
|
|
1504
1525
|
const hashes = {};
|
|
1505
1526
|
let bytes = 0;
|
|
@@ -1510,14 +1531,13 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
|
|
|
1510
1531
|
if (path.split("/").includes("..")) return;
|
|
1511
1532
|
const content = await files.storage.readItem(resolve(files.root, path));
|
|
1512
1533
|
if (content === null) return;
|
|
1513
|
-
|
|
1514
|
-
hashes[path] = hashOf(normalize ? `${content.trim()}\n` : content);
|
|
1534
|
+
hashes[path] = hashOf(content);
|
|
1515
1535
|
bytes += Buffer.byteLength(content);
|
|
1516
|
-
if (bytes <= maxSetBytes) await storage.writeItem(`${dirOf(jobId)}
|
|
1536
|
+
if (bytes <= maxSetBytes) await storage.writeItem(`${dirOf(jobId)}output/${path}`, content);
|
|
1517
1537
|
}
|
|
1518
1538
|
});
|
|
1519
1539
|
if (bytes > maxSetBytes) {
|
|
1520
|
-
await storage.empty(`${dirOf(jobId)}
|
|
1540
|
+
await storage.empty(`${dirOf(jobId)}output/`);
|
|
1521
1541
|
return {
|
|
1522
1542
|
paths: [],
|
|
1523
1543
|
hashes,
|
|
@@ -1533,10 +1553,50 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
|
|
|
1533
1553
|
async function drop(jobId) {
|
|
1534
1554
|
await storage.empty(dirOf(jobId));
|
|
1535
1555
|
}
|
|
1556
|
+
async function add(generation) {
|
|
1557
|
+
const current = await load();
|
|
1558
|
+
for (const expired of current.filter((entry) => !isLive(entry))) await drop(expired.jobId);
|
|
1559
|
+
const live = current.filter((entry) => entry.jobId !== generation.jobId && isLive(entry));
|
|
1560
|
+
const sharesBudget = (entry) => Boolean(entry.initial) === Boolean(generation.initial);
|
|
1561
|
+
const entries = live.filter(sharesBudget);
|
|
1562
|
+
entries.push({
|
|
1563
|
+
...generation,
|
|
1564
|
+
keptAt: now()
|
|
1565
|
+
});
|
|
1566
|
+
while (entries.length > 1 && (entries.length > maxCount || totalBytes(entries) > maxMb * MB)) await drop(entries.shift().jobId);
|
|
1567
|
+
index = [...live.filter((entry) => !sharesBudget(entry)), ...entries];
|
|
1568
|
+
await storage.writeItem(INDEX_KEY, JSON.stringify(index));
|
|
1569
|
+
}
|
|
1536
1570
|
return {
|
|
1537
1571
|
keep,
|
|
1538
1572
|
drop,
|
|
1539
1573
|
/**
|
|
1574
|
+
* Captures initial disk output, reusing a retained ID when its hashes and readable paths match.
|
|
1575
|
+
* A temporary candidate is discarded on reuse, so reconnects do not consume retention slots.
|
|
1576
|
+
*/
|
|
1577
|
+
async keepInitial(files) {
|
|
1578
|
+
const jobId = randomUUID();
|
|
1579
|
+
const output = await keep({
|
|
1580
|
+
jobId,
|
|
1581
|
+
files,
|
|
1582
|
+
maxSetMb: maxMb
|
|
1583
|
+
});
|
|
1584
|
+
const hashes = Object.entries(output.hashes);
|
|
1585
|
+
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));
|
|
1586
|
+
if (previous) {
|
|
1587
|
+
await drop(jobId);
|
|
1588
|
+
await add(previous);
|
|
1589
|
+
return previous;
|
|
1590
|
+
}
|
|
1591
|
+
const generation = {
|
|
1592
|
+
jobId,
|
|
1593
|
+
output,
|
|
1594
|
+
initial: true
|
|
1595
|
+
};
|
|
1596
|
+
await add(generation);
|
|
1597
|
+
return generation;
|
|
1598
|
+
},
|
|
1599
|
+
/**
|
|
1540
1600
|
* Keeps the `InputNode` of `jobId` and returns its size in bytes, or 0 when it keeps none. A ref schema loses
|
|
1541
1601
|
* the copy of its target, which `input.schemas` holds: with it, a large spec outgrows what `JSON.stringify` can return.
|
|
1542
1602
|
*/
|
|
@@ -1564,30 +1624,19 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
|
|
|
1564
1624
|
return stored === null ? void 0 : JSON.parse(stored);
|
|
1565
1625
|
},
|
|
1566
1626
|
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
|
-
},
|
|
1627
|
+
add,
|
|
1579
1628
|
/**
|
|
1580
1629
|
* Reads the requested paths the set holds, skipping any it does not, one page of lines when `limit` is given.
|
|
1581
1630
|
*/
|
|
1582
|
-
async read({ generation,
|
|
1583
|
-
const kept = new Set(generation
|
|
1631
|
+
async read({ generation, paths, cursor = 0, limit }) {
|
|
1632
|
+
const kept = new Set(generation.output.paths);
|
|
1584
1633
|
const files = {};
|
|
1585
1634
|
const pages = {};
|
|
1586
1635
|
await inParallel({
|
|
1587
1636
|
items: paths.filter((path) => kept.has(path)),
|
|
1588
1637
|
limit: READ_CONCURRENCY,
|
|
1589
1638
|
run: async (path) => {
|
|
1590
|
-
const content = await storage.readItem(`${dirOf(generation.jobId)}
|
|
1639
|
+
const content = await storage.readItem(`${dirOf(generation.jobId)}output/${path}`);
|
|
1591
1640
|
if (content === null) return;
|
|
1592
1641
|
if (limit === void 0) {
|
|
1593
1642
|
files[path] = content;
|
|
@@ -1862,6 +1911,25 @@ function isValidPage({ cursor, limit }) {
|
|
|
1862
1911
|
if (cursor !== void 0 && (!Number.isSafeInteger(cursor) || cursor < 0)) return false;
|
|
1863
1912
|
return Number.isSafeInteger(limit) && limit >= 1 && limit <= 2e3;
|
|
1864
1913
|
}
|
|
1914
|
+
/**
|
|
1915
|
+
* Maps generated paths into a temporary directory while keeping the project root for input resolution.
|
|
1916
|
+
*/
|
|
1917
|
+
function stagingStorage({ root, directory }) {
|
|
1918
|
+
const storage = fsStorage();
|
|
1919
|
+
const target = (key) => {
|
|
1920
|
+
const relative = path.relative(root, path.resolve(root, key));
|
|
1921
|
+
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) throw new Error("Output path escapes the project root");
|
|
1922
|
+
return path.join(directory, relative);
|
|
1923
|
+
};
|
|
1924
|
+
return {
|
|
1925
|
+
name: "fs",
|
|
1926
|
+
readItem: (key) => storage.readItem(target(key)),
|
|
1927
|
+
writeItem: (key, value) => storage.writeItem(target(key), value),
|
|
1928
|
+
removeItem: (key) => storage.removeItem(target(key)),
|
|
1929
|
+
readKeys: (base) => storage.readKeys(base ? target(base) : directory),
|
|
1930
|
+
empty: (base) => base ? storage.empty(target(base)) : Promise.resolve()
|
|
1931
|
+
};
|
|
1932
|
+
}
|
|
1865
1933
|
var GenerationRunTarget = class extends RpcTarget {
|
|
1866
1934
|
#run;
|
|
1867
1935
|
constructor(run) {
|
|
@@ -1886,6 +1954,7 @@ var Agent = class extends RpcTarget {
|
|
|
1886
1954
|
#session;
|
|
1887
1955
|
#limits = resolveGenerationLimits();
|
|
1888
1956
|
#store;
|
|
1957
|
+
#sandboxCacheRoot = path.join(tmpdir(), `kubb-studio-${randomUUID()}`);
|
|
1889
1958
|
#diffs = /* @__PURE__ */ new Map();
|
|
1890
1959
|
constructor({ options, session }) {
|
|
1891
1960
|
super();
|
|
@@ -1907,13 +1976,19 @@ var Agent = class extends RpcTarget {
|
|
|
1907
1976
|
}
|
|
1908
1977
|
get #generations() {
|
|
1909
1978
|
this.#store ??= createGenerationStore({
|
|
1910
|
-
storage: this.#sandbox ?
|
|
1979
|
+
storage: cacheStorage({ root: this.#sandbox ? this.#sandboxCacheRoot : this.#options.root }),
|
|
1911
1980
|
maxCount: this.#limits.maxCount,
|
|
1912
1981
|
maxMb: this.#limits.maxMb,
|
|
1913
1982
|
ttlMs: this.#sandbox ? SANDBOX_GENERATION_TTL_MS : void 0
|
|
1914
1983
|
});
|
|
1915
1984
|
return this.#store;
|
|
1916
1985
|
}
|
|
1986
|
+
async clear() {
|
|
1987
|
+
if (this.#sandbox) await rm(resolveCacheDir(this.#sandboxCacheRoot), {
|
|
1988
|
+
recursive: true,
|
|
1989
|
+
force: true
|
|
1990
|
+
});
|
|
1991
|
+
}
|
|
1917
1992
|
#warn({ message, permission }) {
|
|
1918
1993
|
return emit(this.#options.hooks, "studio:warn", {
|
|
1919
1994
|
message,
|
|
@@ -1938,10 +2013,12 @@ var Agent = class extends RpcTarget {
|
|
|
1938
2013
|
message: "Studio must call connect() before any other command"
|
|
1939
2014
|
});
|
|
1940
2015
|
}
|
|
1941
|
-
#logCommand(command, info) {
|
|
2016
|
+
#logCommand(command, info, requestedBy, failed) {
|
|
1942
2017
|
return emit(this.#options.hooks, "studio:command", {
|
|
1943
2018
|
command,
|
|
1944
|
-
info
|
|
2019
|
+
info,
|
|
2020
|
+
requestedBy,
|
|
2021
|
+
failed
|
|
1945
2022
|
});
|
|
1946
2023
|
}
|
|
1947
2024
|
async #readConfigFileView(source) {
|
|
@@ -2025,7 +2102,12 @@ var Agent = class extends RpcTarget {
|
|
|
2025
2102
|
maxFiles: DISK_SNAPSHOT_MAX_FILES
|
|
2026
2103
|
});
|
|
2027
2104
|
if (!files) return void 0;
|
|
2028
|
-
|
|
2105
|
+
const { jobId, output } = await this.#generations.keepInitial(files);
|
|
2106
|
+
return {
|
|
2107
|
+
jobId,
|
|
2108
|
+
files: output.paths,
|
|
2109
|
+
hashes: output.hashes
|
|
2110
|
+
};
|
|
2029
2111
|
}
|
|
2030
2112
|
startGeneration(input) {
|
|
2031
2113
|
const stream = createGenerationStream(this.#options.hooks, input.jobId);
|
|
@@ -2044,7 +2126,9 @@ var Agent = class extends RpcTarget {
|
|
|
2044
2126
|
}
|
|
2045
2127
|
});
|
|
2046
2128
|
}
|
|
2047
|
-
/**
|
|
2129
|
+
/**
|
|
2130
|
+
* Keeps a job's output and permitted parsed input, then cleans up its temporary generation directories.
|
|
2131
|
+
*/
|
|
2048
2132
|
async #runGeneration(input, controller) {
|
|
2049
2133
|
this.#assertConnected();
|
|
2050
2134
|
if (this.#session.closed) throw new Diagnostics.Error({
|
|
@@ -2060,11 +2144,14 @@ var Agent = class extends RpcTarget {
|
|
|
2060
2144
|
settled: settled.promise
|
|
2061
2145
|
};
|
|
2062
2146
|
let root = this.#options.root;
|
|
2147
|
+
let outputRoot;
|
|
2063
2148
|
try {
|
|
2064
2149
|
if (this.#sandbox) root = await mkdtemp(path.join(tmpdir(), "kubb-job-"));
|
|
2150
|
+
if (!this.#sandbox && !this.#permissions.allowWrite) outputRoot = await mkdtemp(path.join(tmpdir(), "kubb-output-"));
|
|
2065
2151
|
return await this.#generate({
|
|
2066
2152
|
input,
|
|
2067
2153
|
root,
|
|
2154
|
+
outputRoot,
|
|
2068
2155
|
signal: controller.signal
|
|
2069
2156
|
});
|
|
2070
2157
|
} finally {
|
|
@@ -2075,12 +2162,18 @@ var Agent = class extends RpcTarget {
|
|
|
2075
2162
|
recursive: true,
|
|
2076
2163
|
force: true
|
|
2077
2164
|
})]).catch(() => {});
|
|
2165
|
+
if (outputRoot) await rm(outputRoot, {
|
|
2166
|
+
recursive: true,
|
|
2167
|
+
force: true
|
|
2168
|
+
});
|
|
2078
2169
|
this.#session.active = void 0;
|
|
2079
2170
|
settled.resolve();
|
|
2080
2171
|
}
|
|
2081
2172
|
}
|
|
2082
|
-
/**
|
|
2083
|
-
|
|
2173
|
+
/**
|
|
2174
|
+
* Merges Studio's config over the project config and confines generation to the configured output folder.
|
|
2175
|
+
*/
|
|
2176
|
+
async #generate({ input, root, outputRoot, signal }) {
|
|
2084
2177
|
const { jobId, config: patch } = input;
|
|
2085
2178
|
const studioInput = this.#sandbox ? patch?.input : void 0;
|
|
2086
2179
|
if (this.#sandbox && (typeof studioInput !== "string" || !studioInput.trim() || getInputKind(studioInput) !== "inline")) return this.#refuse({ message: AgentMessage.SANDBOX_INPUT_ONLY });
|
|
@@ -2111,37 +2204,29 @@ var Agent = class extends RpcTarget {
|
|
|
2111
2204
|
});
|
|
2112
2205
|
const activePlugins = plugins ?? config.plugins;
|
|
2113
2206
|
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
2207
|
const files = await generate({
|
|
2126
2208
|
config: {
|
|
2127
2209
|
...config,
|
|
2128
2210
|
root,
|
|
2129
2211
|
input: studioInput ?? config.input,
|
|
2130
|
-
storage:
|
|
2212
|
+
storage: outputRoot ? stagingStorage({
|
|
2213
|
+
root,
|
|
2214
|
+
directory: outputRoot
|
|
2215
|
+
}) : fsStorage(),
|
|
2131
2216
|
plugins: activePlugins,
|
|
2132
2217
|
adapter,
|
|
2133
2218
|
parsers
|
|
2134
2219
|
},
|
|
2135
2220
|
hooks: this.#options.hooks,
|
|
2136
2221
|
signal,
|
|
2137
|
-
allowExec: this.#permissions.allowExec
|
|
2222
|
+
allowExec: this.#permissions.allowWrite && this.#permissions.allowExec
|
|
2138
2223
|
}).catch(async (error) => {
|
|
2139
2224
|
await this.#generations.drop(jobId);
|
|
2225
|
+
await this.#logCommand("generate", getErrorMessage(error), input.requestedBy, true);
|
|
2140
2226
|
throw error;
|
|
2141
2227
|
});
|
|
2142
2228
|
const output = await this.#generations.keep({
|
|
2143
2229
|
jobId,
|
|
2144
|
-
source: FileSource.OUTPUT,
|
|
2145
2230
|
files,
|
|
2146
2231
|
maxSetMb: this.#limits.maxMb
|
|
2147
2232
|
});
|
|
@@ -2154,16 +2239,14 @@ var Agent = class extends RpcTarget {
|
|
|
2154
2239
|
await this.#generations.add({
|
|
2155
2240
|
jobId,
|
|
2156
2241
|
output,
|
|
2157
|
-
disk,
|
|
2158
2242
|
inputBytes
|
|
2159
2243
|
});
|
|
2160
|
-
await this.#logCommand("generate", `${activePlugins.length} plugin${activePlugins.length === 1 ? "" : "s"}, ${this.#permissions.allowWrite ? "written to disk" : "
|
|
2244
|
+
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);
|
|
2161
2245
|
return {
|
|
2162
2246
|
status: "success",
|
|
2163
2247
|
files: [...files.paths],
|
|
2164
2248
|
fileCount: files.paths.size,
|
|
2165
|
-
hashes: output.hashes
|
|
2166
|
-
disk: disk ? { hashes: disk.hashes } : void 0
|
|
2249
|
+
hashes: output.hashes
|
|
2167
2250
|
};
|
|
2168
2251
|
}
|
|
2169
2252
|
async saveConfig(input) {
|
|
@@ -2197,7 +2280,7 @@ var Agent = class extends RpcTarget {
|
|
|
2197
2280
|
extensions: await this.#installedExtensions()
|
|
2198
2281
|
});
|
|
2199
2282
|
const applied = outcomes.filter((outcome) => outcome.applied).length;
|
|
2200
|
-
await this.#logCommand("saveConfig", `applied ${applied}/${outcomes.length} edits to ${configPath}
|
|
2283
|
+
await this.#logCommand("saveConfig", `applied ${applied}/${outcomes.length} edits to ${configPath}`, input.requestedBy);
|
|
2201
2284
|
return {
|
|
2202
2285
|
outcomes,
|
|
2203
2286
|
changed
|
|
@@ -2233,35 +2316,14 @@ var Agent = class extends RpcTarget {
|
|
|
2233
2316
|
message: AgentMessage.READ_NOT_ALLOWED,
|
|
2234
2317
|
permission: AgentPermission.READ
|
|
2235
2318
|
});
|
|
2236
|
-
if (!Array.isArray(input.paths) || typeof input.jobId !== "string" || !input.jobId
|
|
2319
|
+
if (!Array.isArray(input.paths) || typeof input.jobId !== "string" || !input.jobId) return this.#refuse({ message: AgentMessage.FILE_REQUEST_INVALID });
|
|
2237
2320
|
const { paths } = input;
|
|
2238
2321
|
if (paths.length > 50) return this.#refuse({ message: AgentMessage.TOO_MANY_FILES });
|
|
2239
2322
|
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
2323
|
const generation = await this.#generations.get(input.jobId);
|
|
2259
2324
|
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
2325
|
const result = await this.#generations.read({
|
|
2263
2326
|
generation,
|
|
2264
|
-
source,
|
|
2265
2327
|
paths,
|
|
2266
2328
|
cursor: input.cursor,
|
|
2267
2329
|
limit: input.limit
|
|
@@ -2286,15 +2348,11 @@ var Agent = class extends RpcTarget {
|
|
|
2286
2348
|
})) return this.#refuse({ message: AgentMessage.INVALID_PAGE });
|
|
2287
2349
|
const [generation, baseGeneration] = await Promise.all([this.#generations.get(jobId), this.#generations.get(base.jobId)]);
|
|
2288
2350
|
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
2351
|
const [after, before] = await Promise.all([this.#generations.read({
|
|
2292
2352
|
generation,
|
|
2293
|
-
source: FileSource.OUTPUT,
|
|
2294
2353
|
paths: [path]
|
|
2295
2354
|
}), this.#generations.read({
|
|
2296
2355
|
generation: baseGeneration,
|
|
2297
|
-
source: baseSource,
|
|
2298
2356
|
paths: [path]
|
|
2299
2357
|
})]);
|
|
2300
2358
|
const afterContent = after.files[path];
|
|
@@ -2315,7 +2373,6 @@ var Agent = class extends RpcTarget {
|
|
|
2315
2373
|
const key = JSON.stringify([
|
|
2316
2374
|
jobId,
|
|
2317
2375
|
base.jobId,
|
|
2318
|
-
baseSource,
|
|
2319
2376
|
path
|
|
2320
2377
|
]);
|
|
2321
2378
|
let diff = this.#diffs.get(key);
|
|
@@ -2372,14 +2429,15 @@ async function runSession({ options, openRpc, url, instanceId }) {
|
|
|
2372
2429
|
connected: Promise.withResolvers(),
|
|
2373
2430
|
closed: false
|
|
2374
2431
|
};
|
|
2432
|
+
const agent = new Agent({
|
|
2433
|
+
options,
|
|
2434
|
+
session
|
|
2435
|
+
});
|
|
2375
2436
|
const rpc = openRpc({
|
|
2376
2437
|
url,
|
|
2377
2438
|
token,
|
|
2378
2439
|
instanceId,
|
|
2379
|
-
agent
|
|
2380
|
-
options,
|
|
2381
|
-
session
|
|
2382
|
-
})
|
|
2440
|
+
agent
|
|
2383
2441
|
});
|
|
2384
2442
|
signal?.addEventListener("abort", rpc.close, { once: true });
|
|
2385
2443
|
let timer;
|
|
@@ -2414,6 +2472,7 @@ async function runSession({ options, openRpc, url, instanceId }) {
|
|
|
2414
2472
|
await emit(hooks, "studio:warn", { message: "Canceled the running generation: the connection to Kubb Studio closed" });
|
|
2415
2473
|
}
|
|
2416
2474
|
await active?.settled;
|
|
2475
|
+
await agent.clear();
|
|
2417
2476
|
return {
|
|
2418
2477
|
...close,
|
|
2419
2478
|
connected: session.info !== void 0
|
|
@@ -2721,10 +2780,9 @@ async function createSnapshotPackage({ files, packageInfo }) {
|
|
|
2721
2780
|
const root = await mkdtemp(join(tmpdir(), "kubb-snapshot-"));
|
|
2722
2781
|
const dist = join(root, "dist");
|
|
2723
2782
|
try {
|
|
2724
|
-
await mkdir(dist);
|
|
2783
|
+
await mkdir$1(dist);
|
|
2725
2784
|
await Promise.all([...sources].map(async ([path, { content }]) => {
|
|
2726
|
-
await
|
|
2727
|
-
await writeFile(join(root, path), content);
|
|
2785
|
+
await writeRaw(join(root, path), content);
|
|
2728
2786
|
}));
|
|
2729
2787
|
const entry = [...sources.keys()].filter((path) => /\.(?:[cm]?[jt]sx?)$/.test(path)).map((path) => join(root, path));
|
|
2730
2788
|
if (entry.length) {
|
|
@@ -2764,7 +2822,7 @@ async function createSnapshotPackage({ files, packageInfo }) {
|
|
|
2764
2822
|
const chunks = [...new Map([
|
|
2765
2823
|
["package.json", JSON.stringify(manifest, null, 2)],
|
|
2766
2824
|
...[...sources].map(([path, { content }]) => [path, content]),
|
|
2767
|
-
...await Promise.all(built.map(async (key) => [`dist/${key}`, await
|
|
2825
|
+
...await Promise.all(built.map(async (key) => [`dist/${key}`, await read(join(dist, key))]))
|
|
2768
2826
|
])].flatMap(([path, content]) => {
|
|
2769
2827
|
const bytes = Buffer.from(content);
|
|
2770
2828
|
return [
|
|
@@ -2795,7 +2853,10 @@ async function createSnapshot({ studioUrl = defaultStudioUrl, apiKey, config, ho
|
|
|
2795
2853
|
path: "/api/snapshots"
|
|
2796
2854
|
});
|
|
2797
2855
|
const generated = await generate({
|
|
2798
|
-
config
|
|
2856
|
+
config: {
|
|
2857
|
+
...config,
|
|
2858
|
+
storage: fsStorage()
|
|
2859
|
+
},
|
|
2799
2860
|
hooks,
|
|
2800
2861
|
signal,
|
|
2801
2862
|
allowExec: true
|
|
@@ -2874,6 +2935,6 @@ async function createSnapshot({ studioUrl = defaultStudioUrl, apiKey, config, ho
|
|
|
2874
2935
|
return _data.snapshot;
|
|
2875
2936
|
}
|
|
2876
2937
|
//#endregion
|
|
2877
|
-
export { IncompatibleAgentError, InvalidAgentTokenError, PairingCanceledError, PairingDeniedError, PairingExpiredError, connectAgent, createSnapshot, defaultStudioUrl, pairAgent, writeConfigEdits };
|
|
2938
|
+
export { IncompatibleAgentError, InvalidAgentTokenError, PairingCanceledError, PairingDeniedError, PairingExpiredError, applyConfigEdits, connectAgent, createSnapshot, defaultStudioUrl, pairAgent, writeConfigEdits };
|
|
2878
2939
|
|
|
2879
2940
|
//# sourceMappingURL=index.js.map
|