@kubb/studio 5.11.2 → 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/README.md +3 -3
- package/dist/index.cjs +147 -88
- 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 +150 -91
- package/dist/index.js.map +1 -1
- package/dist/{protocol-BUZ91Gd4.d.ts → protocol-a_DcTFba.d.cts} +14 -39
- package/dist/{protocol-BUZ91Gd4.d.cts → protocol-a_DcTFba.d.ts} +14 -39
- package/dist/protocol.cjs +8 -9
- 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 +9 -9
- package/dist/protocol.js.map +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -85,9 +85,9 @@ running in a developer's project and a container running a fixed plugin set.
|
|
|
85
85
|
the agent is a sandbox and carries its slug and Studio's version. From then on the agent pings every
|
|
86
86
|
30 seconds, and Studio schedules one job at a time on the socket.
|
|
87
87
|
|
|
88
|
-
With `allowRead`, the connect payload
|
|
89
|
-
|
|
90
|
-
|
|
88
|
+
With `allowRead`, the connect payload includes the current `output.path` file list. Studio reads
|
|
89
|
+
file contents from disk through `readFiles` when a user opens a file. Sandbox agents have no disk
|
|
90
|
+
output, and output paths outside the project root are excluded.
|
|
91
91
|
|
|
92
92
|
Studio ends a socket on purpose with one of these close codes:
|
|
93
93
|
|
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 }) {
|
|
@@ -1476,8 +1496,8 @@ async function listDisk({ root, outputPath, maxFiles }) {
|
|
|
1476
1496
|
/**
|
|
1477
1497
|
* Keeps recent generations by job id in `storage`, with an index next to them so a store on disk
|
|
1478
1498
|
* survives a restart. A generation is only ever looked up by its job id, so on a pooled sandbox one
|
|
1479
|
-
* tenant never reaches another's output.
|
|
1480
|
-
* 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.
|
|
1481
1501
|
*/
|
|
1482
1502
|
function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now }) {
|
|
1483
1503
|
let index;
|
|
@@ -1496,7 +1516,7 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
|
|
|
1496
1516
|
/**
|
|
1497
1517
|
* Copies `files` into the store as one set of `jobId`. Above `maxSetMb` only the hashes are kept.
|
|
1498
1518
|
*/
|
|
1499
|
-
async function keep({ jobId,
|
|
1519
|
+
async function keep({ jobId, files, maxSetMb }) {
|
|
1500
1520
|
const maxSetBytes = maxSetMb * MB;
|
|
1501
1521
|
const hashes = {};
|
|
1502
1522
|
let bytes = 0;
|
|
@@ -1507,14 +1527,13 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
|
|
|
1507
1527
|
if (path.split("/").includes("..")) return;
|
|
1508
1528
|
const content = await files.storage.readItem((0, node_path.resolve)(files.root, path));
|
|
1509
1529
|
if (content === null) return;
|
|
1510
|
-
|
|
1511
|
-
hashes[path] = hashOf(normalize ? `${content.trim()}\n` : content);
|
|
1530
|
+
hashes[path] = hashOf(content);
|
|
1512
1531
|
bytes += Buffer.byteLength(content);
|
|
1513
|
-
if (bytes <= maxSetBytes) await storage.writeItem(`${dirOf(jobId)}
|
|
1532
|
+
if (bytes <= maxSetBytes) await storage.writeItem(`${dirOf(jobId)}output/${path}`, content);
|
|
1514
1533
|
}
|
|
1515
1534
|
});
|
|
1516
1535
|
if (bytes > maxSetBytes) {
|
|
1517
|
-
await storage.empty(`${dirOf(jobId)}
|
|
1536
|
+
await storage.empty(`${dirOf(jobId)}output/`);
|
|
1518
1537
|
return {
|
|
1519
1538
|
paths: [],
|
|
1520
1539
|
hashes,
|
|
@@ -1530,10 +1549,50 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
|
|
|
1530
1549
|
async function drop(jobId) {
|
|
1531
1550
|
await storage.empty(dirOf(jobId));
|
|
1532
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
|
+
}
|
|
1533
1566
|
return {
|
|
1534
1567
|
keep,
|
|
1535
1568
|
drop,
|
|
1536
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
|
+
/**
|
|
1537
1596
|
* Keeps the `InputNode` of `jobId` and returns its size in bytes, or 0 when it keeps none. A ref schema loses
|
|
1538
1597
|
* the copy of its target, which `input.schemas` holds: with it, a large spec outgrows what `JSON.stringify` can return.
|
|
1539
1598
|
*/
|
|
@@ -1561,30 +1620,19 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
|
|
|
1561
1620
|
return stored === null ? void 0 : JSON.parse(stored);
|
|
1562
1621
|
},
|
|
1563
1622
|
get: async (jobId) => (await load()).find((generation) => generation.jobId === jobId && isLive(generation)),
|
|
1564
|
-
|
|
1565
|
-
const current = await load();
|
|
1566
|
-
for (const expired of current.filter((entry) => !isLive(entry))) await drop(expired.jobId);
|
|
1567
|
-
const entries = current.filter((entry) => entry.jobId !== generation.jobId && isLive(entry));
|
|
1568
|
-
entries.push({
|
|
1569
|
-
...generation,
|
|
1570
|
-
keptAt: now()
|
|
1571
|
-
});
|
|
1572
|
-
while (entries.length > 1 && (entries.length > maxCount || totalBytes(entries) > maxMb * MB)) await drop(entries.shift().jobId);
|
|
1573
|
-
index = entries;
|
|
1574
|
-
await storage.writeItem(INDEX_KEY, JSON.stringify(entries));
|
|
1575
|
-
},
|
|
1623
|
+
add,
|
|
1576
1624
|
/**
|
|
1577
1625
|
* Reads the requested paths the set holds, skipping any it does not, one page of lines when `limit` is given.
|
|
1578
1626
|
*/
|
|
1579
|
-
async read({ generation,
|
|
1580
|
-
const kept = new Set(generation
|
|
1627
|
+
async read({ generation, paths, cursor = 0, limit }) {
|
|
1628
|
+
const kept = new Set(generation.output.paths);
|
|
1581
1629
|
const files = {};
|
|
1582
1630
|
const pages = {};
|
|
1583
1631
|
await inParallel({
|
|
1584
1632
|
items: paths.filter((path) => kept.has(path)),
|
|
1585
1633
|
limit: READ_CONCURRENCY,
|
|
1586
1634
|
run: async (path) => {
|
|
1587
|
-
const content = await storage.readItem(`${dirOf(generation.jobId)}
|
|
1635
|
+
const content = await storage.readItem(`${dirOf(generation.jobId)}output/${path}`);
|
|
1588
1636
|
if (content === null) return;
|
|
1589
1637
|
if (limit === void 0) {
|
|
1590
1638
|
files[path] = content;
|
|
@@ -1859,6 +1907,25 @@ function isValidPage({ cursor, limit }) {
|
|
|
1859
1907
|
if (cursor !== void 0 && (!Number.isSafeInteger(cursor) || cursor < 0)) return false;
|
|
1860
1908
|
return Number.isSafeInteger(limit) && limit >= 1 && limit <= 2e3;
|
|
1861
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
|
+
}
|
|
1862
1929
|
var GenerationRunTarget = class extends capnweb.RpcTarget {
|
|
1863
1930
|
#run;
|
|
1864
1931
|
constructor(run) {
|
|
@@ -1883,6 +1950,7 @@ var Agent = class extends capnweb.RpcTarget {
|
|
|
1883
1950
|
#session;
|
|
1884
1951
|
#limits = resolveGenerationLimits();
|
|
1885
1952
|
#store;
|
|
1953
|
+
#sandboxCacheRoot = node_path.default.join((0, node_os.tmpdir)(), `kubb-studio-${(0, node_crypto.randomUUID)()}`);
|
|
1886
1954
|
#diffs = /* @__PURE__ */ new Map();
|
|
1887
1955
|
constructor({ options, session }) {
|
|
1888
1956
|
super();
|
|
@@ -1904,13 +1972,19 @@ var Agent = class extends capnweb.RpcTarget {
|
|
|
1904
1972
|
}
|
|
1905
1973
|
get #generations() {
|
|
1906
1974
|
this.#store ??= createGenerationStore({
|
|
1907
|
-
storage:
|
|
1975
|
+
storage: (0, _kubb_core.cacheStorage)({ root: this.#sandbox ? this.#sandboxCacheRoot : this.#options.root }),
|
|
1908
1976
|
maxCount: this.#limits.maxCount,
|
|
1909
1977
|
maxMb: this.#limits.maxMb,
|
|
1910
1978
|
ttlMs: this.#sandbox ? SANDBOX_GENERATION_TTL_MS : void 0
|
|
1911
1979
|
});
|
|
1912
1980
|
return this.#store;
|
|
1913
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
|
+
}
|
|
1914
1988
|
#warn({ message, permission }) {
|
|
1915
1989
|
return emit(this.#options.hooks, "studio:warn", {
|
|
1916
1990
|
message,
|
|
@@ -2022,18 +2096,7 @@ var Agent = class extends capnweb.RpcTarget {
|
|
|
2022
2096
|
maxFiles: DISK_SNAPSHOT_MAX_FILES
|
|
2023
2097
|
});
|
|
2024
2098
|
if (!files) return void 0;
|
|
2025
|
-
const jobId =
|
|
2026
|
-
const output = await this.#generations.keep({
|
|
2027
|
-
jobId,
|
|
2028
|
-
source: require_protocol.FileSource.OUTPUT,
|
|
2029
|
-
files,
|
|
2030
|
-
maxSetMb: this.#limits.maxSnapshotMb,
|
|
2031
|
-
normalize: false
|
|
2032
|
-
});
|
|
2033
|
-
await this.#generations.add({
|
|
2034
|
-
jobId,
|
|
2035
|
-
output
|
|
2036
|
-
});
|
|
2099
|
+
const { jobId, output } = await this.#generations.keepInitial(files);
|
|
2037
2100
|
return {
|
|
2038
2101
|
jobId,
|
|
2039
2102
|
files: output.paths,
|
|
@@ -2057,7 +2120,9 @@ var Agent = class extends capnweb.RpcTarget {
|
|
|
2057
2120
|
}
|
|
2058
2121
|
});
|
|
2059
2122
|
}
|
|
2060
|
-
/**
|
|
2123
|
+
/**
|
|
2124
|
+
* Keeps a job's output and permitted parsed input, then cleans up its temporary generation directories.
|
|
2125
|
+
*/
|
|
2061
2126
|
async #runGeneration(input, controller) {
|
|
2062
2127
|
this.#assertConnected();
|
|
2063
2128
|
if (this.#session.closed) throw new _kubb_core.Diagnostics.Error({
|
|
@@ -2065,7 +2130,7 @@ var Agent = class extends capnweb.RpcTarget {
|
|
|
2065
2130
|
severity: "error",
|
|
2066
2131
|
message: "Connection is closed"
|
|
2067
2132
|
});
|
|
2068
|
-
if (this.#session.active) return this.#refuse({ message:
|
|
2133
|
+
if (this.#session.active) return this.#refuse({ message: require_protocol.AgentMessage.GENERATION_IN_PROGRESS });
|
|
2069
2134
|
const settled = Promise.withResolvers();
|
|
2070
2135
|
this.#session.active = {
|
|
2071
2136
|
jobId: input.jobId,
|
|
@@ -2073,11 +2138,14 @@ var Agent = class extends capnweb.RpcTarget {
|
|
|
2073
2138
|
settled: settled.promise
|
|
2074
2139
|
};
|
|
2075
2140
|
let root = this.#options.root;
|
|
2141
|
+
let outputRoot;
|
|
2076
2142
|
try {
|
|
2077
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-"));
|
|
2078
2145
|
return await this.#generate({
|
|
2079
2146
|
input,
|
|
2080
2147
|
root,
|
|
2148
|
+
outputRoot,
|
|
2081
2149
|
signal: controller.signal
|
|
2082
2150
|
});
|
|
2083
2151
|
} finally {
|
|
@@ -2088,15 +2156,21 @@ var Agent = class extends capnweb.RpcTarget {
|
|
|
2088
2156
|
recursive: true,
|
|
2089
2157
|
force: true
|
|
2090
2158
|
})]).catch(() => {});
|
|
2159
|
+
if (outputRoot) await (0, node_fs_promises.rm)(outputRoot, {
|
|
2160
|
+
recursive: true,
|
|
2161
|
+
force: true
|
|
2162
|
+
});
|
|
2091
2163
|
this.#session.active = void 0;
|
|
2092
2164
|
settled.resolve();
|
|
2093
2165
|
}
|
|
2094
2166
|
}
|
|
2095
|
-
/**
|
|
2096
|
-
|
|
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 }) {
|
|
2097
2171
|
const { jobId, config: patch } = input;
|
|
2098
2172
|
const studioInput = this.#sandbox ? patch?.input : void 0;
|
|
2099
|
-
if (this.#sandbox && (typeof studioInput !== "string" || !studioInput.trim() || (0, _kubb_core.getInputKind)(studioInput) !== "inline")) return this.#refuse({ message:
|
|
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 });
|
|
2100
2174
|
const [config, extensions] = await Promise.all([this.#options.loadConfig(), this.#installedExtensions()]);
|
|
2101
2175
|
const [plugins, adapter, parsers] = await Promise.all([
|
|
2102
2176
|
mergePlugins({
|
|
@@ -2124,37 +2198,28 @@ var Agent = class extends capnweb.RpcTarget {
|
|
|
2124
2198
|
});
|
|
2125
2199
|
const activePlugins = plugins ?? config.plugins;
|
|
2126
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" });
|
|
2127
|
-
const diskFiles = !this.#sandbox && this.#permissions.allowRead ? await listDisk({
|
|
2128
|
-
root,
|
|
2129
|
-
outputPath: config.output.path,
|
|
2130
|
-
maxFiles: DISK_SNAPSHOT_MAX_FILES
|
|
2131
|
-
}) : void 0;
|
|
2132
|
-
const disk = diskFiles ? await this.#generations.keep({
|
|
2133
|
-
jobId,
|
|
2134
|
-
source: require_protocol.FileSource.DISK,
|
|
2135
|
-
files: diskFiles,
|
|
2136
|
-
maxSetMb: this.#limits.maxSnapshotMb
|
|
2137
|
-
}) : void 0;
|
|
2138
2201
|
const files = await generate({
|
|
2139
2202
|
config: {
|
|
2140
2203
|
...config,
|
|
2141
2204
|
root,
|
|
2142
2205
|
input: studioInput ?? config.input,
|
|
2143
|
-
storage:
|
|
2206
|
+
storage: outputRoot ? stagingStorage({
|
|
2207
|
+
root,
|
|
2208
|
+
directory: outputRoot
|
|
2209
|
+
}) : (0, _kubb_core.fsStorage)(),
|
|
2144
2210
|
plugins: activePlugins,
|
|
2145
2211
|
adapter,
|
|
2146
2212
|
parsers
|
|
2147
2213
|
},
|
|
2148
2214
|
hooks: this.#options.hooks,
|
|
2149
2215
|
signal,
|
|
2150
|
-
allowExec: this.#permissions.allowExec
|
|
2216
|
+
allowExec: this.#permissions.allowWrite && this.#permissions.allowExec
|
|
2151
2217
|
}).catch(async (error) => {
|
|
2152
2218
|
await this.#generations.drop(jobId);
|
|
2153
2219
|
throw error;
|
|
2154
2220
|
});
|
|
2155
2221
|
const output = await this.#generations.keep({
|
|
2156
2222
|
jobId,
|
|
2157
|
-
source: require_protocol.FileSource.OUTPUT,
|
|
2158
2223
|
files,
|
|
2159
2224
|
maxSetMb: this.#limits.maxMb
|
|
2160
2225
|
});
|
|
@@ -2167,16 +2232,14 @@ var Agent = class extends capnweb.RpcTarget {
|
|
|
2167
2232
|
await this.#generations.add({
|
|
2168
2233
|
jobId,
|
|
2169
2234
|
output,
|
|
2170
|
-
disk,
|
|
2171
2235
|
inputBytes
|
|
2172
2236
|
});
|
|
2173
|
-
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" : ""}`);
|
|
2174
2238
|
return {
|
|
2175
2239
|
status: "success",
|
|
2176
2240
|
files: [...files.paths],
|
|
2177
2241
|
fileCount: files.paths.size,
|
|
2178
|
-
hashes: output.hashes
|
|
2179
|
-
disk: disk ? { hashes: disk.hashes } : void 0
|
|
2242
|
+
hashes: output.hashes
|
|
2180
2243
|
};
|
|
2181
2244
|
}
|
|
2182
2245
|
async saveConfig(input) {
|
|
@@ -2229,7 +2292,7 @@ var Agent = class extends capnweb.RpcTarget {
|
|
|
2229
2292
|
message: require_protocol.AgentMessage.READ_AST_NOT_ALLOWED,
|
|
2230
2293
|
permission: require_protocol.AgentPermission.READ_AST
|
|
2231
2294
|
});
|
|
2232
|
-
if (typeof input.jobId !== "string" || !input.jobId) return this.#refuse({ message:
|
|
2295
|
+
if (typeof input.jobId !== "string" || !input.jobId) return this.#refuse({ message: require_protocol.AgentMessage.JOB_REQUIRED });
|
|
2233
2296
|
const generation = await this.#generations.get(input.jobId);
|
|
2234
2297
|
if (!generation) return this.#refuse({ message: require_protocol.AgentMessage.GENERATION_GONE });
|
|
2235
2298
|
const kept = generation.inputBytes ? await this.#generations.readInput(input.jobId) : void 0;
|
|
@@ -2246,17 +2309,14 @@ var Agent = class extends capnweb.RpcTarget {
|
|
|
2246
2309
|
message: require_protocol.AgentMessage.READ_NOT_ALLOWED,
|
|
2247
2310
|
permission: require_protocol.AgentPermission.READ
|
|
2248
2311
|
});
|
|
2249
|
-
if (!Array.isArray(input.paths) || typeof input.jobId !== "string" || !input.jobId) return this.#refuse({ message:
|
|
2312
|
+
if (!Array.isArray(input.paths) || typeof input.jobId !== "string" || !input.jobId) return this.#refuse({ message: require_protocol.AgentMessage.FILE_REQUEST_INVALID });
|
|
2250
2313
|
const { paths } = input;
|
|
2251
|
-
if (paths.length > 50) return this.#refuse({ message:
|
|
2314
|
+
if (paths.length > 50) return this.#refuse({ message: require_protocol.AgentMessage.TOO_MANY_FILES });
|
|
2315
|
+
if (!isValidPage(input)) return this.#refuse({ message: require_protocol.AgentMessage.INVALID_PAGE });
|
|
2252
2316
|
const generation = await this.#generations.get(input.jobId);
|
|
2253
2317
|
if (!generation) return this.#refuse({ message: require_protocol.AgentMessage.GENERATION_GONE });
|
|
2254
|
-
if (!isValidPage(input)) return this.#refuse({ message: `The cursor must be a whole line from 0 and come with a limit of 1 to ${require_protocol.MAX_PAGE_LINES} lines` });
|
|
2255
|
-
const source = input.source === require_protocol.FileSource.DISK ? require_protocol.FileSource.DISK : require_protocol.FileSource.OUTPUT;
|
|
2256
|
-
if (!generation[source]) return this.#refuse({ message: "This agent kept no snapshot of the files on disk for that job" });
|
|
2257
2318
|
const result = await this.#generations.read({
|
|
2258
2319
|
generation,
|
|
2259
|
-
source,
|
|
2260
2320
|
paths,
|
|
2261
2321
|
cursor: input.cursor,
|
|
2262
2322
|
limit: input.limit
|
|
@@ -2274,26 +2334,22 @@ var Agent = class extends capnweb.RpcTarget {
|
|
|
2274
2334
|
permission: require_protocol.AgentPermission.READ
|
|
2275
2335
|
});
|
|
2276
2336
|
const { jobId, path, base, cursor = 0, limit } = input;
|
|
2277
|
-
if (typeof jobId !== "string" || !jobId || typeof path !== "string" || !path || typeof base?.jobId !== "string" || !base.jobId) return this.#refuse({ message:
|
|
2337
|
+
if (typeof jobId !== "string" || !jobId || typeof path !== "string" || !path || typeof base?.jobId !== "string" || !base.jobId) return this.#refuse({ message: require_protocol.AgentMessage.DIFF_REQUEST_INVALID });
|
|
2278
2338
|
if (!isValidPage({
|
|
2279
2339
|
cursor,
|
|
2280
2340
|
limit
|
|
2281
|
-
})) return this.#refuse({ message:
|
|
2341
|
+
})) return this.#refuse({ message: require_protocol.AgentMessage.INVALID_PAGE });
|
|
2282
2342
|
const [generation, baseGeneration] = await Promise.all([this.#generations.get(jobId), this.#generations.get(base.jobId)]);
|
|
2283
2343
|
if (!generation || !baseGeneration) return this.#refuse({ message: require_protocol.AgentMessage.GENERATION_GONE });
|
|
2284
|
-
const baseSource = base.source === require_protocol.FileSource.DISK ? require_protocol.FileSource.DISK : require_protocol.FileSource.OUTPUT;
|
|
2285
|
-
if (!baseGeneration[baseSource]) return this.#refuse({ message: "This agent kept no snapshot of the files on disk for that job" });
|
|
2286
2344
|
const [after, before] = await Promise.all([this.#generations.read({
|
|
2287
2345
|
generation,
|
|
2288
|
-
source: require_protocol.FileSource.OUTPUT,
|
|
2289
2346
|
paths: [path]
|
|
2290
2347
|
}), this.#generations.read({
|
|
2291
2348
|
generation: baseGeneration,
|
|
2292
|
-
source: baseSource,
|
|
2293
2349
|
paths: [path]
|
|
2294
2350
|
})]);
|
|
2295
2351
|
const afterContent = after.files[path];
|
|
2296
|
-
if (afterContent === void 0 && path in generation.output.hashes) return this.#refuse({ message:
|
|
2352
|
+
if (afterContent === void 0 && path in generation.output.hashes) return this.#refuse({ message: require_protocol.AgentMessage.HASHES_ONLY });
|
|
2297
2353
|
const beforeContent = before.files[path];
|
|
2298
2354
|
if (beforeContent === void 0) return {
|
|
2299
2355
|
hunks: [],
|
|
@@ -2310,7 +2366,6 @@ var Agent = class extends capnweb.RpcTarget {
|
|
|
2310
2366
|
const key = JSON.stringify([
|
|
2311
2367
|
jobId,
|
|
2312
2368
|
base.jobId,
|
|
2313
|
-
baseSource,
|
|
2314
2369
|
path
|
|
2315
2370
|
]);
|
|
2316
2371
|
let diff = this.#diffs.get(key);
|
|
@@ -2367,14 +2422,15 @@ async function runSession({ options, openRpc, url, instanceId }) {
|
|
|
2367
2422
|
connected: Promise.withResolvers(),
|
|
2368
2423
|
closed: false
|
|
2369
2424
|
};
|
|
2425
|
+
const agent = new Agent({
|
|
2426
|
+
options,
|
|
2427
|
+
session
|
|
2428
|
+
});
|
|
2370
2429
|
const rpc = openRpc({
|
|
2371
2430
|
url,
|
|
2372
2431
|
token,
|
|
2373
2432
|
instanceId,
|
|
2374
|
-
agent
|
|
2375
|
-
options,
|
|
2376
|
-
session
|
|
2377
|
-
})
|
|
2433
|
+
agent
|
|
2378
2434
|
});
|
|
2379
2435
|
signal?.addEventListener("abort", rpc.close, { once: true });
|
|
2380
2436
|
let timer;
|
|
@@ -2409,6 +2465,7 @@ async function runSession({ options, openRpc, url, instanceId }) {
|
|
|
2409
2465
|
await emit(hooks, "studio:warn", { message: "Canceled the running generation: the connection to Kubb Studio closed" });
|
|
2410
2466
|
}
|
|
2411
2467
|
await active?.settled;
|
|
2468
|
+
await agent[Symbol.asyncDispose]();
|
|
2412
2469
|
return {
|
|
2413
2470
|
...close,
|
|
2414
2471
|
connected: session.info !== void 0
|
|
@@ -2718,8 +2775,7 @@ async function createSnapshotPackage({ files, packageInfo }) {
|
|
|
2718
2775
|
try {
|
|
2719
2776
|
await (0, node_fs_promises.mkdir)(dist);
|
|
2720
2777
|
await Promise.all([...sources].map(async ([path, { content }]) => {
|
|
2721
|
-
await (
|
|
2722
|
-
await (0, node_fs_promises.writeFile)((0, node_path.join)(root, path), content);
|
|
2778
|
+
await writeRaw((0, node_path.join)(root, path), content);
|
|
2723
2779
|
}));
|
|
2724
2780
|
const entry = [...sources.keys()].filter((path) => /\.(?:[cm]?[jt]sx?)$/.test(path)).map((path) => (0, node_path.join)(root, path));
|
|
2725
2781
|
if (entry.length) {
|
|
@@ -2759,7 +2815,7 @@ async function createSnapshotPackage({ files, packageInfo }) {
|
|
|
2759
2815
|
const chunks = [...new Map([
|
|
2760
2816
|
["package.json", JSON.stringify(manifest, null, 2)],
|
|
2761
2817
|
...[...sources].map(([path, { content }]) => [path, content]),
|
|
2762
|
-
...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))]))
|
|
2763
2819
|
])].flatMap(([path, content]) => {
|
|
2764
2820
|
const bytes = Buffer.from(content);
|
|
2765
2821
|
return [
|
|
@@ -2790,7 +2846,10 @@ async function createSnapshot({ studioUrl = defaultStudioUrl, apiKey, config, ho
|
|
|
2790
2846
|
path: "/api/snapshots"
|
|
2791
2847
|
});
|
|
2792
2848
|
const generated = await generate({
|
|
2793
|
-
config
|
|
2849
|
+
config: {
|
|
2850
|
+
...config,
|
|
2851
|
+
storage: (0, _kubb_core.fsStorage)()
|
|
2852
|
+
},
|
|
2794
2853
|
hooks,
|
|
2795
2854
|
signal,
|
|
2796
2855
|
allowExec: true
|